Merge lp:~robru/gwibber/identica into lp:~barry/gwibber/py3

Proposed by Robert Bruce Park
Status: Merged
Merged at revision: 1446
Proposed branch: lp:~robru/gwibber/identica
Merge into: lp:~barry/gwibber/py3
Prerequisite: lp:~robru/gwibber/twitter
Diff against target: 386 lines (+290/-15)
5 files modified
gwibber/gwibber/protocols/identica.py (+55/-5)
gwibber/gwibber/protocols/twitter.py (+8/-9)
gwibber/gwibber/tests/test_dbus.py (+4/-0)
gwibber/gwibber/tests/test_identica.py (+216/-0)
gwibber/gwibber/utils/time.py (+7/-1)
To merge this branch: bzr merge lp:~robru/gwibber/identica
Reviewer Review Type Date Requested Status
Barry Warsaw Pending
Review via email: mp+128095@code.launchpad.net

Description of the change

BLAM! Identica done with test coverage in an afternoon!

To post a comment you must log in.
Revision history for this message
Barry Warsaw (barry) wrote :

On Oct 04, 2012, at 07:33 PM, Robert Bruce Park wrote:

>Robert Bruce Park has proposed merging lp:~robru/gwibber/identica into lp:~barry/gwibber/py3 with lp:~robru/gwibber/twitter as a prerequisite.
>
>Requested reviews:
> Barry Warsaw (barry)
>
>For more details, see:
>https://code.launchpad.net/~robru/gwibber/identica/+merge/128095
>
>BLAM! Identica done with test coverage in an afternoon!

Great branch, thanks!

