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

Proposed by Robert Bruce Park
Status: Merged
Merged at revision: 1435
Proposed branch: lp:~robru/gwibber/foursquare
Merge into: lp:~barry/gwibber/py3
Diff against target: 442 lines (+241/-39) (has conflicts)
7 files modified
gwibber/gwibber/protocols/foursquare.py (+98/-0)
gwibber/gwibber/tests/data/foursquare-full.dat (+1/-0)
gwibber/gwibber/tests/test_foursquare.py (+108/-0)
gwibber/gwibber/tests/test_model.py (+2/-2)
gwibber/gwibber/tests/test_protocols.py (+23/-31)
gwibber/gwibber/utils/base.py (+4/-4)
gwibber/gwibber/utils/model.py (+5/-2)
Text conflict in gwibber/gwibber/protocols/foursquare.py
To merge this branch: bzr merge lp:~robru/gwibber/foursquare
Reviewer Review Type Date Requested Status
Barry Warsaw Pending
Review via email: mp+125372@code.launchpad.net

Description of the change

BLAM.

To post a comment you must log in.
lp:~robru/gwibber/foursquare updated
1435. By Robert Bruce Park

Add likes count and avatar URL to FourSquare.receive method.

Also committing foursquare-full.dat, which is a real live json data
dump of a real live foursquare checkin, that we use for testing. It
was supposed to be part of the previous commit, but I forgot to add it.

1436. By Robert Bruce Park

Modify Base._publish to calculate the service name on it's own.

