Merge lp:~robru/gwibber/identica into lp:~barry/gwibber/py3
- identica
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Warsaw | Pending | ||
Review via email: mp+128095@code.launchpad.net |
Commit message
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 : | # |
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): |
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. /code.launchpad .net/~robru/ gwibber/ identica/ +merge/ 128095
>
>Requested reviews:
> Barry Warsaw (barry)
>
>For more details, see:
>https:/
>
>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 :).