Skip to content
This repository was archived by the owner on Jun 11, 2024. It is now read-only.

Commit 9b4a959

Browse files
committed
add support for api version 3.
- Always fast reporting. No more polling if reports are ready - Use authentication token in HTTP header instead of cookies - Model and attribute changes
1 parent b6261e7 commit 9b4a959

File tree

7 files changed

+95
-211
lines changed

7 files changed

+95
-211
lines changed

CHANGES.txt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@
1212
* :class:`atomx.models.PlacementType`
1313
* :class:`atomx.models.Visibility`
1414

15-
- Add :meth:`atomx.models.ScheduledReport.save` to edit ``name`` and ``emails``
16-
of a :class:`atomx.models.ScheduledReport`
15+
- Add :meth:`atomx.models.Report.save` to edit ``name`` and ``emails``
16+
of a :class:`atomx.models.Report`
1717
- :meth:`atomx.Atomx.get` also accepts a model class or instance as resource argument.
1818
E.g.: ``atomx_api.get(atomx.models.Advertiser)`` or ``atomx_api.get(atomx.models.Advertiser(42))``
1919
- Add :mod:`pickle` support for :mod:`atomx.models`.
2020
- Save HTTP headers in :attr:`atomx.Atomx.last_response`.
2121
- Add history support. :meth:`atomx.models.AtomxModel.history`.
22+
- Use api version 3::
23+
24+
* Fast reporting. No more polling if reports are ready.
25+
* Use authentication token in HTTP header instead of cookies.
26+
* Model and attribute changes
2227

2328

2429
1.4

README.rst

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,12 @@ Example Usage
6969
7070
# reporting example
7171
# get a report for a specific publisher
72-
report = atomx.report(scope='publisher', groups=['hour_formatted'], sums=['impressions', 'clicks'], where=[['publisher_id', '==', 42]], from_='2015-02-08 00:00:00', to='2015-02-09 00:00:00', timezone='America/Los_Angeles')
72+
report = atomx.report(scope='publisher', groups=['hour'], metrics=['impressions', 'clicks'], where=[['publisher_id', '==', 42]], from_='2015-02-08 00:00:00', to='2015-02-09 00:00:00', timezone='America/Los_Angeles')
7373
# check if report is ready
7474
print(report.is_ready)
7575
# if pandas is installed you can get the pandas dataframe with `report.pandas`
7676
# you can also get the report csv in `report.content` without pandas
77-
df = report.pandas
78-
# set index to datetime
79-
import pandas as pd
80-
df.index = pd.to_datetime(df.pop('hour_formatted'))
77+
df = report.pandas # A datetime index is automatically set when group by a hour/day/month.
8178
# calculate mean, median, std per hour
8279
means = df.resample('H', how=['mean', 'median', 'std'])
8380
# and plot impression and clicks per day

atomx/__init__.py

