Skip to content

Commit

Permalink
Async support (#63)
Browse files Browse the repository at this point in the history
* Added better Async support
* Fixed bug with the sync command when running multiple reports

Added support for Asynchronous requests. Can make interactive scripting more efficient from the programmer's perspective.
  • Loading branch information
dancingcactus authored Mar 2, 2017
1 parent dd2f2fd commit df128ee
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 98 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ _Note: to disable the ID check add the parameter `disable_validation=True`_

**run()** -- `run(defaultheartbeat=True)` Run the report and check the queue until done. The `defaultheartbeat` writes a . (period) out to the console each time it checks on the report.

**async()** -- Queue the report to Adobe but don't block the program. Use `is_ready()` to check on the report

**is_ready()** -- Checks if the queued report is finished running on the Adobe side. Can only be called after `async()`

**get_report()** -- Retrieves the report object for a finished report. Must call `is_ready()` first.

**set()** -- `set(key, value)` Set a custom attribute in the report definition

Expand Down Expand Up @@ -240,6 +245,31 @@ Here's an example:

`omniture.sync` can queue up (and synchronize) both a list of reports, or a dictionary.

### Running Report Asynchrnously
If you want to run reports in a way that doesn't block. You can use something like the following to do so.

```python-omniture

query = suite.report \
.range('2017-01-01', '2017-01-31', granularity='day') \
.metric('pageviews') \
.filter(segment=segment)
.async()

print(query.check())
#>>>False
print(query.check())
#>>>True
#The report is now ready to grab

report = query.get_report()

```

This is super helpful if your reports take a long time to run because you don't have to keep your laptop open the whole time, especially if you are doing the queries interactively.



### Making other API requests
If you need to make other API requests that are not reporting reqeusts you can do so by
calling `analytics.request(api, method, params)` For example if I wanted to call
Expand Down
2 changes: 1 addition & 1 deletion build/lib/omniture/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from .account import Account, Suite
from .elements import Value
from .query import Query
from .query import Query, ReportNotSubmittedError
from .reports import InvalidReportError, Report, DataWarehouseReport
from .version import __version__
from .utils import AddressableList, affix
Expand Down
115 changes: 69 additions & 46 deletions build/lib/omniture/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ def wrapped_method(self, *vargs, **kwargs):

return wrapped_method

class ReportNotSubmittedError(Exception):
""" Exception that is raised when a is requested by hasn't been submitted
to Adobe
"""
def __init__(self,error):
self.log = logging.getLogger(__name__)
self.log.debug("Report Has not been submitted, call async() or run()")
super(ReportNotSubmittedError, self).__init__("Report Not Submitted")

class Query(object):
""" Lets you build a query to the Reporting API for Adobe Analytics.
Expand All @@ -38,6 +46,7 @@ class Query(object):
"""

GRANULARITY_LEVELS = ['hour', 'day', 'week', 'month', 'quarter', 'year']
STATUSES = ["Not Submitted","Not Ready","Done"]

def __init__(self, suite):
""" Setup the basic structure of the report query. """
Expand All @@ -48,8 +57,13 @@ def __init__(self, suite):
#the raw query and have it work as is
self.raw['reportSuiteID'] = str(self.suite.id)
self.id = None
self.report = reports.Report
self.method = "Get"
self.status = self.STATUSES[0]
#The report object
self.report = reports.Report
#The fully hydrated report object
self.processed_response = None
self.unprocessed_response = None

def _normalize_value(self, value, category):
if isinstance(value, Value):
Expand Down Expand Up @@ -79,6 +93,9 @@ def clone(self):
query = Query(self.suite)
query.raw = copy(self.raw)
query.report = self.report
query.status = self.status
query.processed_response = self.processed_response
query.unprocessed_response = self.unprocessed_response
return query

@immutable
Expand Down Expand Up @@ -266,18 +283,7 @@ def currentData(self):

def build(self):
""" Return the report descriptoin as an object """
if self.report == reports.DataWarehouseReport:
return utils.translate(self.raw, {
'metrics': 'Metric_List',
'breakdowns': 'Breakdown_List',
'dateFrom': 'Date_From',
'dateTo': 'Date_To',
# is this the correct mapping?
'date': 'Date_Preset',
'dateGranularity': 'Date_Granularity',
})
else:
return {'reportDescription': self.raw}
return {'reportDescription': self.raw}

def queue(self):
""" Submits the report to the Queue on the Adobe side. """
Expand All @@ -287,27 +293,16 @@ def queue(self):
self.id = self.suite.request('Report',
self.report.method,
q)['reportID']
self.status = self.STATUSES[1]
return self

def probe(self, fn, heartbeat=None, interval=1, soak=False):
""" Evaluate the response of a report"""
status = 'not ready'
while status == 'not ready':
def probe(self, heartbeat=None, interval=1, soak=False):
""" Keep checking until the report is done"""
#Loop until the report is done
while self.is_ready() == False:
if heartbeat:
heartbeat()
time.sleep(interval)

#Loop until the report is done
#(No longer raises the ReportNotReadyError)
try:
response = fn()
status = 'done'
return response
except reports.ReportNotReadyError:
status = 'not ready'
# if not soak and status not in ['not ready', 'done', 'ready']:
#raise reports.InvalidReportError(response)

#Use a back off up to 30 seconds to play nice with the APIs
if interval < 1:
interval = 1
Expand All @@ -316,22 +311,51 @@ def probe(self, fn, heartbeat=None, interval=1, soak=False):
else:
interval = 30
self.log.debug("Check Interval: %s seconds", interval)

def is_ready(self):
""" inspects the response to see if the report is ready """
if self.status == self.STATUSES[0]:
raise ReportNotSubmittedError('{"message":"Doh! the report needs to be submitted first"}')
elif self.status == self.STATUSES[1]:
try:
# the request method catches the report and populates it automatically
response = self.suite.request('Report','Get',{'reportID': self.id})
self.status = self.STATUSES[2]
self.unprocessed_response = response
self.processed_response = self.report(response, self)
return True
except reports.ReportNotReadyError:
self.status = self.STATUSES[1]
#raise reports.InvalidReportError(response)
return False
elif self.status == self.STATUSES[2]:
return True


# only for SiteCatalyst queries
def sync(self, heartbeat=None, interval=0.01):
""" Run the report synchronously,"""
if not self.id:
print("sync called")
if self.status == self.STATUSES[0]:
print("Queing Report")
self.queue()
self.probe(heartbeat, interval)
if self.status == self.STATUSES[1]:
self.probe()
return self.processed_response

# this looks clunky, but Omniture sometimes reports a report
# as ready when it's really not
get_report = lambda: self.suite.request('Report',
'Get',
{'reportID': self.id})
response = self.probe(get_report, heartbeat, interval)
return self.report(response, self)

#shortcut to run a report immediately
def async(self, callback=None, heartbeat=None, interval=1):
""" Run the Report Asynchrnously """
if self.status == self.STATUSES[0]:
self.queue()
return self

def get_report(self):
self.is_ready()
if self.status == self.STATUSES[2]:
return self.processed_response
else:
raise reports.ReportNotReadyError('{"message":"Doh! the report is not ready yet"}')

def run(self, defaultheartbeat=True, heartbeat=None, interval=0.01):
"""Shortcut for sync(). Runs the current report synchronously. """
if defaultheartbeat == True:
Expand All @@ -346,13 +370,12 @@ def heartbeat(self):
sys.stdout.write('.')
sys.stdout.flush()

# only for SiteCatalyst queries
def async(self, callback=None, heartbeat=None, interval=1):
if not self.id:
self.queue()

raise NotImplementedError()

def check(self):
"""
Basically an alias to is ready to make the interface a bit better
"""
return self.is_ready()

def cancel(self):
""" Cancels a the report from the Queue on the Adobe side. """
Expand Down
2 changes: 1 addition & 1 deletion build/lib/omniture/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 1) we don't load dependencies by storing it in __init__.py
# 2) we can import it in setup.py for the same reason
# 3) we can import it into your module module
__version__ = '0.5.1'
__version__ = '0.5.2'
46 changes: 45 additions & 1 deletion build/lib/tests/testQuery.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,53 @@ def test_report_run(self,m):