I did a little bit of reformatting and such for style (line lengths), fixed
some pyflakes warnings, and added a test for the new identi.ca time format
(tsk, tsk :).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'gwibber/gwibber/protocols/identica.py'
2--- gwibber/gwibber/protocols/identica.py 2012-09-19 22:21:39 +0000
3+++ gwibber/gwibber/protocols/identica.py 2012-10-04 19:32:24 +0000
4@@ -19,8 +19,58 @@
5 ]
6
7
8-from gwibber.utils.base import Base
9-
10-
11-class Identica(Base):
12- pass
13+from gwibber.utils.base import feature
14+from gwibber.protocols.twitter import Twitter
15+
16+
17+class Identica(Twitter):
18+ _api_base = 'http://identi.ca/api/{endpoint}.json'
19+
20+ _timeline = _api_base.format(endpoint='statuses/{}_timeline')
21+ _user_timeline = _timeline.format('user') + '?screen_name={}'
22+ _mentions_timeline = _api_base.format(endpoint='statuses/mentions')
23+
24+ _destroy = _api_base.format(endpoint='statuses/destroy/{}')
25+ _retweet = _api_base.format(endpoint='statuses/retweet/{}')
26+
27+ _search = _api_base.format(endpoint='search')
28+ _search_result_key = 'results'
29+
30+ _tweet_permalink = 'http://identi.ca/notice/{tweet_id}'
31+
32+ def _publish_tweet(self, tweet):
33+ tweet['id_str'] = str(tweet['id'])
34+ super()._publish_tweet(tweet)
35+
36+ def list(self, list_id):
37+ """Identi.ca does not have this feature."""
38+ raise NotImplementedError
39+
40+ def lists(self):
41+ """Identi.ca does not have this feature."""
42+ raise NotImplementedError
43+
44+ def like(self, tweet_id):
45+ """I get 404s on this in spite of Identi.ca's claim to support it."""
46+ raise NotImplementedError
47+
48+ def unlike(self, tweet_id):
49+ """I get 404s on this in spite of Identi.ca's claim to support it."""
50+ raise NotImplementedError
51+
52+ def tag(self, tweet_id):
53+ """Searching for hashtags gives non-hashtags in the results.
54+
55+ Eg, whereas twitter.tag('bike') only gives you tweets
56+ containing '#bike', identica.tag('bike') gives results
57+ containing both 'bike' and '#bike', which is essentially
58+ useless. Just use search() instead.
59+ """
60+ raise NotImplementedError
61+
62+ @feature
63+ def receive(self):
64+ """Gather and publish all incoming messages."""
65+ self.home()
66+ self.mentions()
67+ self.private()
68
69=== modified file 'gwibber/gwibber/protocols/twitter.py'
70--- gwibber/gwibber/protocols/twitter.py 2012-10-04 19:32:24 +0000
71+++ gwibber/gwibber/protocols/twitter.py 2012-10-04 19:32:24 +0000
72@@ -47,12 +47,16 @@
73
74 _timeline = _api_base.format(endpoint='statuses/{}_timeline')
75 _user_timeline = _timeline.format('user') + '?screen_name={}'
76+ _mentions_timeline = _timeline.format('mentions')
77
78 _lists = _api_base.format(endpoint='lists/statuses') + '?list_id={}'
79
80 _destroy = _api_base.format(endpoint='statuses/destroy/{}')
81 _retweet = _api_base.format(endpoint='statuses/retweet/{}')
82
83+ _search = _api_base.format(endpoint='search/tweets')
84+ _search_result_key = 'statuses'
85+
86 _tweet_permalink = 'https://twitter.com/{user_id}/status/{tweet_id}'
87
88 def _locked_login(self, old_token):
89@@ -138,7 +142,7 @@
90 @feature
91 def mentions(self):
92 """Gather the tweets that mention us."""
93- url = self._timeline.format('mentions')
94+ url = self._mentions_timeline
95 for tweet in self._get_url(url):
96 self._publish_tweet(tweet)
97
98@@ -280,19 +284,14 @@
99 @feature
100 def tag(self, hashtag):
101 """Return a list of some recent tweets mentioning hashtag."""
102- url = self._api_base.format(endpoint='search/tweets')
103-
104- response = self._get_url(
105- '{}?q=%23{}'.format(url, hashtag.lstrip('#')))
106- for tweet in response.get('statuses', []):
107- self._publish_tweet(tweet)
108+ self.search('#' + hashtag.lstrip('#'))
109
110 # https://dev.twitter.com/docs/api/1.1/get/search/tweets
111 @feature
112 def search(self, query):
113 """Search for any arbitrary string."""
114- url = self._api_base.format(endpoint='search/tweets')
115+ url = self._search
116
117 response = self._get_url('{}?q={}'.format(url, quote(query, safe='')))
118- for tweet in response.get('statuses', []):
119+ for tweet in response.get(self._search_result_key, []):
120 self._publish_tweet(tweet)
121
122=== modified file 'gwibber/gwibber/tests/test_dbus.py'
123--- gwibber/gwibber/tests/test_dbus.py 2012-10-04 19:32:24 +0000
124+++ gwibber/gwibber/tests/test_dbus.py 2012-10-04 19:32:24 +0000
125@@ -116,6 +116,10 @@
126 'mentions', 'private', 'receive', 'retweet', 'search',
127 'send', 'send_private', 'send_thread', 'tag',
128 'unfollow', 'unlike', 'user'])
129+ self.assertEqual(json.loads(iface.GetFeatures('identica')),
130+ ['delete', 'follow', 'home', 'mentions', 'private',
131+ 'receive', 'retweet', 'search', 'send', 'send_private',
132+ 'send_thread', 'unfollow', 'user'])
133 self.assertEqual(json.loads(iface.GetFeatures('flickr')), ['receive'])
134 self.assertEqual(json.loads(iface.GetFeatures('foursquare')),
135 ['receive'])
136
137=== added file 'gwibber/gwibber/tests/test_identica.py'
138--- gwibber/gwibber/tests/test_identica.py 1970-01-01 00:00:00 +0000
139+++ gwibber/gwibber/tests/test_identica.py 2012-10-04 19:32:24 +0000
140@@ -0,0 +1,216 @@
141+# Copyright (C) 2012 Canonical Ltd
142+#
143+# This program is free software: you can redistribute it and/or modify
144+# it under the terms of the GNU General Public License version 2 as
145+# published by the Free Software Foundation.
146+#
147+# This program is distributed in the hope that it will be useful,
148+# but WITHOUT ANY WARRANTY; without even the implied warranty of
149+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
150+# GNU General Public License for more details.
151+#
152+# You should have received a copy of the GNU General Public License
153+# along with this program. If not, see <http://www.gnu.org/licenses/>.
154+
155+"""Test the Identica plugin."""
156+
157+
158+__all__ = [
159+ 'TestIdentica',
160+ ]
161+
162+
163+import unittest
164+
165+from gi.repository import Dee
166+
167+from gwibber.protocols.identica import Identica
168+from gwibber.testing.helpers import FakeAccount
169+from gwibber.testing.mocks import LogMock
170+from gwibber.utils.model import COLUMN_TYPES
171+
172+
173+try:
174+ # Python 3.3
175+ from unittest import mock
176+except ImportError:
177+ import mock
178+
179+
180+# Create a test model that will not interfere with the user's environment.
181+# We'll use this object as a mock of the real model.
182+TestModel = Dee.SharedModel.new('com.Gwibber.TestSharedModel')
183+TestModel.set_schema_full(COLUMN_TYPES)
184+
185+
186+class TestIdentica(unittest.TestCase):
187+ """Test the Identica API."""
188+
189+ def setUp(self):
190+ self.account = FakeAccount()
191+ self.protocol = Identica(self.account)
192+ self.log_mock = LogMock('gwibber.utils.base',
193+ 'gwibber.protocols.twitter')
194+
195+ def tearDown(self):
196+ # Ensure that any log entries we haven't tested just get consumed so
197+ # as to isolate out test logger from other tests.
198+ self.log_mock.stop()
199+
200+ def test_mentions(self):
201+ get_url = self.protocol._get_url = mock.Mock(return_value=['tweet'])
202+ publish = self.protocol._publish_tweet = mock.Mock()
203+
204+ self.protocol.mentions()
205+
206+ publish.assert_called_with('tweet')
207+ get_url.assert_called_with(
208+ 'http://identi.ca/api/statuses/mentions.json')
209+
210+ def test_user(self):
211+ get_url = self.protocol._get_url = mock.Mock(return_value=['tweet'])
212+ publish = self.protocol._publish_tweet = mock.Mock()
213+
214+ self.protocol.user()
215+
216+ publish.assert_called_with('tweet')
217+ get_url.assert_called_with(
218+ 'http://identi.ca/api/statuses/user_timeline.json?screen_name=')
219+
220+ def test_list(self):
221+ get_url = self.protocol._get_url = mock.Mock(return_value=['tweet'])
222+ publish = self.protocol._publish_tweet = mock.Mock()
223+
224+ self.assertRaises(NotImplementedError,
225+ self.protocol.list,
226+ 'some_list_id')
227+
228+ def test_lists(self):
229+ get_url = self.protocol._get_url = mock.Mock(
230+ return_value=[dict(id_str='twitlist')])
231+ publish = self.protocol.list = mock.Mock()
232+
233+ self.assertRaises(NotImplementedError,
234+ self.protocol.lists)
235+
236+ def test_private(self):
237+ get_url = self.protocol._get_url = mock.Mock(return_value=['tweet'])
238+ publish = self.protocol._publish_tweet = mock.Mock()
239+
240+ self.protocol.private()
241+
242+ publish.assert_called_with('tweet')
243+ self.assertEqual(
244+ get_url.mock_calls,
245+ [mock.call('http://identi.ca/api/direct_messages.json'),
246+ mock.call('http://identi.ca/api/direct_messages/sent.json')])
247+
248+ def test_send_private(self):
249+ get_url = self.protocol._get_url = mock.Mock(return_value='tweet')
250+ publish = self.protocol._publish_tweet = mock.Mock()
251+
252+ self.protocol.send_private('pumpichank', 'Are you mocking me?')
253+
254+ publish.assert_called_with('tweet')
255+ get_url.assert_called_with(
256+ 'http://identi.ca/api/direct_messages/new.json',
257+ dict(text='Are you mocking me?', screen_name='pumpichank'))
258+
259+ def test_send(self):
260+ get_url = self.protocol._get_url = mock.Mock(return_value='tweet')
261+ publish = self.protocol._publish_tweet = mock.Mock()
262+
263+ self.protocol.send('Hello, twitterverse!')
264+
265+ publish.assert_called_with('tweet')
266+ get_url.assert_called_with(
267+ 'http://identi.ca/api/statuses/update.json',
268+ dict(status='Hello, twitterverse!'))
269+
270+ def test_send_thread(self):
271+ get_url = self.protocol._get_url = mock.Mock(return_value='tweet')
272+ publish = self.protocol._publish_tweet = mock.Mock()
273+
274+ self.protocol.send_thread(
275+ '1234',
276+ 'Why yes, I would love to respond to your tweet @pumpichank!')
277+
278+ publish.assert_called_with('tweet')
279+ get_url.assert_called_with(
280+ 'http://identi.ca/api/statuses/update.json',
281+ dict(status='Why yes, I would love to respond to your tweet @pumpichank!',
282+ in_reply_to_status_id='1234'))
283+
284+ def test_delete(self):
285+ get_url = self.protocol._get_url = mock.Mock(return_value='tweet')
286+ publish = self.protocol._unpublish = mock.Mock()
287+
288+ self.protocol.delete('1234')
289+
290+ publish.assert_called_with('1234')
291+ get_url.assert_called_with(
292+ 'http://identi.ca/api/statuses/destroy/1234.json',
293+ dict(trim_user='true'))
294+
295+ def test_retweet(self):
296+ get_url = self.protocol._get_url = mock.Mock(return_value='tweet')
297+ publish = self.protocol._publish_tweet = mock.Mock()
298+
299+ self.protocol.retweet('1234')
300+
301+ publish.assert_called_with('tweet')
302+ get_url.assert_called_with(
303+ 'http://identi.ca/api/statuses/retweet/1234.json',
304+ dict(trim_user='true'))
305+
306+ def test_unfollow(self):
307+ get_url = self.protocol._get_url = mock.Mock()
308+
309+ self.protocol.unfollow('pumpichank')
310+
311+ get_url.assert_called_with(
312+ 'http://identi.ca/api/friendships/destroy.json',
313+ dict(screen_name='pumpichank'))
314+
315+ def test_follow(self):
316+ get_url = self.protocol._get_url = mock.Mock()
317+
318+ self.protocol.follow('pumpichank')
319+
320+ get_url.assert_called_with(
321+ 'http://identi.ca/api/friendships/create.json',
322+ dict(screen_name='pumpichank', follow='true'))
323+
324+ def test_like(self):
325+ get_url = self.protocol._get_url = mock.Mock()
326+
327+ self.assertRaises(NotImplementedError,
328+ self.protocol.like,
329+ '1234')
330+
331+ def test_unlike(self):
332+ get_url = self.protocol._get_url = mock.Mock()
333+
334+ self.assertRaises(NotImplementedError,
335+ self.protocol.unlike,
336+ '1234')
337+
338+ def test_tag(self):
339+ get_url = self.protocol._get_url = mock.Mock(
340+ return_value=dict(statuses=['tweet']))
341+ publish = self.protocol._publish_tweet = mock.Mock()
342+
343+ self.assertRaises(NotImplementedError,
344+ self.protocol.tag,
345+ 'hashtag')
346+
347+ def test_search(self):
348+ get_url = self.protocol._get_url = mock.Mock(
349+ return_value=dict(results=['tweet']))
350+ publish = self.protocol._publish_tweet = mock.Mock()
351+
352+ self.protocol.search('hello')
353+
354+ publish.assert_called_with('tweet')
355+ get_url.assert_called_with(
356+ 'http://identi.ca/api/search.json?q=hello')
357
358=== modified file 'gwibber/gwibber/utils/time.py'
359--- gwibber/gwibber/utils/time.py 2012-09-20 16:26:53 +0000
360+++ gwibber/gwibber/utils/time.py 2012-10-04 19:32:24 +0000
361@@ -32,6 +32,7 @@
362 # Date time formats. Assume no microseconds and no timezone.
363 ISO8601_FORMAT = '%Y-%m-%dT%H:%M:%S'
364 TWITTER_FORMAT = '%a %b %d %H:%M:%S %Y'
365+IDENTICA_FORMAT = '%a, %d %b %Y %H:%M:%S'
366
367
368 @contextmanager
369@@ -55,11 +56,16 @@
370 return datetime.strptime(t, TWITTER_FORMAT)
371
372
373+def _from_identica(t):
374+ return datetime.strptime(t, IDENTICA_FORMAT)
375+
376+
377 def _fromutctimestamp(t):
378 return datetime.utcfromtimestamp(float(t))
379
380
381-PARSERS = (_from_iso8601, _from_iso8601alt, _from_twitter, _fromutctimestamp)
382+PARSERS = (_from_iso8601, _from_iso8601alt, _from_twitter,
383+ _from_identica, _fromutctimestamp)
384
385
386 def parsetime(t):

Subscribers

People subscribed via source and target branches