Skip to content

Commit 89c4152

Browse files
author
Steven Cummings
committed
Initial commit
0 parents  commit 89c4152

8 files changed

Lines changed: 409 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.pyc

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
PythonKC Meetup.com API Client
2+
==============================
3+
4+
A clean, minimal client to the Meetup.com API for retrieving Meetup events for
5+
the [PythonKC group](http://www.meetup.com/pythonkc/).
6+
7+
Example Usage
8+
-------------
9+
10+
>>> from pythonkc_meetups import PythonKCMeetups
11+
>>> meetups = PythonKCMeetups(api_key='<your API key here>')
12+
>>> next_meetup = meetups.get_upcoming_events()[0]
13+
>>> next_meetup.name
14+
u'Hackathon!'
15+
>>> next_meetup.time
16+
datetime.datetime(2011, 8, 13, 10, 0, tzinfo=tzoffset(None, -18000))
17+
>>> next_meetup.venue.name
18+
u"Salva O'Renick"
19+
>>> next_meetup.venue.lat, next_meetup.venue.lon
20+
(39.091053, -94.576996)
21+
>>> next_meetup.yes_rsvp_count
22+
9
23+
>>> next_meetup.event_url
24+
u'http://www.meetup.com/pythonkc/events/25940081/'

pythonkc_meetups/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Provides PythonKC Meetup.com events.
4+
5+
"""
6+
7+
8+
from pythonkc_meetups.client import PythonKCMeetups

pythonkc_meetups/client.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Provides the PythonKC Meetup.com API client implementation.
4+
5+
"""
6+
7+
8+
from pythonkc_meetups.exceptions import PythonKCMeetupsBadJson
9+
from pythonkc_meetups.exceptions import PythonKCMeetupsBadResponse
10+
from pythonkc_meetups.exceptions import PythonKCMeetupsMeetupDown
11+
from pythonkc_meetups.exceptions import PythonKCMeetupsNotJson
12+
from pythonkc_meetups.exceptions import PythonKCMeetupsRateLimitExceeded
13+
from pythonkc_meetups.parsers import parse_event
14+
import httplib2
15+
import json
16+
import mimeparse
17+
import urllib
18+
19+
20+
EVENTS_URL = 'https://api.meetup.com/2/events.json'
21+
GROUP_URLNAME = 'pythonkc'
22+
23+
24+
class PythonKCMeetups(object):
25+
26+
"""
27+
Retrieves information about PythonKC meetups.
28+
29+
"""
30+
31+
def __init__(self, api_key, http_timeout=1, http_retries=2):
32+
"""
33+
Create a new instance.
34+
35+
Parameters
36+
----------
37+
api_key
38+
The Meetup.com API consumer key to make requests with.
39+
http_timeout
40+
Time, in seconds, to give HTTP requests to complete.
41+
http_retries
42+
The number of times to retry requests when it is appropriate to do
43+
so.
44+
45+
"""
46+
self._api_key = api_key
47+
self._http_timeout = http_timeout
48+
self._http_retries = http_retries
49+
50+
def get_upcoming_events(self):
51+
"""
52+
Get the upcoming PythonKC meetup events.
53+
54+
Returns
55+
-------
56+
List of ``pythonkc_meetups.types.MeetupEvent``, ordered by event time,
57+
ascending.
58+
59+
"""
60+
61+
query = urllib.urlencode({'key': self._api_key,
62+
'group_urlname': GROUP_URLNAME})
63+
url = '{0}?{1}'.format(EVENTS_URL, query)
64+
data = self._http_get_json(url)
65+
events = data['results']
66+
return [parse_event(event) for event in events]
67+
68+
def _http_get_json(self, url):
69+
"""
70+
Make an HTTP GET request to the specified URL, check that it returned a
71+
JSON response, and returned the data parsed from that response.
72+
73+
Parameters
74+
----------
75+
url
76+
The URL to GET.
77+
78+
Returns
79+
-------
80+
Dictionary of data parsed from a JSON HTTP response.
81+
82+
Exceptions
83+
----------
84+
* PythonKCMeetupsBadJson
85+
* PythonKCMeetupsNotJson
86+
* PythonKCMeetupsMeetupDown
87+
* PythonKCMeetupsRateLimitExceeded
88+
* PythonKCMeetupsBadResponse
89+
90+
"""
91+
response, content = self._http_get(url)
92+
93+
content_type = response['content-type']
94+
parsed_mimetype = mimeparse.parse_mime_type(content_type)
95+
if parsed_mimetype[1] not in ('json', 'javascript'):
96+
raise PythonKCMeetupsNotJson(content_type)
97+
98+
try:
99+
return json.loads(content)
100+
except ValueError as e:
101+
raise PythonKCMeetupsBadJson(e)
102+
103+
def _http_get(self, url):
104+
"""
105+
Make an HTTP GET request to the specified URL and return the response.
106+
107+
Retries
108+
-------
109+
The constructor of this class takes an argument specifying the number
110+
of times to retry a GET. The statuses which are retried on are: 408,
111+
500, 502, 503, and 504.
112+
113+
Returns
114+
-------
115+
A tuple of response, content, where response is a dictionary of
116+
response metadata (headers), and content is the entity body returned.
117+
118+
Exceptions
119+
----------
120+
* PythonKCMeetupsMeetupDown
121+
* PythonKCMeetupsRateLimitExceeded
122+
* PythonKCMeetupsBadResponse
123+
124+
"""
125+
for try_number in range(self._http_retries + 1):
126+
client = httplib2.Http(timeout=self._http_timeout)
127+
response, content = client.request(url, 'GET')
128+
if response.status == 200:
129+
return response, content
130+
131+
if (try_number >= self._http_retries or
132+
response.status not in (408, 500, 502, 503, 504)):
133+
134+
if response.status >= 500:
135+
raise PythonKCMeetupsMeetupDown(response, content)
136+
if response.status == 400:
137+
try:
138+
data = json.loads(content)
139+
if data.get('code', None) == 'limit':
140+
raise PythonKCMeetupsRateLimitExceeded
141+
except: # Don't lose original error when JSON is bad
142+
pass
143+
raise PythonKCMeetupsBadResponse(response, content)

pythonkc_meetups/exceptions.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Overview
4+
========
5+
Defines exceptions raised by meetupy.
6+
7+
"""
8+
9+
10+
class PythonKCMeetupsException(Exception):
11+
"""
12+
Indicates a general problem interacting with the Meetup.com API.
13+
14+
This is meant to be the base type for exceptions that meetupy raises and
15+
shouldn't itself be raised.
16+
17+
"""
18+
pass
19+
20+
21+
class PythonKCMeetupsBadResponse(PythonKCMeetupsException):
22+
"""
23+
Indicates that there was a problem with a response returned by the
24+
Meetup.com API.
25+
26+
This may include client-error status codes (i.e., 4XX), but those are not
27+
expected as this client was coded to the HTTP spec and Meetup.com's API.
28+
29+
"""
30+
pass
31+
32+
33+
class PythonKCMeetupsNotJson(PythonKCMeetupsBadResponse):
34+
"""
35+
Indicates that a media type other than JSON was returned where JSON was
36+
expected. Either ``application/json`` or ``text/javascript`` is expected in
37+
the ``Content-Type`` header of API HTTP responses.
38+
39+
"""
40+
pass
41+
42+
43+
class PythonKCMeetupsBadJson(PythonKCMeetupsBadResponse):
44+
"""
45+
Indicates that the data returned from the server could not be parsed as
46+
JSON, despite the content type indicating a JSON response.
47+
48+
"""
49+
pass
50+
51+
52+
class PythonKCMeetupsMeetupDown(PythonKCMeetupsException):
53+
"""
54+
Indicates that a server error status code (i.e, 5XX) was returned from the
55+
Meetup.com API.
56+
57+
"""
58+
pass
59+
60+
61+
class PythonKCMeetupsRateLimitExceeded(PythonKCMeetupsException):
62+
"""
63+
Indicates that the rate limit has been exceed for the current API key or
64+
OAuth token.
65+
66+
"""
67+
pass

pythonkc_meetups/parsers.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Provides functions for converting Meetup.com API response data into the types
4+
returned by the PythonKC meetup client.
5+
6+
"""
7+
8+
9+
from dateutil.tz import tzoffset
10+
from dateutil.tz import tzutc
11+
from pythonkc_meetups.types import MeetupEvent
12+
from pythonkc_meetups.types import MeetupVenue
13+
import datetime
14+
15+
16+
def parse_event(data):
17+
"""
18+
Parse a ``MeetupEvent`` from the given response data.
19+
20+
Returns
21+
-------
22+
A ``pythonkc_meetups.types.MeetupEvent``.
23+
24+
"""
25+
return MeetupEvent(
26+
id=data.get('id', None),
27+
name=data.get('name', None),
28+
description=data.get('description', None),
29+
time=parse_datetime(data.get('time', None),
30+
data.get('utc_offset', None)),
31+
status=data.get('status', None),
32+
yes_rsvp_count=data.get('yes_rsvp_count', None),
33+
maybe_rsvp_count=data.get('maybe_rsvp_count', None),
34+
event_url=data.get('event_url', None),
35+
photo_url=data.get('photo_url', None),
36+
venue=parse_venue(data.get('venue', None))
37+
)
38+
39+
40+
def parse_venue(data):
41+
"""
42+
Parse a ``MeetupVenue`` from the given response data.
43+
44+
Returns
45+
-------
46+
A `pythonkc_meetups.types.`MeetupVenue`` if non-empty data is given,
47+
otherwise ``None``.
48+
49+
"""
50+
if data:
51+
return MeetupVenue(
52+
id=data.get('id', None),
53+
name=data.get('name', None),
54+
address_1=data.get('address_1', None),
55+
address_2=data.get('address_2', None),
56+
address_3=data.get('address_3', None),
57+
city=data.get('city', None),
58+
state=data.get('state', None),
59+
zip=data.get('zip', None),
60+
country=data.get('country', None),
61+
lat=data.get('lat', None),
62+
lon=data.get('lon', None)
63+
)
64+
65+
66+
def parse_datetime(utc_timestamp_ms, utc_offset_ms):
67+
"""
68+
Create a timezone-aware ``datetime.datetime`` from the given UTC timestamp
69+
(in milliseconds), if provided. Also, the offset is applied, if given.
70+
71+
Parameters
72+
----------
73+
utc_timestamp_ms
74+
UTC timestamp in milliseconds.
75+
utc_offset_ms
76+
Offset from UTC, in milliseconds, to apply to the time.
77+
78+
Returns
79+
-------
80+
A ``datetime.datetime`` if a timestamp is given, otherwise ``None``.
81+
82+
"""
83+
if utc_timestamp_ms:
84+
utc_timestamp_s = utc_timestamp_ms / 1000
85+
dt = datetime.datetime.fromtimestamp(utc_timestamp_s, tzutc())
86+
87+
if utc_offset_ms:
88+
utc_offset_s = utc_offset_ms / 1000
89+
tz_offset = tzoffset(None, utc_offset_s)
90+
dt = dt.astimezone(tz_offset)
91+
92+
return dt

0 commit comments

Comments
 (0)