self.assertIsInstance(self.analytics.suites[test_report_suite].report.run(), omniture.Report, "The run method doesn't work to create a report")

@requests_mock.mock()
def test_report_async(self,m):
"""Make sure that are report are run Asynchrnously """
path = os.path.dirname(__file__)

with open(path+'/mock_objects/basic_report.json') as data_file:
json_response = data_file.read()

with open(path+'/mock_objects/Report.Queue.json') as queue_file:
report_queue = queue_file.read()

#setup mock object
m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response)
m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue)

query = self.analytics.suites[test_report_suite].report.async()
self.assertIsInstance(query, omniture.Query, "The Async method doesn't work")
self.assertTrue(query.check(), "The check method is weird")
self.assertIsInstance(query.get_report(), omniture.Report, "The check method is weird")

@requests_mock.mock()
def test_report_bad_async(self,m):
"""Make sure that are report can't be checked on out of order """
path = os.path.dirname(__file__)

with open(path+'/mock_objects/Report.Get.NotReady.json') as data_file:
json_response = data_file.read()

with open(path+'/mock_objects/Report.Queue.json') as queue_file:
report_queue = queue_file.read()

#setup mock object
m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response)
m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue)


with self.assertRaises(omniture.query.ReportNotSubmittedError):
self.analytics.suites[test_report_suite].report.get_report()
query = self.analytics.suites[test_report_suite].report.async()
self.assertIsInstance(query, omniture.Query, "The Async method doesn't work")
self.assertFalse(query.check(), "The check method is weird")
with self.assertRaises(omniture.reports.ReportNotReadyError):
query.get_report()

#@unittest.skip("skip")
def test_bad_element(self):
"""Test to make sure the element validation is woring"""
"""Test to make sure the element validation is working"""
self.assertRaises(KeyError,self.analytics.suites[test_report_suite].report.element, "pages")

@unittest.skip("Test Not Finished")
Expand Down
2 changes: 1 addition & 1 deletion omniture/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from .account import Account, Suite
from .elements import Value
from .query import Query
from .query import Query, ReportNotSubmittedError
from .reports import InvalidReportError, Report, DataWarehouseReport
from .version import __version__
from .utils import AddressableList, affix
Expand Down
Loading

0 comments on commit df128ee

Please sign in to comment.