├── .gitignore ├── README.md ├── omniture ├── __init__.py ├── account.py ├── elements.py ├── query.py ├── reports.py └── utils.py ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | dist 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-omniture 2 | 3 | `python-omniture` is a wrapper around the Adobe Omniture web analytics API. 4 | 5 | It is not meant to be comprehensive. Instead, it provides a high-level interface 6 | to certain common kinds of queries, and allows you to do construct other queries 7 | closer to the metal. 8 | 9 | ## Installation 10 | 11 | Through PyPI: 12 | 13 | pip install omniture 14 | 15 | Latest and greatest: 16 | 17 | pip install git+git://github.com/stdbrouw/python-omniture.git 18 | 19 | ## Authentication 20 | 21 | The most straightforward way to authenticate is with: 22 | 23 | import omniture 24 | account = omniture.authenticate('my_username', 'my_secret') 25 | 26 | However, to avoid hardcoding passwords, instead you can also put your username 27 | and password in unix environment variables (e.g. in your `.bashrc`): 28 | 29 | export OMNITURE_USERNAME=my_username 30 | export OMNITURE_SECRET=my_secret 31 | 32 | With your credentials in the environment, you can then log in as follows: 33 | 34 | import os 35 | import omniture 36 | account = omniture.authenticate(os.environ) 37 | 38 | ## Account and suites 39 | 40 | You can very easily access some basic information about your account and your 41 | reporting suites: 42 | 43 | print analytics.suites 44 | suite = analytics.suites['guardiangu-network'] 45 | print suite 46 | print len(suite.evars) 47 | print suite.segments 48 | print suite.elements 49 | 50 | You can refer to suites, segments, elements and so on using both their 51 | human-readable name or their id. So for example `suite.segments['pageviews']` and `suite.segments['Page Views']` will work exactly the same. This is especially useful in cases when segment or metric identifiers are long strings of gibberish. That way you don't have to riddle your code with references to `evar16` or `custom4` and instead can call them by their title. 52 | 53 | ## Running a report 54 | 55 | `python-omniture` can run ranked, trended and "over time" reports 56 | 57 | Here's a quick example: 58 | 59 | report = network.report \ 60 | .over_time(metrics=['pageviews', 'visitors']) \ 61 | .range('2013-05-01', '2013-05-31', granularity='month') \ 62 | .sync() 63 | 64 | Some basic features of the three kinds of reports you can run: 65 | 66 | * over_time 67 | * supports multiple metrics but only one element: time 68 | * useful if you need information on a per-page basis 69 | * supports hourly reporting (and up) 70 | * ranked 71 | * ranks pages in relation to the metric 72 | * one number (per metric) for the entire reporting period 73 | * only supports daily, weekly and monthly reporting 74 | * trended 75 | * movement of a single element and metric over time (e.g. visits to world news over time) 76 | * supports hourly reporting (and up) 77 | 78 | Accessing the data in a report works as follows: 79 | 80 | report.data['pageviews'] 81 | 82 | ### Getting down to the plumbing. 83 | 84 | This module is still in beta and you should expect some things not to work. In particular, trended reports have not seen much love (though they should work), and data warehouse reports don't work at all. 85 | 86 | In these cases, it can be useful to use the lower-level access this module provides through `mysuite.report.set` -- you can pass set either a key and value, a dictionary with key-value pairs or you can pass keyword arguments. These will then be added to the raw query. You can always check what the raw query is going to be with the `build` method on queries. 87 | 88 | query = network.report \ 89 | .over_time(metrics=['pageviews', 'visitors']) \ 90 | .set(dateGranularity='month') 91 | .set({'segmentId': 'social'}) 92 | .set('name', 'my report name') 93 | 94 | print query.build() 95 | 96 | ### Running multiple reports 97 | 98 | If you're interested in automating a large number of reports, you can speed up the 99 | execution by first queueing all the reports and only _then_ waiting on the results. 100 | 101 | Here's an example: 102 | 103 | queue = [] 104 | for segment in segments: 105 | report = network.report \ 106 | .range('2013-05-01', '2013-05-31', granularity='day') \ 107 | .over_time(metrics=['pageviews']) \ 108 | .filter(segment=segment) 109 | queue.append(report) 110 | 111 | heartbeat = lambda: sys.stdout.write('.') 112 | reports = omniture.sync(queue, heartbeat) 113 | 114 | for report in reports: 115 | print report.segment 116 | print report.data['pageviews'] 117 | 118 | `omniture.sync` can queue up (and synchronize) both a list of reports, or a dictionary. 119 | -------------------------------------------------------------------------------- /omniture/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from account import Account, Suite 4 | from elements import Value, Element, Segment 5 | from query import Query 6 | from reports import InvalidReportError, Report, OverTimeReport, \ 7 | RankedReport, TrendedReport, DataWarehouseReport 8 | 9 | 10 | def authenticate(username, secret=None, endpoint=Account.DEFAULT_ENDPOINT, prefix='', suffix=''): 11 | # if no secret is specified, we will assume that instead 12 | # we have received a dictionary with credentials (such as 13 | # from os.environ) 14 | if not secret: 15 | source = username 16 | key_to_username = utils.affix(prefix, 'OMNITURE_USERNAME', suffix) 17 | key_to_secret = utils.affix(prefix, 'OMNITURE_SECRET', suffix) 18 | username = source[key_to_username] 19 | secret = source[key_to_secret] 20 | 21 | return Account(username, secret, endpoint) 22 | 23 | 24 | def queue(queries): 25 | if isinstance(queries, dict): 26 | queries = queries.values() 27 | 28 | for query in queries: 29 | query.queue() 30 | 31 | 32 | def sync(queries, heartbeat=None, interval=1): 33 | """ 34 | `omniture.sync` will queue a number of reports and then 35 | block until the results are ready. 36 | 37 | Queueing reports is idempotent, meaning that you can also 38 | use `omniture.sync` to fetch the results for queries that 39 | have already been queued: 40 | 41 | query = mysuite.report.range('2013-06-06').over_time('pageviews', 'page') 42 | omniture.queue(query) 43 | omniture.sync(query) 44 | """ 45 | 46 | queue(queries) 47 | 48 | if isinstance(queries, list): 49 | return [query.sync(heartbeat, interval) for query in queries] 50 | elif isinstance(queries, dict): 51 | return {key: query.sync(heartbeat, interval) for key, query in queries.items()} 52 | else: 53 | message = "Queries should be a list or a dictionary, received: {}".format( 54 | queries.__class__) 55 | raise ValueError(message) 56 | -------------------------------------------------------------------------------- /omniture/account.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import binascii 3 | import time 4 | import sha 5 | import json 6 | from datetime import datetime 7 | from elements import Value, Element, Segment 8 | from query import Query 9 | import utils 10 | 11 | # encoding: utf-8 12 | 13 | class Account(object): 14 | DEFAULT_ENDPOINT = 'https://api.omniture.com/admin/1.3/rest/' 15 | 16 | def __init__(self, username, secret, endpoint=DEFAULT_ENDPOINT): 17 | self.username = username 18 | self.secret = secret 19 | self.endpoint = endpoint 20 | data = self.request('Company', 'GetReportSuites')['report_suites'] 21 | suites = [Suite(suite['site_title'], suite['rsid'], self) for suite in data] 22 | self.suites = utils.AddressableList(suites) 23 | 24 | def request(self, api, method, query={}): 25 | response = requests.post( 26 | self.endpoint, 27 | params={'method': api + '.' + method}, 28 | data=json.dumps(query), 29 | headers=self._build_token() 30 | ) 31 | return response.json() 32 | 33 | def _serialize_header(self, properties): 34 | header = [] 35 | for key, value in properties.items(): 36 | header.append('{key}="{value}"'.format(key=key, value=value)) 37 | return ', '.join(header) 38 | 39 | def _build_token(self): 40 | nonce = str(time.time()) 41 | base64nonce = binascii.b2a_base64(binascii.a2b_qp(nonce)) 42 | created_date = datetime.today().isoformat() + 'Z' 43 | sha_object = sha.new(nonce + created_date + self.secret) 44 | password_64 = binascii.b2a_base64(sha_object.digest()) 45 | 46 | properties = { 47 | "Username": self.username, 48 | "PasswordDigest": password_64.strip(), 49 | "Nonce": base64nonce.strip(), 50 | "Created": created_date, 51 | } 52 | header = 'UsernameToken ' + self._serialize_header(properties) 53 | 54 | return {'X-WSSE': header} 55 | 56 | 57 | class Suite(Value): 58 | def request(self, api, method, query={}): 59 | raw_query = {} 60 | raw_query.update(query) 61 | if 'reportDescription' in raw_query: 62 | raw_query['reportDescription']['reportSuiteID'] = self.id 63 | elif api == 'ReportSuite': 64 | raw_query['rsid_list'] = [self.id] 65 | 66 | return self.account.request(api, method, raw_query) 67 | 68 | def __init__(self, title, id, account): 69 | super(Suite, self).__init__(title, id, account) 70 | 71 | self.account = account 72 | 73 | @property 74 | @utils.memoize 75 | def metrics(self): 76 | data = self.request('ReportSuite', 'GetAvailableMetrics')[0]['available_metrics'] 77 | return Value.list('metrics', data, self, 'display_name', 'metric_name') 78 | 79 | @property 80 | @utils.memoize 81 | def elements(self): 82 | data = self.request('ReportSuite', 'GetAvailableElements')[0]['available_elements'] 83 | return Element.list('elements', data, self, 'display_name', 'element_name') 84 | 85 | @property 86 | @utils.memoize 87 | def evars(self): 88 | data = self.request('ReportSuite', 'GetEVars')[0]['evars'] 89 | return Value.list('evars', data, self, 'name', 'evar_num') 90 | 91 | @property 92 | @utils.memoize 93 | def segments(self): 94 | data = self.request('ReportSuite', 'GetSegments')[0]['sc_segments'] 95 | return Segment.list('segments', data, self, 'name', 'id') 96 | 97 | @property 98 | def report(self): 99 | return Query(self) 100 | 101 | -------------------------------------------------------------------------------- /omniture/elements.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import utils 4 | from copy import copy 5 | 6 | 7 | class Value(object): 8 | def __init__(self, title, id, parent, extra={}): 9 | self.title = title 10 | self.id = id 11 | self.parent = parent 12 | self.properties = {'id': id} 13 | 14 | for k, v in extra.items(): 15 | setattr(self, k, v) 16 | 17 | @classmethod 18 | def list(cls, name, items, parent, title='title', id='id'): 19 | values = [cls(item[title], item[id], parent, item) for item in items] 20 | return utils.AddressableList(values, name) 21 | 22 | def __repr__(self): 23 | return "<{title}: {id} in {parent}>".format(**self.__dict__) 24 | 25 | def copy(self): 26 | value = self.__class__(self.title, self.id, self.parent) 27 | value.properties = copy(self.properties) 28 | return value 29 | 30 | def serialize(self): 31 | return self.properties 32 | 33 | def __str__(self): 34 | return self.title 35 | 36 | 37 | class Element(Value): 38 | def range(self, *vargs): 39 | l = len(vargs) 40 | if l == 1: 41 | start = 0 42 | stop = vargs[0] 43 | elif l == 2: 44 | start, stop = vargs 45 | 46 | top = stop - start 47 | 48 | element = self.copy() 49 | element.properties['startingWith'] = str(start) 50 | element.properties['top'] = str(top) 51 | 52 | return element 53 | 54 | def search(self, keywords, type='AND'): 55 | type = type.upper() 56 | 57 | types = ['AND', 'OR', 'NOT'] 58 | if type not in types: 59 | raise ValueError("Search type should be one of: " + ", ".join(types)) 60 | 61 | element = self.copy() 62 | element.properties['search'] = { 63 | 'type': type, 64 | 'keywords': utils.wrap(keywords), 65 | } 66 | return element 67 | 68 | def select(self, keys): 69 | element = self.copy() 70 | element.properties['selected'] = utils.wrap(keys) 71 | return element 72 | 73 | 74 | class Segment(Element): 75 | pass 76 | 77 | -------------------------------------------------------------------------------- /omniture/query.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import time 4 | from copy import copy 5 | import functools 6 | from dateutil.relativedelta import relativedelta 7 | from elements import Value, Element, Segment 8 | import reports 9 | import utils 10 | 11 | 12 | def immutable(method): 13 | @functools.wraps(method) 14 | def wrapped_method(self, *vargs, **kwargs): 15 | obj = self.clone() 16 | method(obj, *vargs, **kwargs) 17 | return obj 18 | 19 | return wrapped_method 20 | 21 | 22 | class Query(object): 23 | GRANULARITY_LEVELS = ['hour', 'day', 'month'] 24 | 25 | def __init__(self, suite): 26 | self.suite = suite 27 | self.raw = {} 28 | self.id = None 29 | self.report = None 30 | 31 | def _normalize_value(self, value, category): 32 | if isinstance(value, Value): 33 | return value 34 | else: 35 | return getattr(self.suite, category)[value] 36 | 37 | def _serialize_value(self, value, category): 38 | return self._normalize_value(value, category).serialize() 39 | 40 | def _serialize_values(self, values, category): 41 | if not isinstance(values, list): 42 | values = [values] 43 | 44 | return [self._serialize_value(value, category) for value in values] 45 | 46 | def _serialize(self, obj): 47 | if isinstance(obj, list): 48 | return [self._serialize(el) for el in obj] 49 | elif isinstance(obj, Value): 50 | return obj.serialize() 51 | else: 52 | return obj 53 | 54 | def clone(self): 55 | query = Query(self.suite) 56 | query.raw = copy(self.raw) 57 | query.report = self.report 58 | return query 59 | 60 | @immutable 61 | def range(self, start, stop=None, months=0, days=0, granularity=None): 62 | start = utils.date(start) 63 | stop = utils.date(stop) 64 | 65 | if days or months: 66 | stop = start + relativedelta(days=days-1, months=months) 67 | else: 68 | stop = stop or start 69 | 70 | if start == stop: 71 | self.raw['date'] = start.isoformat() 72 | else: 73 | self.raw.update({ 74 | 'dateFrom': start.isoformat(), 75 | 'dateTo': stop.isoformat(), 76 | }) 77 | 78 | if granularity: 79 | if granularity not in self.GRANULARITY_LEVELS: 80 | levels = ", ".join(self.GRANULARITY_LEVELS) 81 | raise ValueError("Granularity should be one of: " + levels) 82 | 83 | self.raw['dateGranularity'] = granularity 84 | 85 | return self 86 | 87 | @immutable 88 | def set(self, key=None, value=None, **kwargs): 89 | """ 90 | `set` is a way to add raw properties to the request, 91 | for features that python-omniture does not support but the 92 | SiteCatalyst API does support. For convenience's sake, 93 | it will serialize Value and Element objects but will 94 | leave any other kind of value alone. 95 | """ 96 | 97 | if key and value: 98 | self.raw[key] = self._serialize(value) 99 | elif key or kwargs: 100 | properties = key or kwargs 101 | for key, value in properties.items(): 102 | self.raw[key] = self._serialize(value) 103 | else: 104 | raise ValueError("Query#set requires a key and value, a properties dictionary or keyword arguments.") 105 | 106 | return self 107 | 108 | @immutable 109 | def sort(self, facet): 110 | #self.raw['sortBy'] = facet 111 | raise NotImplementedError() 112 | return self 113 | 114 | @immutable 115 | def filter(self, segments=None, segment=None): 116 | # It would appear to me that 'segment_id' has a strict subset 117 | # of the functionality of 'segments', but until I find out for 118 | # sure, I'll provide both options. 119 | if segments: 120 | self.raw['segments'] = self._serialize_values(segments, 'segments') 121 | elif segment: 122 | self.raw['segment_id'] = self._normalize_value(segment, 'segments').id 123 | else: 124 | raise ValueError() 125 | 126 | return self 127 | 128 | @immutable 129 | def ranked(self, metrics, elements): 130 | self._serialize_values(metrics, 'metrics') 131 | 132 | self.report = reports.RankedReport 133 | self.raw['metrics'] = self._serialize_values(metrics, 'metrics') 134 | self.raw['elements'] = self._serialize_values(elements, 'elements') 135 | return self 136 | 137 | @immutable 138 | def trended(self, metric, element): 139 | if isinstance(metric, list) or isinstance(element, list): 140 | raise ValueError("Trended reports can only be generated for one metric and one element.") 141 | 142 | self.report = reports.TrendedReport 143 | self.raw['metrics'] = self._serialize_values(metric, 'metrics') 144 | self.raw['elements'] = self._serialize_values(element, 'elements') 145 | return self 146 | 147 | @immutable 148 | def over_time(self, metrics): 149 | self.report = reports.OverTimeReport 150 | self.raw['metrics'] = self._serialize_values(metrics, 'metrics') 151 | return self 152 | 153 | # TODO: data warehouse reports are a work in progress 154 | @immutable 155 | def data(self, metrics, breakdowns): 156 | self.report = reports.DataWarehouseReport 157 | self.raw['metrics'] = self._serialize_values(metrics, 'metrics') 158 | # TODO: haven't figured out how breakdowns work yet 159 | self.raw['breakdowns'] = False 160 | return self 161 | 162 | def build(self): 163 | if self.report == reports.DataWarehouseReport: 164 | return utils.translate(self.raw, { 165 | 'metrics': 'Metric_List', 166 | 'breakdowns': 'Breakdown_List', 167 | 'dateFrom': 'Date_From', 168 | 'dateTo': 'Date_To', 169 | # is this the correct mapping? 170 | 'date': 'Date_Preset', 171 | 'dateGranularity': 'Date_Granularity', 172 | }) 173 | else: 174 | return {'reportDescription': self.raw} 175 | 176 | def queue(self): 177 | q = self.build() 178 | self.id = self.suite.request('Report', self.report.method, q)['reportID'] 179 | return self 180 | 181 | def probe(self, fn, heartbeat=None, interval=1, soak=False): 182 | status = 'not ready' 183 | while status == 'not ready': 184 | if heartbeat: 185 | heartbeat() 186 | time.sleep(interval) 187 | response = fn() 188 | status = response['status'] 189 | 190 | if not soak and status not in ['not ready', 'done', 'ready']: 191 | raise reports.InvalidReportError(response) 192 | 193 | return response 194 | 195 | # only for SiteCatalyst queries 196 | def sync(self, heartbeat=None, interval=1): 197 | if not self.id: 198 | self.queue() 199 | 200 | # this looks clunky, but Omniture sometimes reports a report 201 | # as ready when it's really not 202 | check_status = lambda: self.suite.request('Report', 'GetStatus', {'reportID': self.id}) 203 | get_report = lambda: self.suite.request('Report', 'GetReport', {'reportID': self.id}) 204 | status = self.probe(check_status, heartbeat, interval, soak=True) 205 | response = self.probe(get_report, heartbeat, interval) 206 | return self.report(response, self) 207 | 208 | # only for SiteCatalyst queries 209 | def async(self, callback=None, heartbeat=None, interval=1): 210 | if not self.id: 211 | self.queue() 212 | 213 | raise NotImplementedError() 214 | 215 | # only for Data Warehouse queries 216 | def request(self, name='python-omniture query', ftp=None, email=None): 217 | raise NotImplementedError() 218 | 219 | def cancel(self): 220 | if self.report == reports.DataWarehouseReport: 221 | return self.suite.request('DataWarehouse', 'CancelRequest', {'Request_Id': self.id}) 222 | else: 223 | return self.suite.request('Report', 'CancelReport', {'reportID': self.id}) 224 | -------------------------------------------------------------------------------- /omniture/reports.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from elements import Value, Element, Segment 4 | import utils 5 | 6 | 7 | class InvalidReportError(Exception): 8 | def normalize(self, error): 9 | print 'error', error 10 | 11 | if 'error_msg' in error: 12 | return { 13 | 'status': error['status'], 14 | 'code': error['error_code'], 15 | 'message': error.get('error_msg', ''), 16 | } 17 | else: 18 | return { 19 | 'status': error['statusMsg'], 20 | 'code': error['status'], 21 | 'message': error.get('statusDesc', ''), 22 | } 23 | 24 | def __init__(self, error): 25 | error = self.normalize(error) 26 | message = "{status}: {message} ({code})".format(**error) 27 | super(InvalidReportError, self).__init__(message) 28 | 29 | 30 | # TODO: also make this iterable (go through rows) 31 | class Report(object): 32 | def process(self): 33 | self.status = self.raw['status'] 34 | self.timing = { 35 | 'queue': float(self.raw['waitSeconds']), 36 | 'execution': float(self.raw['runSeconds']), 37 | } 38 | self.report = report = self.raw['report'] 39 | self.metrics = Value.list('metrics', report['metrics'], self.suite, 'name', 'id') 40 | self.elements = Value.list('elements', report['elements'], self.suite, 'name', 'id') 41 | self.period = report['period'] 42 | segment = report['segment_id'] 43 | if len(segment): 44 | self.segment = self.query.suite.segments[report['segment_id']] 45 | else: 46 | self.segment = None 47 | 48 | self.data = utils.AddressableDict(self.metrics) 49 | for column in self.data: 50 | column.value = [] 51 | 52 | def to_dataframe(self): 53 | import pandas as pd 54 | raise NotImplementedError() 55 | # return pd.DataFrame() 56 | 57 | def serialize(self, verbose=False): 58 | if verbose: 59 | facet = 'title' 60 | else: 61 | facet = 'id' 62 | 63 | d = {} 64 | for el in self.data: 65 | key = getattr(el, facet) 66 | d[key] = el.value 67 | return d 68 | 69 | def __init__(self, raw, query): 70 | #from pprint import pprint 71 | #pprint(raw) 72 | 73 | self.raw = raw 74 | self.query = query 75 | self.suite = query.suite 76 | self.process() 77 | 78 | def __repr__(self): 79 | info = { 80 | 'metrics': ", ".join(map(str, self.metrics)), 81 | 'elements': ", ".join(map(str, self.elements)), 82 | } 83 | return "".format(**info) 84 | 85 | class OverTimeReport(Report): 86 | def process(self): 87 | super(OverTimeReport, self).process() 88 | 89 | # TODO: this works for over_time reports and I believe for ranked 90 | # reports as well, but trended reports have their data in 91 | # `data.breakdown:[breakdown:[counts]]` 92 | for row in self.report['data']: 93 | for i, value in enumerate(row['counts']): 94 | if self.metrics[i].type == 'number': 95 | value = float(value) 96 | self.data[i].append(value) 97 | 98 | OverTimeReport.method = 'QueueOvertime' 99 | 100 | 101 | class RankedReport(Report): 102 | def process(self): 103 | super(RankedReport, self).process() 104 | 105 | for row in self.report['data']: 106 | for i, value in enumerate(row['counts']): 107 | if self.metrics[i].type == 'number': 108 | value = float(value) 109 | self.data[i].append((row['name'], row['url'], value)) 110 | 111 | RankedReport.method = 'QueueRanked' 112 | 113 | 114 | class TrendedReport(Report): 115 | def process(self): 116 | super(TrendedReport, self).process() 117 | 118 | TrendedReport.method = 'QueueTrended' 119 | 120 | 121 | class DataWarehouseReport(object): 122 | pass 123 | 124 | DataWarehouseReport.method = 'Request' 125 | 126 | -------------------------------------------------------------------------------- /omniture/utils.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import datetime 3 | from dateutil.parser import parse as parse_date 4 | 5 | class memoize: 6 | def __init__(self, function): 7 | self.function = function 8 | self.memoized = {} 9 | 10 | def __call__(self, *args): 11 | try: 12 | return self.memoized[args] 13 | except KeyError: 14 | self.memoized[args] = self.function(*args) 15 | return self.memoized[args] 16 | 17 | 18 | class AddressableList(list): 19 | def __init__(self, items, name='items'): 20 | super(AddressableList, self).__init__(items) 21 | self.name = name 22 | 23 | def __getitem__(self, key): 24 | if isinstance(key, int): 25 | return super(AddressableList, self).__getitem__(key) 26 | else: 27 | matches = [item for item in self if item.title == key or item.id == key] 28 | count = len(matches) 29 | if count > 1: 30 | matches = map(repr, matches) 31 | error = "Found multiple matches for {key}: {matches}. ".format( 32 | key=key, matches=", ".join(matches)) 33 | advice = "Use the identifier instead." 34 | raise KeyError(error + advice) 35 | elif count == 1: 36 | return matches[0] 37 | else: 38 | raise KeyError("Cannot find {key} among the available {name}".format( 39 | key=key, name=self.name)) 40 | 41 | 42 | class AddressableDict(AddressableList): 43 | def __getitem__(self, key): 44 | item = super(AddressableDict, self).__getitem__(key) 45 | return item.value 46 | 47 | 48 | def date(obj): 49 | if obj is None: 50 | return None 51 | elif isinstance(obj, datetime.date): 52 | if hasattr(dt, 'date'): 53 | return obj.date() 54 | else: 55 | return obj 56 | elif isinstance(obj, basestring): 57 | return parse_date(obj).date() 58 | else: 59 | raise ValueError("Can only convert strings into dates, received {}".format(obj.__class__)) 60 | 61 | 62 | def wrap(obj): 63 | if isinstance(obj, list): 64 | return obj 65 | else: 66 | return [obj] 67 | 68 | 69 | def affix(prefix, base, suffix, connector='_'): 70 | if prefix: 71 | prefix = prefix + connector 72 | else: 73 | prefix = '' 74 | 75 | if suffix: 76 | suffix = connector + suffix 77 | else: 78 | suffix = '' 79 | 80 | return prefix + base + suffix 81 | 82 | 83 | def translate(d, mapping): 84 | d = copy.copy(d) 85 | 86 | for src, dest in mapping.items(): 87 | if src in d: 88 | d[dest] = d[src] 89 | del d[src] 90 | 91 | return d -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name='omniture', 4 | description='A wrapper for the Adobe Omniture and SiteCatalyst web analytics API.', 5 | long_description=open('README.md').read(), 6 | author='Stijn Debrouwere', 7 | author_email='stijn@stdout.be', 8 | url='http://stdbrouw.github.com/python-omniture/', 9 | download_url='http://www.github.com/stdbrouw/python-omniture/tarball/master', 10 | version='0.3.1', 11 | license='MIT', 12 | packages=find_packages(), 13 | keywords='data analytics api wrapper adobe', 14 | install_requires=[ 15 | 'requests', 16 | 'python-dateutil', 17 | ], 18 | classifiers=['Development Status :: 4 - Beta', 19 | 'Intended Audience :: Developers', 20 | 'License :: OSI Approved :: MIT License', 21 | 'Operating System :: OS Independent', 22 | 'Programming Language :: Python', 23 | 'Topic :: Scientific/Engineering :: Information Analysis', 24 | ], 25 | ) -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import omniture 2 | import sys 3 | import os 4 | from pprint import pprint 5 | 6 | analytics = omniture.Account() 7 | analytics.authenticate(os.environ) 8 | 9 | #print analytics.suites 10 | print analytics.suites['guardiangu-parioli-taste-of-rome'] 11 | print analytics.suites['Media Prof Network'] 12 | network = analytics.suites['guardiangu-network'] 13 | print len(network.evars) 14 | #pprint(network.segments) 15 | print network.segments['First Time Visitors'] 16 | 17 | segments = [ 18 | 'UK (Locked)', 19 | 'US (Locked)', 20 | ] 21 | 22 | queue = [] 23 | 24 | for segment in segments: 25 | report = network.report \ 26 | .range('2013-05-01', '2013-05-31', granularity='day') \ 27 | .over_time(metrics=['pageviews']) \ 28 | .filter(segment=segment) 29 | 30 | queue.append(report) 31 | 32 | def heartbeat(): 33 | sys.stdout.write('.') 34 | sys.stdout.flush() 35 | 36 | reports = omniture.sync(queue, heartbeat) 37 | 38 | for report in reports: 39 | print report.segment 40 | print report.data['pageviews'] 41 | 42 | --------------------------------------------------------------------------------