Lines changed: 48 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -42,50 +42,48 @@ class Atomx(object):
4242
(without the resource payload) in :attr:`.Atomx.last_response`. (default: `True`)
4343
:return: :class:`.Atomx` session to interact with the api
4444
"""
45-
def __init__(self, email, password, api_endpoint=API_ENDPOINT, save_response=True):
46-
self.auth_tkt = None
45+
def __init__(self, email, password,
46+
api_endpoint=API_ENDPOINT, save_response=True, expiration=None):
47+
self.auth_token = None
4748
self.user = None
48-
self.email = email
49-
self.password = password
5049
self.api_endpoint = api_endpoint.rstrip('/') + '/'
5150
self.save_response = save_response
5251
#: Contains the response of the last api call, if `save_response` was set `True`
5352
self.last_response = None
54-
self.session = requests.Session()
55-
self.login()
53+
self.login(email, password, expiration)
5654

57-
def login(self, email=None, password=None):
55+
@property
56+
def _auth_header(self):
57+
if self.auth_token:
58+
return {'Authorization': 'Bearer ' + self.auth_token}
59+
60+
def login(self, email, password, expiration=None):
5861
"""Gets new authentication token for user ``email``.
5962
6063
This method is automatically called in :meth:`__init__` so
6164
you rarely have to call this method directly.
6265
63-
:param str email: Use this email instead of the one provided at
64-
construction time. (optional)
65-
:param str password: Use this password instead of the one provided at
66-
construction time. (optional)
66+
:param str email: Email to use for login.
67+
:param str password: Password to use for login.
68+
:param int expiration: Number of seconds that the auth token should be valid. (optional)
6769
:return: None
6870
:raises: :class:`.exceptions.InvalidCredentials` if ``email``/``password`` is wrong
6971
"""
70-
if email:
71-
self.email = email
72-
if password:
73-
self.password = password
74-
75-
r = self.session.post(self.api_endpoint + 'login',
76-
json={'email': self.email, 'password': self.password})
72+
json = {'email': email, 'password': password}
73+
if expiration:
74+
json['expiration'] = expiration
75+
r = requests.post(self.api_endpoint + 'login', json=json)
7776
if not r.ok:
7877
if r.status_code == 401:
7978
raise InvalidCredentials
8079
raise APIError(r.json()['error'])
81-
self.auth_tkt = r.json()['auth_tkt']
80+
self.auth_token = r.json()['auth_token']
8281
self.user = models.User(session=self, **r.json()['user'])
8382

8483
def logout(self):
8584
"""Removes authentication token from session."""
86-
self.auth_tkt = None
85+
self.auth_token = None
8786
self.user = None
88-
self.session.get(self.api_endpoint + 'logout')
8987

9088
def search(self, query):
9189
"""Search for ``query``.
@@ -114,7 +112,9 @@ def search(self, query):
114112
:param str query: keyword to search for.
115113
:return: dict with list of :mod:`.models` as values
116114
"""
117-
r = self.session.get(self.api_endpoint + 'search', params={'q': query})
115+
r = requests.get(self.api_endpoint + 'search',
116+
params={'q': query},
117+
headers=self._auth_header)
118118
r_json = r.json()
119119
if not r.ok:
120120
raise APIError(r_json['error'])
@@ -134,7 +134,8 @@ def search(self, query):
134134
return search_result
135135

136136
def report(self, scope=None, groups=None, metrics=None, where=None, from_=None, to=None,
137-
timezone='UTC', emails=None, fast=True, when=None, interval=None, name=None):
137+
timezone='UTC', emails=None, when=None, interval=None, name=None,
138+
sort=None, limit=None, offset=None):
138139
"""Create a report.
139140
140141
See the `reporting atomx wiki <https://wiki.atomx.com/reporting>`_
@@ -164,16 +165,15 @@ def report(self, scope=None, groups=None, metrics=None, where=None, from_=None,
164165
:param emails: One or multiple email addresses that should get
165166
notified once the report is finished and ready to download.
166167
:type emails: str or list
167-
:param bool fast: if `False` the report will always be run against the low level data.
168-
This is useful for billing reports for example.
169-
The default is `True` which means it will always try to use aggregate data
170-
to speed up the query.
171168
:param str when: When should the scheduled report run. (daily, monthly, monday-sunday)
172169
:param str interval: Time period included in the scheduled report ('N days' or 'N month')
173-
:param str name: Optional name for the report
170+
:param str name: Optional name for the report.
171+
:param str or list sort: List of columns to sort by.
172+
:param int limit: Number of rows to return
173+
:param int offset: Number of rows to skip.
174174
:return: A :class:`atomx.models.Report` model
175175
"""
176-
report_json = {'timezone': timezone, 'fast': fast}
176+
report_json = {'timezone': timezone}
177177

178178
if name:
179179
report_json['name'] = name
@@ -227,7 +227,17 @@ def report(self, scope=None, groups=None, metrics=None, where=None, from_=None,
227227
emails = [emails]
228228
report_json['emails'] = emails
229229

230-
r = self.session.post(self.api_endpoint + 'report', json=report_json)
230+
params = {}
231+
if limit:
232+
params['limit'] = limit
233+
if offset:
234+
params['offset'] = offset
235+
if sort:
236+
if isinstance(sort, list):
237+
sort = ','.join(sort)
238+
params['sort'] = sort
239+
r = requests.post(self.api_endpoint + 'report',
240+
params=params, json=report_json, headers=self._auth_header)
231241
r_json = r.json()
232242
if not r.ok:
233243
raise APIError(r_json['error'])
@@ -238,65 +248,8 @@ def report(self, scope=None, groups=None, metrics=None, where=None, from_=None,
238248
self.last_response = r_json
239249
self.last_response['_headers'] = r.headers
240250

241-
if is_scheduled_report:
242-
return models.ScheduledReport(session=self, **report)
243-
244251
return models.Report(session=self, **report)
245252

246-
def report_status(self, report):
247-
"""Get the status for a `report`.
248-
249-
This is typically used by calling :meth:`.models.Report.status`.
250-
251-
:param report: Either a :class:`str` that contains the ``id`` of
252-
of the report or an :class:`.models.Report` instance.
253-
:type report: :class:`.models.Report` or :class:`list`
254-
:return: :class:`dict` containing the report status.
255-
"""
256-
if isinstance(report, models.Report):
257-
report_id = report.id
258-
else:
259-
report_id = report
260-
261-
r = self.session.get(self.api_endpoint + 'report/' + report_id, params={'status': True})
262-
if not r.ok:
263-
raise APIError(r.json()['error'])
264-
265-
if self.save_response:
266-
self.last_response = r.json()
267-
self.last_response['_headers'] = r.headers
268-
269-
return r.json()['report']
270-
271-
def report_get(self, report, sort=None, limit=None, offset=None):
272-
"""Get the content (csv) of a :class:`.models.Report`
273-
274-
Typically used by calling :meth:`.models.Report.content` or
275-
:meth:`.models.Report.pandas`.
276-
277-
:param report: Either a :class:`str` that contains the ``id`` of
278-
of the report or an :class:`.models.Report` instance.
279-
:type report: :class:`.models.Report` or :class:`list`
280-
:return: :class:`str` with the report content.
281-
"""
282-
if isinstance(report, models.Report):
283-
report_id = report.id
284-
else:
285-
report_id = report
286-
287-
params = {}
288-
if limit:
289-
params['limit'] = int(limit)
290-
if offset:
291-
params['offset'] = int(offset)
292-
if sort:
293-
params['sort'] = sort
294-
295-
r = self.session.get(self.api_endpoint + 'report/' + report_id, params=params)
296-
if not r.ok:
297-
raise APIError(r.json()['error'])
298-
return r.content.decode()
299-
300253
def get(self, resource, *args, **kwargs):
301254
"""Returns a list of models from :mod:`.models` if you query for
302255
multiple models or a single instance of a model from :mod:`.models`
@@ -355,7 +308,7 @@ def get(self, resource, *args, **kwargs):
355308
"""
356309
if isclass(resource) and issubclass(resource, models.AtomxModel):
357310
resource = resource._resource_name
358-
elif isinstance(resource, models.AtomxModel):
311+
elif hasattr(resource, '_resource_name'):
359312
resource_path = resource._resource_name
360313
if hasattr(resource, 'id'):
361314
resource_path += '/' + str(resource.id)
@@ -364,7 +317,7 @@ def get(self, resource, *args, **kwargs):
364317
resource = resource.strip('/')
365318
for a in args:
366319
resource += '/' + str(a)
367-
r = self.session.get(self.api_endpoint + resource, params=kwargs)
320+
r = requests.get(self.api_endpoint + resource, params=kwargs, headers=self._auth_header)
368321
if not r.ok:
369322
raise APIError(r.json()['error'])
370323

@@ -383,7 +336,7 @@ def get(self, resource, *args, **kwargs):
383336
elif model_name == 'reporting': # special case for `/reports` status
384337
return {
385338
'reports': [models.Report(session=self, **m) for m in res['reports']],
386-
'scheduled': [models.ScheduledReport(session=self, **m) for m in res['scheduled']]
339+
'scheduled': [models.Report(session=self, **m) for m in res['scheduled']]
387340
}
388341
return res
389342

@@ -397,8 +350,8 @@ def post(self, resource, json, **kwargs):
397350
:param kwargs: URL Parameters of the request.
398351
:return: :class:`dict` with the newly created resource.
399352
"""
400-
r = self.session.post(self.api_endpoint + resource.strip('/'),
401-
json=json, params=kwargs)
353+
r = requests.post(self.api_endpoint + resource.strip('/'),
354+
json=json, params=kwargs, headers=self._auth_header)
402355
r_json = r.json()
403356
if not r.ok:
404357
raise APIError(r_json['error'])
@@ -424,8 +377,8 @@ def put(self, resource, id, json, **kwargs):
424377
:param kwargs: URL Parameters of the request.
425378
:return: :class:`dict` with the modified resource.
426379
"""
427-
r = self.session.put(self.api_endpoint + resource.strip('/') + '/' + str(id),
428-
json=json, params=kwargs)
380+
r = requests.put(self.api_endpoint + resource.strip('/') + '/' + str(id),
381+
json=json, params=kwargs, headers=self._auth_header)
429382
r_json = r.json()
430383
if not r.ok:
431384
raise APIError(r_json['error'])
@@ -449,7 +402,7 @@ def delete(self, resource, *args, **kwargs):
449402
resource = resource.strip('/')
450403
for a in args:
451404
resource += '/' + str(a)
452-
r = self.session.delete(self.api_endpoint + resource, params=kwargs)
405+
r = requests.delete(self.api_endpoint + resource, params=kwargs, headers=self._auth_header)
453406
r_json = r.json()
454407
if not r.ok:
455408
raise APIError(r_json['error'])

0 commit comments

Comments
 (0)