This alleviates the need for every single protocol having to specify
'self.__class__.__name__.lower()' every single time that it wants to
call Base._publish. Instead, Base._publish simply calls that ugliness
just once, in one place.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'gwibber/gwibber/protocols/foursquare.py'
2--- gwibber/gwibber/protocols/foursquare.py 2012-09-19 22:21:39 +0000
3+++ gwibber/gwibber/protocols/foursquare.py 2012-09-20 03:15:40 +0000
4@@ -19,8 +19,106 @@
5 ]
6
7
8+import gettext
9+import logging
10+import datetime
11+
12+from gwibber.errors import AuthorizationError
13+from gwibber.utils.authentication import Authentication
14 from gwibber.utils.base import Base
15+from gwibber.utils.download import get_json
16+from gwibber.utils.results import Results
17+
18+
19+log = logging.getLogger('gwibber.service')
20+_ = gettext.lgettext
21+
22+# The '&v=YYYYMMDD' defines the date that the API was last confirmed
23+# to be functional, and is used by foursquare to indicate how old our
24+# software is. In the event that they change their API, an old 'v'
25+# date will tell them to give us the old, deprecated API behaviors,
26+# giving us some time to be notified of API breakage and update
27+# accordingly. If you're working on this code and you don't see any
28+# bugs with foursquare then feel free to update the date here.
29+API_BASE = 'https://api.foursquare.com/v2/'
30+TOKEN ='?oauth_token={access_token}&v=20120917'
31+SELF_URL = API_BASE + 'users/self' + TOKEN
32+CHECKIN_URL = API_BASE + 'checkins/{checkin_id}' + TOKEN
33+RECENT_URL = API_BASE + 'checkins/recent' + TOKEN
34+
35+HTML_PREFIX = 'https://foursquare.com/'
36+USER_URL = HTML_PREFIX + 'user/{user_id}'
37+VENUE_URL = HTML_PREFIX + 'venue/{venue_id}'
38+
39+
40+def _full_name(user):
41+ names = (user.get('firstName'), user.get('lastName'))
42+ return ' '.join(name for name in names if name)
43
44
45 class FourSquare(Base):
46+<<<<<<< TREE
47 pass
48+=======
49+ def _locked_login(self, old_token):
50+ log.debug('{} to FourSquare'.format(
51+ 'Re-authenticating' if old_token else 'Logging in'))
52+
53+ result = Authentication(self.account, log).login()
54+ if result is None:
55+ log.error('No FourSquare authentication results received.')
56+ return
57+
58+ token = result.get('AccessToken')
59+ if token is not None:
60+ data = get_json(SELF_URL.format(access_token=token)) or {}
61+ user = data.get('response', {}).get('user', {})
62+ names = (user.get('firstName'), user.get('lastName'))
63+ uid = user['id']
64+
65+ self.account.update(username=_full_name(user),
66+ access_token=token,
67+ user_id=uid)
68+ log.debug('FourSquare UID is: ' + uid)
69+ else:
70+ log.error('No AccessToken in FourSquare session: {!r}', result)
71+
72+ def receive(self):
73+ """Gets a list of each friend's most recent check-ins."""
74+ if 'access_token' not in self.account and not self._login():
75+ log.error('FourSquare: {}'.format(_('Unable to authenticate.')))
76+
77+ token = self.account.get('access_token')
78+ if token is not None:
79+ result = get_json(RECENT_URL.format(access_token=token))
80+
81+ response_code = result.get('meta', {}).get('code')
82+ if response_code != 200:
83+ log.error('FourSquare: Error: {}'.format(result))
84+ return
85+
86+ checkins = result.get('response', {}).get('recent', [])
87+ for checkin in checkins:
88+ user = checkin.get('user', {})
89+ avatar = user.get('photo', {})
90+ avatar_url = '{prefix}100x100{suffix}'.format(**avatar)
91+ checkin_id = checkin.get('id')
92+ tz = checkin.get('timeZoneOffset', 0)
93+ epoch = checkin.get('createdAt')
94+ timestamp = datetime.datetime.fromtimestamp(
95+ epoch - (tz * 36)).isoformat()
96+
97+ self._publish(
98+ account_id=self.account['id'],
99+ message_id=checkin_id,
100+ stream='messages',
101+ sender=_full_name(user),
102+ from_me=(user.get('relationship') == 'self'),
103+ timestamp=timestamp,
104+ message=checkin.get('shout', ''),
105+ likes=checkin.get('likes', {}).get('count', 0),
106+ icon_uri=avatar_url,
107+ url=CHECKIN_URL.format(access_token=token,
108+ checkin_id=checkin_id),
109+ )
110+>>>>>>> MERGE-SOURCE
111
112=== added file 'gwibber/gwibber/tests/data/foursquare-full.dat'
113--- gwibber/gwibber/tests/data/foursquare-full.dat 1970-01-01 00:00:00 +0000
114+++ gwibber/gwibber/tests/data/foursquare-full.dat 2012-09-20 03:15:40 +0000
115@@ -0,0 +1,1 @@
116+{"meta":{"code":200},"notifications":[{"type":"notificationTray","item":{"unreadCount":0}}],"response":{"recent":[{"id":"50574c9ce4b0a9a6e84433a0","createdAt":1347898524,"type":"checkin","shout":"Working on gwibber's foursquare plugin.","timeZoneOffset":-300,"user":{"id":"37199983","firstName":"Jimbob","lastName":"Smith","relationship":"self","photo":{"prefix":"https:\/\/irs0.4sqi.net\/img\/user\/","suffix":"\/5IEW3VIX55BBEXAO.jpg"}},"venue":{"id":"4e73f722fa76059700582a27","name":"Pop Soda's Coffee House & Gallery","contact":{"phone":"2044157666","formattedPhone":"(204) 415-7666","twitter":"PopSodasWpg"},"location":{"address":"625 Portage Ave.","crossStreet":"Furby St.","lat":49.88873164336725,"lng":-97.158043384552,"postalCode":"R3B 2G4","city":"Winnipeg","state":"MB","country":"Canada","cc":"CA"},"categories":[{"id":"4bf58dd8d48988d16d941735","name":"Café","pluralName":"Cafés","shortName":"Café","icon":{"prefix":"https:\/\/foursquare.com\/img\/categories_v2\/food\/cafe_","suffix":".png"},"primary":true}],"verified":false,"stats":{"checkinsCount":216,"usersCount":84,"tipCount":8},"url":"http:\/\/www.popsodascoffeehouse.com","likes":{"count":0,"groups":[]},"friendVisits":{"count":0,"summary":"You've been here","items":[{"visitedCount":1,"liked":false,"user":{"id":"37199983","firstName":"Jimbob","lastName":"Smith","relationship":"self","photo":{"prefix":"https:\/\/irs0.4sqi.net\/img\/user\/","suffix":"\/5IEW3VIX55BBEXAO.jpg"}}}]},"beenHere":{"count":1,"marked":true},"specials":{"count":0}},"source":{"name":"foursquare for Android","url":"https:\/\/foursquare.com\/download\/#\/android"},"photos":{"count":0,"items":[]}}]}}
117
118=== added file 'gwibber/gwibber/tests/test_foursquare.py'
119--- gwibber/gwibber/tests/test_foursquare.py 1970-01-01 00:00:00 +0000
120+++ gwibber/gwibber/tests/test_foursquare.py 2012-09-20 03:15:40 +0000
121@@ -0,0 +1,108 @@
122+# Copyright (C) 2012 Canonical Ltd
123+#
124+# This program is free software: you can redistribute it and/or modify
125+# it under the terms of the GNU General Public License version 2 as
126+# published by the Free Software Foundation.
127+#
128+# This program is distributed in the hope that it will be useful,
129+# but WITHOUT ANY WARRANTY; without even the implied warranty of
130+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
131+# GNU General Public License for more details.
132+#
133+# You should have received a copy of the GNU General Public License
134+# along with this program. If not, see <http://www.gnu.org/licenses/>.
135+
136+"""Test the FourSquare plugin."""
137+
138+__all__ = [
139+ 'TestFourSquare',
140+ ]
141+
142+
143+import unittest
144+
145+from gi.repository import Dee
146+
147+from gwibber.errors import AuthorizationError
148+from gwibber.testing.helpers import FakeAccount, LogPreserver
149+from gwibber.testing.mocks import FakeData
150+from gwibber.protocols.foursquare import FourSquare
151+from gwibber.utils.model import COLUMN_TYPES
152+
153+
154+try:
155+ # Python 3.3
156+ from unittest import mock
157+except ImportError:
158+ import mock
159+
160+
161+# Create a test model that will not interfere with the user's environment.
162+# We'll use this object as a mock of the real model.
163+TestModel = Dee.SharedModel.new('com.Gwibber.TestSharedModel')
164+TestModel.set_schema_full(COLUMN_TYPES)
165+
166+
167+# Disable i18n translations so we only need to test English responses.
168+@mock.patch.dict('gwibber.protocols.foursquare.__dict__', {'_': lambda s: s})
169+class TestFourSquare(unittest.TestCase):
170+ """Test the FourSquare API."""
171+
172+ @classmethod
173+ def setUpClass(cls):
174+ cls._log_state = LogPreserver()
175+
176+ @classmethod
177+ def tearDownClass(cls):
178+ cls._log_state.restore()
179+
180+ def setUp(self):
181+ self.account = FakeAccount()
182+ self.protocol = FourSquare(self.account)
183+
184+ def test_protocol_info(self):
185+ self.assertEqual(self.protocol.__class__.__name__, 'FourSquare')
186+
187+ @mock.patch('gwibber.utils.authentication.Authentication.login',
188+ return_value=None)
189+ @mock.patch('gwibber.utils.download.get_json',
190+ return_value=None)
191+ def test_unsuccessful_authentication(self, *mocks):
192+ self.assertFalse(self.protocol._login())
193+ self.assertNotIn('username', self.account)
194+ self.assertNotIn('user_id', self.account)
195+
196+ @mock.patch('gwibber.utils.authentication.Authentication.login',
197+ return_value=dict(AccessToken='tokeny goodness'))
198+ @mock.patch('gwibber.protocols.foursquare.get_json',
199+ return_value=dict(
200+ response=dict(
201+ user=dict(firstName='Bob',
202+ lastName='Loblaw',
203+ id='1234567'))))
204+ def test_successful_authentication(self, *mocks):
205+ self.assertTrue(self.protocol._login())
206+ self.assertEqual(self.account['username'], 'Bob Loblaw')
207+ self.assertEqual(self.account['user_id'], '1234567')
208+
209+ @mock.patch('gwibber.utils.base.Model', TestModel)
210+ @mock.patch('gwibber.utils.download.urlopen',
211+ FakeData('gwibber.tests.data', 'foursquare-full.dat'))
212+ @mock.patch('gwibber.protocols.foursquare.FourSquare._login',
213+ return_value=True)
214+ def test_receive(self, *mocks):
215+ self.account['access_token'] = 'tokeny goodness'
216+ self.assertEqual(0, TestModel.get_n_rows())
217+ self.protocol.receive()
218+ self.assertEqual(1, TestModel.get_n_rows())
219+ expected = [
220+ [['foursquare', 'faker/than fake', '50574c9ce4b0a9a6e84433a0']],
221+ 'messages', 'Jimbob Smith', '', True, '2012-09-17T14:15:24',
222+ "Working on gwibber's foursquare plugin.", '',
223+ 'https://irs0.4sqi.net/img/user/100x100/5IEW3VIX55BBEXAO.jpg',
224+ 'https://api.foursquare.com/v2/checkins/50574c9ce4b0a9a6e84433a0' +
225+ '?oauth_token=tokeny goodness&v=20120917', '', '', '', '', 0.0,
226+ False, '', '', '', '', '', '', '', '', '', '', '', '', '', '',
227+ '', '', '', '', '', '', '']
228+ for i, col in enumerate(TestModel[0]):
229+ self.assertEqual(col, expected[i])
230
231=== modified file 'gwibber/gwibber/tests/test_model.py'
232--- gwibber/gwibber/tests/test_model.py 2012-09-19 20:05:40 +0000
233+++ gwibber/gwibber/tests/test_model.py 2012-09-20 03:15:40 +0000
234@@ -34,10 +34,10 @@
235
236 def test_basic_properties(self):
237 self.assertIsInstance(Model, Dee.SharedModel)
238- self.assertEqual(Model.get_n_columns(), 39)
239+ self.assertEqual(Model.get_n_columns(), 37)
240 self.assertEqual(Model.get_n_rows(), 0)
241 self.assertEqual(Model.get_schema(),
242- ['aas', 's', 's', 's', 's', 'b', 's', 's', 's', 's',
243+ ['aas', 's', 's', 's', 'b', 's', 's', 's',
244 's', 's', 's', 's', 's', 's', 'd', 'b', 's', 's',
245 's', 's', 's', 's', 's', 's', 's', 's', 's', 's',
246 's', 's', 's', 's', 's', 's', 's', 's', 's'])
247
248=== modified file 'gwibber/gwibber/tests/test_protocols.py'
249--- gwibber/gwibber/tests/test_protocols.py 2012-09-19 22:21:39 +0000
250+++ gwibber/gwibber/tests/test_protocols.py 2012-09-20 03:15:40 +0000
251@@ -141,12 +141,9 @@
252 self.assertEqual(Model.get_n_rows(), 0)
253 self.assertEqual(TestModel.get_n_rows(), 0)
254 base = Base(None)
255- base._publish('a', 'b', 'c', message='a',
256- from_me=True, likes=1, liked=True)
257- base._publish('a', 'b', 'c', message='b',
258- from_me=True, likes=1, liked=True)
259- base._publish('a', 'b', 'c', message='c',
260- from_me=True, likes=1, liked=True)
261+ base._publish('a', 'b', message='a')
262+ base._publish('a', 'b', message='b')
263+ base._publish('a', 'b', message='c')
264 self.assertEqual(Model.get_n_rows(), 0)
265 self.assertEqual(TestModel.get_n_rows(), 3)
266
267@@ -155,7 +152,7 @@
268 base = Base(None)
269 self.assertEqual(0, TestModel.get_n_rows())
270 with self.assertRaises(TypeError) as cm:
271- base._publish('x', 'y', 'z', invalid_argument='not good')
272+ base._publish('x', 'y', invalid_argument='not good')
273 self.assertEqual(str(cm.exception),
274 'Unexpected keyword arguments: invalid_argument')
275
276@@ -165,7 +162,7 @@
277 base = Base(None)
278 self.assertEqual(0, TestModel.get_n_rows())
279 with self.assertRaises(TypeError) as cm:
280- base._publish('x', 'y', 'z', bad='no', wrong='yes')
281+ base._publish('x', 'y', bad='no', wrong='yes')
282 self.assertEqual(str(cm.exception),
283 'Unexpected keyword arguments: bad, wrong')
284
285@@ -176,17 +173,16 @@
286 base = Base(None)
287 self.assertEqual(0, TestModel.get_n_rows())
288 self.assertTrue(base._publish(
289- service='facebook', account_id='1/facebook',
290- message_id='1234', stream='messages', sender='fred',
291- sender_nick='freddy', from_me=True, timestamp='today',
292- message='hello, @jimmy', likes=10, liked=True))
293+ account_id='1/facebook', message_id='1234', stream='messages',
294+ sender='fred', sender_nick='freddy', from_me=True,
295+ timestamp='today', message='hello, @jimmy', likes=10, liked=True))
296 self.assertEqual(1, TestModel.get_n_rows())
297 row = TestModel.get_row(0)
298 # For convenience.
299 def V(column_name):
300 return row[COLUMN_INDICES[column_name]]
301 self.assertEqual(V('message_ids'),
302- [['facebook', '1/facebook', '1234']])
303+ [['base', '1/facebook', '1234']])
304 self.assertEqual(V('stream'), 'messages')
305 self.assertEqual(V('sender'), 'fred')
306 self.assertEqual(V('sender_nick'), 'freddy')
307@@ -215,18 +211,16 @@
308 # Insert the first message into the table. The key will be the string
309 # 'fredhellojimmy'
310 self.assertTrue(base._publish(
311- service='facebook', account_id='1/facebook',
312- message_id='1234', stream='messages', sender='fred',
313- sender_nick='freddy', from_me=True, timestamp='today',
314- message='hello, @jimmy', likes=10, liked=True))
315+ account_id='1/facebook', message_id='1234', stream='messages',
316+ sender='fred', sender_nick='freddy', from_me=True,
317+ timestamp='today', message='hello, @jimmy', likes=10, liked=True))
318 # Insert the second message into the table. Note that because
319 # punctuation was stripped from the above message, this one will also
320 # have the key 'fredhellojimmy', thus it will be deemed a duplicate.
321 self.assertTrue(base._publish(
322- service='twitter', account_id='2/twitter',
323- message_id='5678', stream='messages', sender='fred',
324- sender_nick='freddy', from_me=True, timestamp='today',
325- message='hello jimmy', likes=10, liked=False))
326+ account_id='2/twitter', message_id='5678', stream='messages',
327+ sender='fred', sender_nick='freddy', from_me=True,
328+ timestamp='today', message='hello jimmy', likes=10, liked=False))
329 # See, we get only one row in the table.
330 self.assertEqual(1, TestModel.get_n_rows())
331 # The first published message wins.
332@@ -234,8 +228,8 @@
333 self.assertEqual(row[COLUMN_INDICES['message']], 'hello, @jimmy')
334 # Both message ids will be present, in the order they were published.
335 self.assertEqual(row[COLUMN_INDICES['message_ids']],
336- [['facebook', '1/facebook', '1234'],
337- ['twitter', '2/twitter', '5678']])
338+ [['base', '1/facebook', '1234'],
339+ ['base', '2/twitter', '5678']])
340
341 @mock.patch('gwibber.utils.base.Model', TestModel)
342 @mock.patch('gwibber.utils.base._seen_messages', {})
343@@ -247,17 +241,15 @@
344 self.assertEqual(0, TestModel.get_n_rows())
345 # The key for this row is 'fredhellojimmy'
346 self.assertTrue(base._publish(
347- service='facebook', account_id='1/facebook',
348- message_id='1234', stream='messages', sender='fred',
349- sender_nick='freddy', from_me=True, timestamp='today',
350- message='hello, @jimmy', likes=10, liked=True))
351+ account_id='1/facebook', message_id='1234', stream='messages',
352+ sender='fred', sender_nick='freddy', from_me=True,
353+ timestamp='today', message='hello, @jimmy', likes=10, liked=True))
354 self.assertEqual(1, TestModel.get_n_rows())
355 # The key for this row is 'tedtholomewhellojimmy'
356 self.assertTrue(base._publish(
357- service='facebook', account_id='1/facebook',
358- message_id='34567', stream='messages', sender='tedtholomew',
359- sender_nick='teddy', from_me=False, timestamp='today',
360- message='hello, @jimmy', likes=10, liked=True))
361+ account_id='1/facebook', message_id='34567', stream='messages',
362+ sender='tedtholomew', sender_nick='teddy', from_me=False,
363+ timestamp='today', message='hello, @jimmy', likes=10, liked=True))
364 # See? Two rows in the table.
365 self.assertEqual(2, TestModel.get_n_rows())
366 # The first row is the message from fred.
367
368=== modified file 'gwibber/gwibber/utils/base.py'
369--- gwibber/gwibber/utils/base.py 2012-09-19 22:21:39 +0000
370+++ gwibber/gwibber/utils/base.py 2012-09-20 03:15:40 +0000
371@@ -25,7 +25,7 @@
372
373 from threading import Lock, Thread
374
375-from gwibber.utils.model import COLUMN_INDICES, SCHEMA, Model
376+from gwibber.utils.model import COLUMN_INDICES, SCHEMA, DEFAULTS, Model
377
378
379 IGNORED = string.punctuation + string.whitespace
380@@ -98,7 +98,7 @@
381 thread.daemon = True
382 thread.start()
383
384- def _publish(self, service, account_id, message_id, **kwargs):
385+ def _publish(self, account_id, message_id, **kwargs):
386 """Publish fresh data into the model, ignoring duplicates.
387
388 :param service: The name of the service this message is published
389@@ -125,7 +125,7 @@
390 # The column value is a list of lists (see gwibber/utils/model.py for
391 # details), and because the arguments are themselves a list, this gets
392 # initialized as a triply-nested list.
393- args = [[[service, account_id, message_id]]]
394+ args = [[[self.__class__.__name__.lower(), account_id, message_id]]]
395 # Now iterate through all the column names listed in the SCHEMA,
396 # except for the first, since we just composed its value in the
397 # preceding line. Pop matching column values from the kwargs, in the
398@@ -135,7 +135,7 @@
399 #
400 # Missing column values default to the empty string.
401 for column_name, column_type in SCHEMA[1:]:
402- args.append(kwargs.pop(column_name, ''))
403+ args.append(kwargs.pop(column_name, DEFAULTS[column_type]))
404 if len(kwargs) > 0:
405 raise TypeError('Unexpected keyword arguments: {}'.format(
406 COMMA_SPACE.join(sorted(kwargs))))
407
408=== modified file 'gwibber/gwibber/utils/model.py'
409--- gwibber/gwibber/utils/model.py 2012-09-19 20:05:40 +0000
410+++ gwibber/gwibber/utils/model.py 2012-09-20 03:15:40 +0000
411@@ -27,6 +27,7 @@
412 'COLUMN_NAMES',
413 'COLUMN_TYPES',
414 'COLUMN_INDICES',
415+ 'DEFAULTS',
416 ]
417
418
419@@ -54,7 +55,6 @@
420 SCHEMA = (
421 ('message_ids', 'aas'),
422 ('stream', 's'),
423- ('transient', 's'),
424 ('sender', 's'),
425 ('sender_nick', 's'),
426 ('from_me', 'b'),
427@@ -64,7 +64,6 @@
428 ('icon_uri', 's'),
429 ('url', 's'),
430 ('source', 's'),
431- ('timestring', 's'),
432 ('reply_nick', 's'),
433 ('reply_name', 's'),
434 ('reply_url', 's'),
435@@ -103,3 +102,7 @@
436
437 Model = Dee.SharedModel.new('com.Gwibber.Streams')
438 Model.set_schema_full(COLUMN_TYPES)
439+
440+
441+# This defines default values for the different data types
442+DEFAULTS = dict(s='', b=False, d=0)

Subscribers

People subscribed via source and target branches