├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── build.sh ├── build └── lib │ ├── omniture │ ├── __init__.py │ ├── account.py │ ├── elements.py │ ├── query.py │ ├── reports.py │ ├── utils.py │ └── version.py │ └── tests │ ├── __init__.py │ ├── testAccount.py │ ├── testAll.py │ ├── testElement.py │ ├── testQuery.py │ ├── testReports.py │ └── testUtils.py ├── logging.json ├── omniture ├── __init__.py ├── account.py ├── elements.py ├── query.py ├── reports.py ├── utils.py └── version.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── mock_objects ├── Company.GetReportSuites.json ├── Report.Get.NotReady.json ├── Report.GetElements.json ├── Report.GetMetrics.json ├── Report.Queue.json ├── Segments.Get.json ├── basic_report.json ├── invalid_element.json ├── invalid_metric.json ├── invalid_segment.json ├── mixed_classifications.json ├── multi_classifications.json ├── ranked_report.json ├── ranked_report_inf.json ├── segmented_report.json ├── trended_report.html └── trended_report.json ├── testAccount.py ├── testElement.py ├── testQuery.py ├── testReports.py └── testUtils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | 4 | [report] 5 | # Regexes for lines to exclude from consideration 6 | 7 | include = 8 | omniture/__init__.py 9 | omniture/[A-z]*.py 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | dist 5 | *-jgrover* 6 | *.log 7 | .coverage 8 | coverage.xml 9 | htmlcov 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | python: 4 | - "2.7" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7" 8 | cache: 9 | - pip 10 | before_install: 11 | - export TZ=America/Denver 12 | install: 13 | - pip install --upgrade pip 14 | - pip install codecov 15 | - pip install . 16 | - pip install -r requirements.txt 17 | - pip install requests_mock 18 | - pip install pylint 19 | script: 20 | - coverage run -m unittest discover && pylint --py3k --errors-only omniture 21 | after_success: 22 | - coverage report -m 23 | - bash <(curl -s https://codecov.io/bash) 24 | - codecov 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) {{{year}}} {{{fullname}}} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.md 2 | include README.md 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-omniture 2 | [![Build Status](https://travis-ci.org/dancingcactus/python-omniture.svg?branch=master)](https://travis-ci.org/dancingcactus/python-omniture) 3 | [![codecov](https://codecov.io/gh/dancingcactus/python-omniture/branch/master/graph/badge.svg)](https://codecov.io/gh/dancingcactus/python-omniture) 4 | 5 | 6 | `python-omniture` is a wrapper around the Adobe Analytics API. 7 | 8 | It is not meant to be comprehensive. Instead, it provides a high-level interface 9 | to certain many of the common reporting queries, and allows you to do construct other queries 10 | closer to the metal. 11 | 12 | ## Installation 13 | 14 | Through PyPI (older version): 15 | 16 | pip install omniture 17 | 18 | Latest and greatest: 19 | 20 | pip install git+http://github.com/dancingcactus/python-omniture.git 21 | 22 | supports python 2.7 and 3.5+ 23 | 24 | ## Authentication 25 | 26 | The most straightforward way to authenticate is with: 27 | 28 | ```python 29 | import omniture 30 | analytics = omniture.authenticate('my_username', 'my_secret') 31 | ``` 32 | 33 | However, to avoid hardcoding passwords, instead you can also put your username 34 | and password in unix environment variables (e.g. in your `.bashrc`): 35 | 36 | ```bash 37 | export OMNITURE_USERNAME=my_username 38 | export OMNITURE_SECRET=my_secret 39 | ``` 40 | 41 | With your credentials in the environment, you can then log in as follows: 42 | 43 | ```python 44 | import os 45 | import omniture 46 | analytics = omniture.authenticate(os.environ) 47 | ``` 48 | 49 | ## Account and suites 50 | 51 | You can very easily access some basic information about your account and your 52 | reporting suites: 53 | 54 | ```python 55 | print analytics.suites 56 | suite = analytics.suites['reportsuite_name'] 57 | print suite 58 | print suite.metrics 59 | print suite.elements 60 | print suite.segments 61 | ``` 62 | 63 | You can refer to suites, segments, elements and so on using both their 64 | human-readable name or their id. So for example `suite.metrics['pageviews']` and `suite.metrics['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 `event4` and instead can call them by their title. Just remember that if you change the friendly name in the interface it will break your script. 65 | 66 | ## Running a report 67 | 68 | `python-omniture` can run ranked, trended and over time reports. Pathing reports are still in the works 69 | 70 | Here's a quick example: 71 | 72 | ```python 73 | report = suite.report \ 74 | .element('page') \ 75 | .metric('pageviews') \ 76 | .run() 77 | ``` 78 | This will generate the report definition and run the report. You can alternatively generate a report definition and save the report defintion to a variable by omitting the call to the `run()` method 79 | 80 | If you call `print` on the report defintion it will print out the JSON that you can use in the [API explorer](https://marketing.adobe.com/developer/en_US/get-started/api-explorer) for debugging or to use in other scripts 81 | 82 | ```python 83 | report = suite.report \ 84 | .element('page') \ 85 | .metric('pageviews') \ 86 | 87 | print report 88 | ``` 89 | 90 | ### Report Options 91 | Here are the options you can add to a report. 92 | 93 | **element()** - `element('element_id or element_name', **kwargs)` Adds an element to the report. If you need to pass in additional information in to the element (e.g. `top` or `startingWith` or `classification`) you can use the kwargs to do so. A full list of options available is documented [here](https://marketing.adobe.com/developer/en_US/documentation/analytics-reporting-1-4/r-reportdescriptionelement). If multiple elements are present then they are broken-down by one another 94 | 95 | _Note: to disable the ID check add the parameter `disable_validation=True`_ 96 | 97 | **breakdown()** - `breakdown('element_id or element_name', **kwargs)` Same as element. It is included to make report queries more readable when there are multiple element. Use when there are more than one element. eg. 98 | 99 | ```python 100 | report = suite.report.element('evar1').breakdown('evar2') 101 | ``` 102 | 103 | _Note: to disable the ID check add the parameter `disable_validation=True`_ 104 | 105 | **metric()** - `metric('metric')` Adds a metric to the report. You can pass a list of metrics if you wish to process the report using multiple metrics. 106 | 107 | ```python 108 | report = suite.report.element('evar1').metric(['visits','pageviews','instances']) 109 | ``` 110 | 111 | _Note: to disable the ID check add the parameter `disable_validation=True`_ 112 | 113 | **range()** - `range('start', 'stop=None', 'months=0', 'days=0', 'granularity=None')` Sets the date range for the report. All dates shoudl be listed in ISO-8601 (e.g. 'YYYY-MM-DD') 114 | 115 | * **Start** -- Start date for the report. If no stop date is specified then the report will be for a single day 116 | * **Stop** -- End date for the report. 117 | * **months** -- Number of months back to run the report 118 | * **days** -- Number of days back from now to run the report 119 | * **granularity** -- The Granularity of the report (`hour`, `day`, `week`, `month`) 120 | 121 | **granularity()** -- `granularity('granularity')` Set the granularity of the report 122 | 123 | **sortBy()** -- `sortBy('metric')` Set the sortBy metric 124 | 125 | **filter()** -- `filter('segment')` or `filter(element='element', selected=[])` Set the segment to be applied to the report. Can either be an segment id/name or can be used to define an inline segment by specifying the paramtered. You can add multiple filters if needed and they will be stacked (anded together) 126 | ```python 127 | report = suite.report.filter('537d509ee4b0893ab30616c7') 128 | report = suite.report.filter(element='page', selected=['homepage']) 129 | report = suite.report.filter('537d509ee4b0893ab30616c7')\ 130 | .filter(element='page', selected=['homepage']) 131 | ``` 132 | _Note: to disable the ID check add the parameter `disable_validation=True`_ 133 | 134 | **currentData()** --`currentData()` Set the currentData flag 135 | 136 | **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. 137 | 138 | **asynch()** -- Queue the report to Adobe but don't block the program. Use `is_ready()` to check on the report 139 | 140 | **is_ready()** -- Checks if the queued report is finished running on the Adobe side. Can only be called after `asynch()` 141 | 142 | **get_report()** -- Retrieves the report object for a finished report. Must call `is_ready()` first. 143 | 144 | **set()** -- `set(key, value)` Set a custom attribute in the report definition 145 | 146 | ## Using the Results of a report 147 | 148 | To see the raw output of a report. 149 | ```python 150 | print report 151 | ``` 152 | 153 | If you need an easy way to access the data in a report: 154 | 155 | ```python 156 | data = report.data 157 | ``` 158 | 159 | This will generate a list of dicts with the metrics and elements called out by id. 160 | 161 | 162 | ### Pandas Support 163 | `python-omniture` can also generate a data frame of the data returned. It works as follows: 164 | 165 | ```python 166 | df = report.dataframe 167 | ``` 168 | 169 | Pandas Data frames can be useful if you need to analyize the the data or transform it easily. 170 | 171 | ### Getting down to the plumbing. 172 | 173 | This module is still in beta and you should expect some things not to work. In particular, pathing reports have not seen much love (though they should work), and data warehouse reports don't work at all. 174 | 175 | 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 by simply printing the qeury. 176 | 177 | ```python 178 | query = suite.report \ 179 | .element('pages') 180 | .metric('pageviews) 181 | .set(anomalyDetection='month') 182 | 183 | 184 | print query 185 | ``` 186 | 187 | 188 | ### JSON Reports 189 | The underlying API is a JSON API. At anytime you can get a string representation of the report that you are created by calling report.json(). 190 | 191 | ```python 192 | json_string = suite.report.element('pageviews').json() 193 | ``` 194 | 195 | That JSON can be used in the [API explorer](https://marketing.adobe.com/developer/api-explorer). You have have it formatted nice and printed out without the unicode representations if you use the following 196 | 197 | ```python 198 | print suite.report.element('pageviews') 199 | ``` 200 | 201 | You can also create a report from JSON or a string representation of JSON. 202 | 203 | ```python 204 | report = suite.jsonReport("{'reportDescription':{'reportSuiteID':'foo'}}") 205 | ``` 206 | 207 | These two functions allow you to serialize and unserialize reports which can be helpful to re-run reports that error out. 208 | 209 | 210 | ### Removing client side validation to increase performance 211 | The library checks to make sure the elements, metrics and segments are all valid before submitting the report to the server. To validate these the library will make an API call to get the elements, metrics and segments. The library is pretty effecient with the API calls meaning it will only request them when needed and it will cache the request for subsequent calls. However, if you are running a script on a daily basis the with the same metrics, dimensions and segments this check can be redundant, especially if you are running reports across multiple report suites. To disable this check you woudl add the `disable_validation=True` parameter to the method calls. Here is how you would do it. 212 | 213 | ```python 214 | 215 | suite.report.metric("page",disable_validation=True).run() 216 | suite.report.element("pageviews",disable_validation=True).run() 217 | suite.report.filter("somesegmentID",disable_validation=True).run() 218 | 219 | ``` 220 | 221 | One thing to note is that this method only support the IDs and not the friendly names and it still requires that those IDs be valid (the server still checks them). 222 | 223 | 224 | 225 | ### Running multiple reports 226 | 227 | If you're interested in automating a large number of reports, you can speed up the 228 | execution by first queueing all the reports and only _then_ waiting on the results. 229 | 230 | Here's an example: 231 | 232 | ```python 233 | queue = [] 234 | for segment in segments: 235 | report = suite.report \ 236 | .range('2013-05-01', '2013-05-31', granularity='day') \ 237 | .metric('pageviews') \ 238 | .filter(segment=segment) 239 | queue.append(report) 240 | 241 | heartbeat = lambda: sys.stdout.write('.') 242 | reports = omniture.sync(queue, heartbeat) 243 | 244 | for report in reports: 245 | print report.segment 246 | print report.data 247 | 248 | ``` 249 | 250 | `omniture.sync` can queue up (and synchronize) both a list of reports, or a dictionary. 251 | 252 | ### Running Report Asynchrnously 253 | If you want to run reports in a way that doesn't block. You can use something like the following to do so. 254 | 255 | ```python-omniture 256 | 257 | query = suite.report \ 258 | .range('2017-01-01', '2017-01-31', granularity='day') \ 259 | .metric('pageviews') \ 260 | .filter(segment=segment) 261 | .asynch() 262 | 263 | print(query.check()) 264 | #>>>False 265 | print(query.check()) 266 | #>>>True 267 | #The report is now ready to grab 268 | 269 | report = query.get_report() 270 | 271 | ``` 272 | 273 | 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. 274 | 275 | 276 | 277 | ### Making other API requests 278 | If you need to make other API requests that are not reporting reqeusts you can do so by 279 | calling `analytics.request(api, method, params)` For example if I wanted to call 280 | Company.GetReportSuites I would do 281 | 282 | ```python 283 | response = analytics.request('Company', 'GetReportSuites') 284 | ``` 285 | 286 | ### Contributing 287 | Feel free to contribute by filing issues or issuing a pull reqeust. 288 | 289 | #### Build 290 | If you want to build the module 291 | 292 | ```bash 293 | bash build.sh 294 | ``` 295 | 296 | If you want to run unit tests 297 | 298 | ```bash 299 | python -m unittest discover 300 | ``` 301 | 302 | Contributers 303 | Special Thanks to 304 | * [adibbehjat](https://github.com/adibbehjat) for helping think through the client side validation and when to skip it 305 | * [aarontoledo](https://github.com/aarontoledo) for helping with the dreaded classificaitons bug 306 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python setup.py sdist --formats=gztar,zip 4 | python setup.py bdist --format=gztar,zip 5 | python setup.py bdist_egg 6 | 7 | -------------------------------------------------------------------------------- /build/lib/omniture/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import absolute_import 3 | 4 | import os 5 | import json 6 | import logging.config 7 | import io 8 | 9 | from .account import Account, Suite 10 | from .elements import Value 11 | from .query import Query, ReportNotSubmittedError 12 | from .reports import InvalidReportError, Report, DataWarehouseReport 13 | from .version import __version__ 14 | from .utils import AddressableList, affix 15 | 16 | 17 | def authenticate(username, secret=None, endpoint=Account.DEFAULT_ENDPOINT, prefix='', suffix=''): 18 | """ Authenticate to the Adobe API using WSSE """ 19 | #setup logging 20 | setup_logging() 21 | # if no secret is specified, we will assume that instead 22 | # we have received a dictionary with credentials (such as 23 | # from os.environ) 24 | if not secret: 25 | source = username 26 | key_to_username = affix(prefix, 'OMNITURE_USERNAME', suffix) 27 | key_to_secret = affix(prefix, 'OMNITURE_SECRET', suffix) 28 | username = source[key_to_username] 29 | secret = source[key_to_secret] 30 | 31 | return Account(username, secret, endpoint) 32 | 33 | 34 | def queue(queries): 35 | if isinstance(queries, dict): 36 | queries = queries.values() 37 | 38 | for query in queries: 39 | query.queue() 40 | 41 | 42 | def sync(queries, heartbeat=None, interval=1): 43 | """ 44 | `omniture.sync` will queue a number of reports and then 45 | block until the results are all ready. 46 | 47 | Queueing reports is idempotent, meaning that you can also 48 | use `omniture.sync` to fetch the results for queries that 49 | have already been queued: 50 | 51 | query = mysuite.report.range('2013-06-06').over_time('pageviews', 'page') 52 | omniture.queue(query) 53 | omniture.sync(query) 54 | 55 | The interval will operate under an exponetial decay until it reaches 5 minutes. At which point it will ping every 5 minutes 56 | """ 57 | 58 | queue(queries) 59 | 60 | if isinstance(queries, list): 61 | return [query.sync(heartbeat, interval) for query in queries] 62 | elif isinstance(queries, dict): 63 | return {key: query.sync(heartbeat, interval) for key, query in queries.items()} 64 | else: 65 | message = "Queries should be a list or a dictionary, received: {}".format( 66 | queries.__class__) 67 | raise ValueError(message) 68 | 69 | 70 | def setup_logging(default_path='logging.json', default_level=logging.INFO, env_key='LOG_CFG'): 71 | """Setup logging configuration. """ 72 | path = default_path 73 | value = os.getenv(env_key, None) 74 | if value: 75 | path = value 76 | if os.path.exists(path): 77 | with io.open(path, 'rt') as f: 78 | config = json.load(f) 79 | f.close() 80 | logging.config.dictConfig(config) 81 | requests_log = logging.getLogger("requests") 82 | requests_log.setLevel(logging.WARNING) 83 | 84 | else: 85 | logging.basicConfig(level=default_level) 86 | -------------------------------------------------------------------------------- /build/lib/omniture/account.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import print_function 3 | from future.utils import python_2_unicode_compatible 4 | 5 | import requests 6 | import binascii 7 | import json 8 | from datetime import datetime, date 9 | import logging 10 | import uuid 11 | import hashlib 12 | import base64 13 | import os 14 | from datetime import datetime 15 | 16 | from .elements import Value 17 | from .query import Query 18 | from . import reports 19 | from . import utils 20 | 21 | @python_2_unicode_compatible 22 | class Account(object): 23 | """ A wrapper for the Adobe Analytics API. Allows you to query the reporting API """ 24 | DEFAULT_ENDPOINT = 'https://api.omniture.com/admin/1.4/rest/' 25 | 26 | def __init__(self, username, secret, endpoint=DEFAULT_ENDPOINT, cache=False, cache_key=None): 27 | """Authentication to make requests.""" 28 | self.log = logging.getLogger(__name__) 29 | self.log.info(datetime.now().strftime("%Y-%m-%d %I%p:%M:%S")) 30 | self.username = username 31 | self.secret = secret 32 | self.endpoint = endpoint 33 | #Allow someone to set a custom cache key 34 | self.cache = cache 35 | if cache_key: 36 | self.cache_key = cache_key 37 | else: 38 | self.cache_key = date.today().isoformat() 39 | if self.cache: 40 | data = self.request_cached('Company', 'GetReportSuites')['report_suites'] 41 | else: 42 | data = self.request('Company', 'GetReportSuites')['report_suites'] 43 | suites = [Suite(suite['site_title'], suite['rsid'], self) for suite in data] 44 | self.suites = utils.AddressableList(suites) 45 | 46 | def request_cached(self, api, method, query={}, cache_key=None): 47 | if cache_key: 48 | key = cache_key 49 | else: 50 | key = self.cache_key 51 | 52 | #Generate a shortened hash of the query string so that method don't collide 53 | query_hash = base64.urlsafe_b64encode(hashlib.md5(query).digest()) 54 | 55 | try: 56 | with open(self.file_path+'/data_'+api+'_'+method+'_'+query_hash+'_'+key+'.txt') as fp: 57 | for line in fp: 58 | if line: 59 | data = ast.literal_eval(line) 60 | 61 | except IOError as e: 62 | data = self.request(api, method, query) 63 | 64 | # Capture all other old text files 65 | #TODO decide if the query should be included in the file list to be cleared out when the cache key changes 66 | filelist = [f for f in os.listdir(self.file_path) if f.startswith('data_'+api+'_'+method)] 67 | 68 | # Delete them 69 | for f in filelist: 70 | os.remove(self.file_path+'/'+f) 71 | 72 | # Build the new data 73 | the_file = open(self.file_path+'/data_'+api+'_'+method+'_'+query_hash+'_'+key+'.txt', 'w') 74 | the_file.write(str(data)) 75 | the_file.close() 76 | 77 | 78 | def request(self, api, method, query={}): 79 | """ 80 | Make a request to the Adobe APIs. 81 | 82 | * api -- the class of APIs you would like to call (e.g. Report, 83 | ReportSuite, Company, etc.) 84 | * method -- the method you would like to call inside that class 85 | of api 86 | * query -- a python object representing the parameters you would 87 | like to pass to the API 88 | """ 89 | self.log.info("Request: %s.%s Parameters: %s", api, method, query) 90 | response = requests.post( 91 | self.endpoint, 92 | params={'method': api + '.' + method}, 93 | data=json.dumps(query), 94 | headers=self._build_token() 95 | ) 96 | self.log.debug("Response for %s.%s:%s", api, method, response.text) 97 | json_response = response.json() 98 | 99 | if type(json_response) == dict: 100 | self.log.debug("Error Code %s", json_response.get('error')) 101 | if json_response.get('error') == 'report_not_ready': 102 | raise reports.ReportNotReadyError(json_response) 103 | elif json_response.get('error') != None: 104 | raise reports.InvalidReportError(json_response) 105 | else: 106 | return json_response 107 | else: 108 | return json_response 109 | 110 | def jsonReport(self, reportJSON): 111 | """Generates a Report from the JSON (including selecting the report suite)""" 112 | if type(reportJSON) == str: 113 | reportJSON = json.loads(reportJSON) 114 | suiteID = reportJSON['reportDescription']['reportSuiteID'] 115 | suite = self.suites[suiteID] 116 | return suite.jsonReport(reportJSON) 117 | 118 | 119 | def _serialize_header(self, properties): 120 | header = [] 121 | for key, value in properties.items(): 122 | header.append('{key}="{value}"'.format(key=key, value=value)) 123 | return ', '.join(header) 124 | 125 | def _build_token(self): 126 | nonce = str(uuid.uuid4()) 127 | base64nonce = binascii.b2a_base64(binascii.a2b_qp(nonce)) 128 | created_date = datetime.utcnow().isoformat() + 'Z' 129 | sha = nonce + created_date + self.secret 130 | sha_object = hashlib.sha1(sha.encode()) 131 | password_64 = binascii.b2a_base64(sha_object.digest()) 132 | 133 | properties = { 134 | "Username": self.username, 135 | "PasswordDigest": password_64.decode().strip(), 136 | "Nonce": base64nonce.decode().strip(), 137 | "Created": created_date, 138 | } 139 | header = 'UsernameToken ' + self._serialize_header(properties) 140 | 141 | return {'X-WSSE': header} 142 | 143 | def _repr_html_(self): 144 | """ Format in HTML for iPython Users """ 145 | html = "" 146 | html += "{0}: {1}
".format("Username", self.username) 147 | html += "{0}: {1}
".format("Secret", "***************") 148 | html += "{0}: {1}
".format("Report Suites", len(self.suites)) 149 | html += "{0}: {1}
".format("Endpoint", self.endpoint) 150 | return html 151 | 152 | def __str__(self): 153 | return "Analytics Account -------------\n Username: \ 154 | {0} \n Report Suites: {1} \n Endpoint: {2}" \ 155 | .format(self.username, len(self.suites), self.endpoint) 156 | 157 | 158 | class Suite(Value): 159 | """Lets you query a specific report suite. """ 160 | def request(self, api, method, query={}): 161 | raw_query = {} 162 | raw_query.update(query) 163 | if method == 'GetMetrics' or method == 'GetElements': 164 | raw_query['reportSuiteID'] = self.id 165 | 166 | return self.account.request(api, method, raw_query) 167 | 168 | def __init__(self, title, id, account, cache=False): 169 | self.log = logging.getLogger(__name__) 170 | super(Suite, self).__init__(title, id, account) 171 | self.account = account 172 | 173 | @property 174 | @utils.memoize 175 | def metrics(self): 176 | """ Return the list of valid metricsfor the current report suite""" 177 | if self.account.cache: 178 | data = self.request_cache('Report', 'GetMetrics') 179 | else: 180 | data = self.request('Report', 'GetMetrics') 181 | return Value.list('metrics', data, self, 'name', 'id') 182 | 183 | @property 184 | @utils.memoize 185 | def elements(self): 186 | """ Return the list of valid elementsfor the current report suite """ 187 | if self.account.cache: 188 | data = self.request_cached('Report', 'GetElements') 189 | else: 190 | data = self.request('Report', 'GetElements') 191 | return Value.list('elements', data, self, 'name', 'id') 192 | 193 | @property 194 | @utils.memoize 195 | def segments(self): 196 | """ Return the list of valid segments for the current report suite """ 197 | try: 198 | if self.account.cache: 199 | data = self.request_cached('Segments', 'Get',{"accessLevel":"shared"}) 200 | else: 201 | data = self.request('Segments', 'Get',{"accessLevel":"shared"}) 202 | return Value.list('segments', data, self, 'name', 'id',) 203 | except reports.InvalidReportError: 204 | data = [] 205 | return Value.list('segments', data, self, 'name', 'id',) 206 | 207 | @property 208 | def report(self): 209 | """ Return a report to be run on this report suite """ 210 | return Query(self) 211 | 212 | def jsonReport(self,reportJSON): 213 | """Creates a report from JSON. Accepts either JSON or a string. Useful for deserializing requests""" 214 | q = Query(self) 215 | #TODO: Add a method to the Account Object to populate the report suite this call will ignore it on purpose 216 | if type(reportJSON) == str: 217 | reportJSON = json.loads(reportJSON) 218 | 219 | reportJSON = reportJSON['reportDescription'] 220 | 221 | if 'dateFrom' in reportJSON and 'dateTo' in reportJSON: 222 | q = q.range(reportJSON['dateFrom'],reportJSON['dateTo']) 223 | elif 'dateFrom' in reportJSON: 224 | q = q.range(reportJSON['dateFrom']) 225 | elif 'date' in reportJSON: 226 | q = q.range(reportJSON['date']) 227 | else: 228 | q = q 229 | 230 | if 'dateGranularity' in reportJSON: 231 | q = q.granularity(reportJSON['dateGranularity']) 232 | 233 | if 'source' in reportJSON: 234 | q = q.set('source',reportJSON['source']) 235 | 236 | if 'metrics' in reportJSON: 237 | for m in reportJSON['metrics']: 238 | q = q.metric(m['id']) 239 | 240 | if 'elements' in reportJSON: 241 | for e in reportJSON['elements']: 242 | id = e['id'] 243 | del e['id'] 244 | q= q.element(id, **e) 245 | 246 | if 'locale' in reportJSON: 247 | q = q.set('locale',reportJSON['locale']) 248 | 249 | if 'sortMethod' in reportJSON: 250 | q = q.set('sortMethod',reportJSON['sortMethod']) 251 | 252 | if 'sortBy' in reportJSON: 253 | q = q.sortBy(reportJSON['sortBy']) 254 | 255 | #WARNING This doesn't carry over segment IDs meaning you can't manipulate the segments in the new object 256 | #TODO Loop through and add segment ID with filter method (need to figure out how to handle combined) 257 | if 'segments' in reportJSON: 258 | q = q.set('segments', reportJSON['segments']) 259 | 260 | if 'anomalyDetection' in reportJSON: 261 | q = q.set('anomalyDetection',reportJSON['anomalyDetection']) 262 | 263 | if 'currentData' in reportJSON: 264 | q = q.set('currentData',reportJSON['currentData']) 265 | 266 | if 'elementDataEncoding' in reportJSON: 267 | q = q.set('elementDataEncoding',reportJSON['elementDataEncoding']) 268 | return q 269 | 270 | def _repr_html_(self): 271 | """ Format in HTML for iPython Users """ 272 | return "{0}{1}".format(self.id, self.title) 273 | 274 | def __str__(self): 275 | return "ID {0:25} | Name: {1} \n".format(self.id, self.title) 276 | -------------------------------------------------------------------------------- /build/lib/omniture/elements.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | from future.utils import python_2_unicode_compatible 5 | 6 | import copy 7 | import logging 8 | 9 | from .import utils 10 | 11 | @python_2_unicode_compatible 12 | class Value(object): 13 | """ Searchable Dict. Can search on both the key and the value """ 14 | def __init__(self, title, id, parent, extra={}): 15 | self.log = logging.getLogger(__name__) 16 | self.title = str(title) 17 | self.id = id 18 | self.parent = parent 19 | self.properties = {'id': id} 20 | 21 | for k, v in extra.items(): 22 | setattr(self, k, v) 23 | 24 | @classmethod 25 | def list(cls, name, items, parent, title='title', id='id'): 26 | values = [cls(item[title], str(item[id]), parent, item) for item in items] 27 | return utils.AddressableList(values, name) 28 | 29 | def __repr__(self): 30 | print(self) 31 | return "<{title}: {id} in {parent}>".format(**self.__dict__) 32 | 33 | def copy(self): 34 | value = self.__class__(self.title, self.id, self.parent) 35 | value.properties = copy.copy(self.properties) 36 | return value 37 | 38 | def serialize(self): 39 | return self.properties 40 | 41 | def _repr_html_(self): 42 | """ Format in HTML for iPython Users """ 43 | return "{0}{1}".format(self.id, self.title) 44 | 45 | 46 | def __str__(self): 47 | """ allows users to print this out in a user friendly using print 48 | """ 49 | return "ID {0:25} | Name: {1} \n".format(self.id, self.title) 50 | -------------------------------------------------------------------------------- /build/lib/omniture/query.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | from future.utils import python_2_unicode_compatible 5 | 6 | import time 7 | from copy import copy, deepcopy 8 | import functools 9 | from dateutil.relativedelta import relativedelta 10 | import json 11 | import logging 12 | import sys 13 | 14 | from .elements import Value 15 | from . import reports 16 | from . import utils 17 | 18 | 19 | def immutable(method): 20 | @functools.wraps(method) 21 | def wrapped_method(self, *vargs, **kwargs): 22 | obj = self.clone() 23 | method(obj, *vargs, **kwargs) 24 | return obj 25 | 26 | return wrapped_method 27 | 28 | class ReportNotSubmittedError(Exception): 29 | """ Exception that is raised when a is requested by hasn't been submitted 30 | to Adobe 31 | """ 32 | def __init__(self,error): 33 | self.log = logging.getLogger(__name__) 34 | self.log.debug("Report Has not been submitted, call asynch() or run()") 35 | super(ReportNotSubmittedError, self).__init__("Report Not Submitted") 36 | 37 | @python_2_unicode_compatible 38 | class Query(object): 39 | """ Lets you build a query to the Reporting API for Adobe Analytics. 40 | 41 | Methods in this object are chainable. For example 42 | >>> report = report.element("page").element("prop1"). 43 | metric("pageviews").granularity("day").run() 44 | Making it easy to create a report. 45 | 46 | To see the raw definition use 47 | >>> print report 48 | """ 49 | 50 | GRANULARITY_LEVELS = ['hour', 'day', 'week', 'month', 'quarter', 'year'] 51 | STATUSES = ["Not Submitted","Not Ready","Done"] 52 | 53 | def __init__(self, suite): 54 | """ Setup the basic structure of the report query. """ 55 | self.log = logging.getLogger(__name__) 56 | self.suite = suite 57 | self.raw = {} 58 | #Put the report suite in so the user can print 59 | #the raw query and have it work as is 60 | self.raw['reportSuiteID'] = str(self.suite.id) 61 | self.id = None 62 | self.method = "Get" 63 | self.status = self.STATUSES[0] 64 | #The report object 65 | self.report = reports.Report 66 | #The fully hydrated report object 67 | self.processed_response = None 68 | self.unprocessed_response = None 69 | 70 | def _normalize_value(self, value, category): 71 | if isinstance(value, Value): 72 | return value 73 | else: 74 | return getattr(self.suite, category)[value] 75 | 76 | def _serialize_value(self, value, category): 77 | return self._normalize_value(value, category).serialize() 78 | 79 | def _serialize_values(self, values, category): 80 | if not isinstance(values, list): 81 | values = [values] 82 | 83 | return [self._serialize_value(value, category) for value in values] 84 | 85 | def _serialize(self, obj): 86 | if isinstance(obj, list): 87 | return [self._serialize(el) for el in obj] 88 | elif isinstance(obj, Value): 89 | return obj.serialize() 90 | else: 91 | return obj 92 | 93 | def clone(self): 94 | """ Return a copy of the current object. """ 95 | query = Query(self.suite) 96 | query.raw = copy(self.raw) 97 | query.report = self.report 98 | query.status = self.status 99 | query.processed_response = self.processed_response 100 | query.unprocessed_response = self.unprocessed_response 101 | return query 102 | 103 | @immutable 104 | def range(self, start, stop=None, months=0, days=0, granularity=None): 105 | """ 106 | Define a date range for the report. 107 | 108 | * start -- The start date of the report. If stop is not present 109 | it is assumed to be the to and from dates. 110 | * stop (optional) -- the end date of the report (inclusive). 111 | * months (optional, named) -- months to run used for relative dates 112 | * days (optional, named)-- days to run used for relative dates) 113 | * granulartiy (optional, named) -- set the granularity for the report 114 | """ 115 | start = utils.date(start) 116 | stop = utils.date(stop) 117 | 118 | if days or months: 119 | stop = start + relativedelta(days=days-1, months=months) 120 | else: 121 | stop = stop or start 122 | 123 | if start == stop: 124 | self.raw['date'] = start.isoformat() 125 | else: 126 | self.raw.update({ 127 | 'dateFrom': start.isoformat(), 128 | 'dateTo': stop.isoformat(), 129 | }) 130 | 131 | if granularity: 132 | self.raw = self.granularity(granularity).raw 133 | 134 | return self 135 | 136 | @immutable 137 | def granularity(self, granularity): 138 | """ 139 | Set the granulartiy for the report. 140 | 141 | Values are one of the following 142 | 'hour', 'day', 'week', 'month', 'quarter', 'year' 143 | """ 144 | if granularity not in self.GRANULARITY_LEVELS: 145 | levels = ", ".join(self.GRANULARITY_LEVELS) 146 | raise ValueError("Granularity should be one of: " + levels) 147 | 148 | self.raw['dateGranularity'] = granularity 149 | 150 | return self 151 | 152 | @immutable 153 | def set(self, key=None, value=None, **kwargs): 154 | """ 155 | Set a custom property in the report 156 | 157 | `set` is a way to add raw properties to the request, 158 | for features that python-omniture does not support but the 159 | SiteCatalyst API does support. For convenience's sake, 160 | it will serialize Value and Element objects but will 161 | leave any other kind of value alone. 162 | """ 163 | 164 | if key and value: 165 | self.raw[key] = self._serialize(value) 166 | elif key or kwargs: 167 | properties = key or kwargs 168 | for key, value in properties.items(): 169 | self.raw[key] = self._serialize(value) 170 | else: 171 | raise ValueError("Query#set requires a key and value, \ 172 | a properties dictionary or keyword arguments.") 173 | 174 | return self 175 | 176 | @immutable 177 | def filter(self, segment=None, segments=None, disable_validation=False, **kwargs): 178 | """ Set Add a segment to the report. """ 179 | # It would appear to me that 'segment_id' has a strict subset 180 | # of the functionality of 'segments', but until I find out for 181 | # sure, I'll provide both options. 182 | if 'segments' not in self.raw: 183 | self.raw['segments'] = [] 184 | 185 | if disable_validation == False: 186 | if segments: 187 | self.raw['segments'].extend(self._serialize_values(segments, 'segments')) 188 | elif segment: 189 | self.raw['segments'].append({"id":self._normalize_value(segment, 190 | 'segments').id}) 191 | elif kwargs: 192 | self.raw['segments'].append(kwargs) 193 | else: 194 | raise ValueError() 195 | 196 | else: 197 | if segments: 198 | self.raw['segments'].extend([{"id":segment} for segment in segments]) 199 | elif segment: 200 | self.raw['segments'].append({"id":segment}) 201 | elif kwargs: 202 | self.raw['segments'].append(kwargs) 203 | else: 204 | raise ValueError() 205 | return self 206 | 207 | @immutable 208 | def element(self, element, disable_validation=False, **kwargs): 209 | """ 210 | Add an element to the report. 211 | 212 | This method is intended to be called multiple time. Each time it will 213 | add an element as a breakdown 214 | After the first element, each additional element is considered 215 | a breakdown 216 | """ 217 | 218 | if self.raw.get('elements', None) == None: 219 | self.raw['elements'] = [] 220 | 221 | if disable_validation == False: 222 | element = self._serialize_value(element, 'elements') 223 | else: 224 | element = {"id":element} 225 | 226 | if kwargs != None: 227 | element.update(kwargs) 228 | self.raw['elements'].append(deepcopy(element)) 229 | 230 | #TODO allow this method to accept a list 231 | return self 232 | 233 | 234 | def breakdown(self, element, **kwargs): 235 | """ Pass through for element. Adds an element to the report. """ 236 | return self.element(element, **kwargs) 237 | 238 | 239 | def elements(self, *args, **kwargs): 240 | """ Shortcut for adding multiple elements. Doesn't support arguments """ 241 | obj = self 242 | for e in args: 243 | obj = obj.element(e, **kwargs) 244 | 245 | return obj 246 | 247 | @immutable 248 | def metric(self, metric, disable_validation=False): 249 | """ 250 | Add an metric to the report. 251 | 252 | This method is intended to be called multiple time. 253 | Each time a metric will be added to the report 254 | """ 255 | if self.raw.get('metrics', None) == None: 256 | self.raw['metrics'] = [] 257 | 258 | # If the metric provided is a list, return after list is read 259 | if isinstance(metric, list): 260 | 261 | for m in metric: 262 | 263 | if disable_validation == False: 264 | self.raw['metrics'].append(self._serialize_value(m, 'metrics')) 265 | else: 266 | self.raw['metrics'].append({"id":m}) 267 | 268 | 269 | return self 270 | 271 | # Process single metric 272 | if disable_validation == False: 273 | self.raw['metrics'].append(self._serialize_value(metric, 'metrics')) 274 | else: 275 | self.raw['metrics'].append({"id":metric}) 276 | 277 | #self.raw['metrics'] = self._serialize_values(metric, 'metrics') 278 | #TODO allow this metric to accept a list 279 | return self 280 | 281 | def metrics(self, *args, **kwargs): 282 | """ Shortcut for adding multiple metrics """ 283 | obj = self 284 | for m in args: 285 | obj = obj.metric(m, **kwargs) 286 | 287 | return obj 288 | 289 | @immutable 290 | def sortBy(self, metric): 291 | """ Specify the sortBy Metric """ 292 | self.raw['sortBy'] = metric 293 | return self 294 | 295 | @immutable 296 | def currentData(self): 297 | """ Set the currentData flag """ 298 | self.raw['currentData'] = True 299 | return self 300 | 301 | 302 | def build(self): 303 | """ Return the report descriptoin as an object """ 304 | return {'reportDescription': self.raw} 305 | 306 | def queue(self): 307 | """ Submits the report to the Queue on the Adobe side. """ 308 | q = self.build() 309 | self.log.debug("Suite Object: %s Method: %s, Query %s", 310 | self.suite, self.report.method, q) 311 | self.id = self.suite.request('Report', 312 | self.report.method, 313 | q)['reportID'] 314 | self.status = self.STATUSES[1] 315 | return self 316 | 317 | def probe(self, heartbeat=None, interval=1, soak=False): 318 | """ Keep checking until the report is done""" 319 | #Loop until the report is done 320 | while self.is_ready() == False: 321 | if heartbeat: 322 | heartbeat() 323 | time.sleep(interval) 324 | #Use a back off up to 30 seconds to play nice with the APIs 325 | if interval < 1: 326 | interval = 1 327 | elif interval < 30: 328 | interval = round(interval * 1.5) 329 | else: 330 | interval = 30 331 | self.log.debug("Check Interval: %s seconds", interval) 332 | 333 | def is_ready(self): 334 | """ inspects the response to see if the report is ready """ 335 | if self.status == self.STATUSES[0]: 336 | raise ReportNotSubmittedError('{"message":"Doh! the report needs to be submitted first"}') 337 | elif self.status == self.STATUSES[1]: 338 | try: 339 | # the request method catches the report and populates it automatically 340 | response = self.suite.request('Report','Get',{'reportID': self.id}) 341 | self.status = self.STATUSES[2] 342 | self.unprocessed_response = response 343 | self.processed_response = self.report(response, self) 344 | return True 345 | except reports.ReportNotReadyError: 346 | self.status = self.STATUSES[1] 347 | #raise reports.InvalidReportError(response) 348 | return False 349 | elif self.status == self.STATUSES[2]: 350 | return True 351 | 352 | 353 | def sync(self, heartbeat=None, interval=0.01): 354 | """ Run the report synchronously,""" 355 | if self.status == self.STATUSES[0]: 356 | self.queue() 357 | self.probe(heartbeat, interval) 358 | if self.status == self.STATUSES[1]: 359 | self.probe() 360 | return self.processed_response 361 | 362 | def asynch(self, callback=None, heartbeat=None, interval=1): 363 | """ Run the Report Asynchrnously """ 364 | if self.status == self.STATUSES[0]: 365 | self.queue() 366 | return self 367 | 368 | def get_report(self): 369 | self.is_ready() 370 | if self.status == self.STATUSES[2]: 371 | return self.processed_response 372 | else: 373 | raise reports.ReportNotReadyError('{"message":"Doh! the report is not ready yet"}') 374 | 375 | def run(self, defaultheartbeat=True, heartbeat=None, interval=0.01): 376 | """Shortcut for sync(). Runs the current report synchronously. """ 377 | if defaultheartbeat == True: 378 | rheartbeat = self.heartbeat 379 | else: 380 | rheartbeat = heartbeat 381 | 382 | return self.sync(rheartbeat, interval) 383 | 384 | def heartbeat(self): 385 | """ A default heartbeat method that prints a dot for each request """ 386 | sys.stdout.write('.') 387 | sys.stdout.flush() 388 | 389 | 390 | def check(self): 391 | """ 392 | Basically an alias to is ready to make the interface a bit better 393 | """ 394 | return self.is_ready() 395 | 396 | def cancel(self): 397 | """ Cancels a the report from the Queue on the Adobe side. """ 398 | return self.suite.request('Report', 399 | 'CancelReport', 400 | {'reportID': self.id}) 401 | def json(self): 402 | """ Return a JSON string of the Request """ 403 | return str(json.dumps(self.build(), indent=4, separators=(',', ': '), sort_keys=True)) 404 | 405 | def __str__(self): 406 | return self.json() 407 | 408 | def _repr_html_(self): 409 | """ Format in HTML for iPython Users """ 410 | report = { str(key):value for key,value in self.raw.items() } 411 | html = "Current Report Settings
" 412 | for k,v in sorted(list(report.items())): 413 | html += "{0}: {1}
".format(k,v) 414 | if self.id: 415 | html += "This report has been submitted
" 416 | html += "{0}: {1}
".format("ReportId", self.id) 417 | return html 418 | 419 | 420 | def __dir__(self): 421 | """ Give sensible options for Tab Completion mostly for iPython """ 422 | return ['asynch','breakdown','cancel','clone','currentData', 'element', 423 | 'filter', 'granularity', 'id','json' ,'metric', 'queue', 'range', 'raw', 'report', 424 | 'request', 'run', 'set', 'sortBy', 'suite'] -------------------------------------------------------------------------------- /build/lib/omniture/reports.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | from future.utils import python_2_unicode_compatible 5 | 6 | import logging 7 | from datetime import datetime 8 | import json 9 | 10 | from .elements import Value 11 | 12 | import warnings 13 | 14 | class InvalidReportError(Exception): 15 | """ 16 | Exception raised when the API says a report defintion is 17 | invalid 18 | """ 19 | def normalize(self, error): 20 | print ('error', error) 21 | return { 22 | 'error': error.get('error'), 23 | 'error_description': error.get('error_description'), 24 | 'error_uri': error.get('error_uri', ''), 25 | } 26 | 27 | def __init__(self, error): 28 | self.log = logging.getLogger(__name__) 29 | error = self.normalize(error) 30 | self.message = "{error}: {error_description} ({error_uri})".format(**error) 31 | super(InvalidReportError, self).__init__(self.message) 32 | 33 | class ReportNotReadyError(Exception): 34 | """ Exception that is raised when a report is not ready to be downloaded 35 | """ 36 | def __init__(self,error): 37 | self.log = logging.getLogger(__name__) 38 | self.log.debug("Report Not Ready") 39 | super(ReportNotReadyError, self).__init__("Report Not Ready") 40 | 41 | 42 | # TODO: also make this iterable (go through rows) 43 | @python_2_unicode_compatible 44 | class Report(object): 45 | """ 46 | Object to parse the responses of the report 47 | 48 | To get the data use 49 | >>> report.data 50 | 51 | To get a Pandas DataFrame use 52 | >>> report.dataframe 53 | 54 | To get the raw response use 55 | >>> print report 56 | 57 | """ 58 | def process(self): 59 | """ Parse out the relevant data from the report and store it for easy access 60 | Should only be used internally to the class 61 | """ 62 | self.timing = { 63 | 'queue': float(self.raw['waitSeconds']), 64 | 'execution': float(self.raw['runSeconds']), 65 | } 66 | self.log.debug("Report Wait Time: %s, Report Execution Time: %s", self.timing['queue'], self.timing['execution']) 67 | self.report = report = self.raw['report'] 68 | self.metrics = Value.list('metrics', report['metrics'], self.suite, 'name', 'id') 69 | self.elements = Value.list('elements', report['elements'], self.suite, 'name', 'id') 70 | self.period = str(report['period']) 71 | self.type = str(report['type']) 72 | 73 | segments = report.get('segments') 74 | if segments: 75 | self.segments = [] 76 | for s in segments: 77 | try: 78 | self.segments.append(self.query.suite.segments[s['id']]) 79 | except KeyError as e: 80 | warnings.warn(repr(e)) 81 | self.segments.append(s['id']) 82 | else: 83 | self.segments = None 84 | 85 | #Set as none until it is actually used 86 | self.dict_data = None 87 | self.pandas_data = None 88 | 89 | @property 90 | def data(self): 91 | """ Returns the report data as a set of dicts for easy quering 92 | It generates the dicts on the 1st call then simply returns the reference to the data in subsequent calls 93 | """ 94 | #If the data hasn't been generate it generate the data 95 | if self.dict_data == None: 96 | self.dict_data = self.parse_rows(self.report['data']) 97 | 98 | return self.dict_data 99 | 100 | def parse_rows(self,row, level=0, upperlevels=None): 101 | """ 102 | Parse through the data returned by a repor. Return a list of dicts. 103 | 104 | This method is recursive. 105 | """ 106 | #self.log.debug("Level %s, Upperlevels %s, Row Type %s, Row: %s", level,upperlevels, type(row), row) 107 | data = {} 108 | data_set = [] 109 | 110 | #merge in the upper levels 111 | if upperlevels != None: 112 | data.update(upperlevels) 113 | 114 | 115 | if type(row) == list: 116 | for r in row: 117 | #on the first call set add to the empty list 118 | pr = self.parse_rows(r,level, data.copy()) 119 | if type(pr) == dict: 120 | data_set.append(pr) 121 | #otherwise add to the existing list 122 | else: 123 | data_set.extend(pr) 124 | 125 | #pull out the metrics from the lowest level 126 | if type(row) == dict: 127 | #pull out any relevant data from the current record 128 | #Handle datetime isn't in the elements list for trended reports 129 | if level == 0 and self.type == "trended": 130 | element = "datetime" 131 | elif self.type == "trended": 132 | if hasattr(self.elements[level-1], 'classification'): 133 | #handle the case where there are multiple classifications 134 | element = str(self.elements[level-1].id) + ' | ' + str(self.elements[level-1].classification) 135 | else: 136 | element = str(self.elements[level-1].id) 137 | else: 138 | if hasattr(self.elements[level], 'classification'): 139 | #handle the case where there are multiple classifications 140 | element = str(self.elements[level].id) + ' | ' + str(self.elements[level].classification) 141 | else: 142 | element = str(self.elements[level].id) 143 | 144 | 145 | if element == "datetime": 146 | data[element] = datetime(int(row.get('year',0)),int(row.get('month',0)),int(row.get('day',0)),int(row.get('hour',0))) 147 | data["datetime_friendly"] = str(row['name']) 148 | else: 149 | try: 150 | data[element] = str(row['name']) 151 | 152 | # If the name value is Null or non-encodable value, return null 153 | except: 154 | data[element] = "null" 155 | #parse out any breakdowns and add to the data set 156 | if 'breakdown' in row and len(row['breakdown']) > 0: 157 | data_set.extend(self.parse_rows(row['breakdown'], level+1, data)) 158 | elif 'counts' in row: 159 | for index, metric in enumerate(row['counts']): 160 | #decide what type of event 161 | if metric == 'INF': 162 | data[str(self.metrics[index].id)] = 0 163 | elif self.metrics[index].decimals > 0 or metric.find('.') >-1: 164 | data[str(self.metrics[index].id)] = float(metric) 165 | else: 166 | data[str(self.metrics[index].id)] = int(metric) 167 | 168 | 169 | 170 | if len(data_set)>0: 171 | return data_set 172 | else: 173 | return data 174 | 175 | @property 176 | def dataframe(self): 177 | """ 178 | Returns pandas DataFrame for additional analysis. 179 | 180 | Will generate the data the first time it is called otherwise passes a cached version 181 | """ 182 | 183 | if self.pandas_data is None: 184 | self.pandas_data = self.to_dataframe() 185 | 186 | return self.pandas_data 187 | 188 | 189 | def to_dataframe(self): 190 | import pandas as pd 191 | return pd.DataFrame.from_dict(self.data) 192 | 193 | def __init__(self, raw, query): 194 | self.log = logging.getLogger(__name__) 195 | self.raw = raw 196 | self.query = query 197 | self.suite = query.suite 198 | self.process() 199 | 200 | def __repr__(self): 201 | info = { 202 | 'metrics': ", ".join(map(str, self.metrics)), 203 | 'elements': ", ".join(map(str, self.elements)), 204 | } 205 | return "".format(**info) 206 | 207 | def __dir__(self): 208 | """ Give sensible options for Tab Completion mostly for iPython """ 209 | return ['data','dataframe', 'metrics','elements', 'segments', 'period', 'type', 'timing'] 210 | 211 | def _repr_html_(self): 212 | """ Format in HTML for iPython Users """ 213 | html = "" 214 | for index, item in enumerate(self.data): 215 | html += "" 216 | #populate header Row 217 | if index < 1: 218 | html += "" 219 | if 'datetime' in item: 220 | html += "".format('datetime') 221 | for key in sorted(list(item.keys())): 222 | if key != 'datetime': 223 | html += "".format(key) 224 | html += "" 225 | 226 | #Make sure date time is alway listed first 227 | if 'datetime' in item: 228 | html += "".format(item['datetime']) 229 | for key, value in sorted(list(item.items())): 230 | if key != 'datetime': 231 | html += "".format(value) 232 | html += "" 233 | return html 234 | 235 | def __str__(self): 236 | return json.dumps(self.raw,indent=4, separators=(',', ': '),sort_keys=True) 237 | 238 | Report.method = "Queue" 239 | 240 | 241 | class DataWarehouseReport(object): 242 | pass 243 | 244 | DataWarehouseReport.method = 'Request' 245 | -------------------------------------------------------------------------------- /build/lib/omniture/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from copy import copy 4 | import datetime 5 | from dateutil.parser import parse as parse_date 6 | import six 7 | 8 | 9 | class memoize: 10 | def __init__(self, function): 11 | self.function = function 12 | self.memoized = {} 13 | 14 | def __call__(self, *args): 15 | try: 16 | return self.memoized[args] 17 | except KeyError: 18 | self.memoized[args] = self.function(*args) 19 | return self.memoized[args] 20 | 21 | 22 | class AddressableList(list): 23 | """ List of items addressable either by id or by name """ 24 | def __init__(self, items, name='items'): 25 | super(AddressableList, self).__init__(items) 26 | self.name = name 27 | 28 | def __getitem__(self, key): 29 | if isinstance(key, int): 30 | return super(AddressableList, self).__getitem__(key) 31 | else: 32 | matches = [item for item in self if item.title == key or item.id == key] 33 | count = len(matches) 34 | if count > 1: 35 | matches = list(map(repr, matches)) 36 | error = "Found multiple matches for {key}: {matches}. ".format( 37 | key=key, matches=", ".join(matches)) 38 | advice = "Use the identifier instead." 39 | raise KeyError(error + advice) 40 | elif count == 1: 41 | return matches[0] 42 | else: 43 | raise KeyError("Cannot find {key} among the available {name}" 44 | .format(key=key, name=self.name)) 45 | 46 | def _repr_html_(self): 47 | """ HTML formating for iPython users """ 48 | html = "
{0}{0}
{0}{0}
" 49 | html += "".format("ID", "Title") 50 | for i in self: 51 | html += "" 52 | html += i._repr_html_() 53 | html += "" 54 | html +="
{0}{1}
" 55 | return html 56 | 57 | def __str__(self): 58 | string = "" 59 | for i in self: 60 | string += i.__str__() 61 | return string 62 | 63 | def __repr__(self): 64 | return "" 65 | 66 | 67 | def date(obj): 68 | #used to ensure compatibility with Python3 without having to user six 69 | try: 70 | basestring 71 | except NameError: 72 | basestring = str 73 | 74 | if obj is None: 75 | return None 76 | elif isinstance(obj, datetime.date): 77 | if hasattr(obj, 'date'): 78 | return obj.date() 79 | else: 80 | return obj 81 | elif isinstance(obj, six.string_types): 82 | return parse_date(obj).date() 83 | elif isinstance(obj, six.text_type): 84 | return parse_date(str(obj)).date() 85 | else: 86 | raise ValueError("Can only convert strings into dates, received {}" 87 | .format(obj.__class__)) 88 | 89 | 90 | def wrap(obj): 91 | if isinstance(obj, list): 92 | return obj 93 | else: 94 | return [obj] 95 | 96 | 97 | def affix(prefix=None, base=None, suffix=None, connector='_'): 98 | if prefix: 99 | prefix = prefix + connector 100 | else: 101 | prefix = '' 102 | 103 | if suffix: 104 | suffix = connector + suffix 105 | else: 106 | suffix = '' 107 | 108 | return prefix + base + suffix 109 | 110 | 111 | def translate(d, mapping): 112 | d = copy(d) 113 | 114 | for src, dest in mapping.items(): 115 | if src in d: 116 | d[dest] = d[src] 117 | del d[src] 118 | 119 | return d 120 | -------------------------------------------------------------------------------- /build/lib/omniture/version.py: -------------------------------------------------------------------------------- 1 | # 1) we don't load dependencies by storing it in __init__.py 2 | # 2) we can import it in setup.py for the same reason 3 | # 3) we can import it into your module module 4 | __version__ = '0.6.0' 5 | -------------------------------------------------------------------------------- /build/lib/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dancingcactus/python-omniture/572b6c6ae4126631053e981404eb84cad16bc1ec/build/lib/tests/__init__.py -------------------------------------------------------------------------------- /build/lib/tests/testAccount.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import unittest 4 | import requests_mock 5 | import omniture 6 | import os 7 | 8 | creds = {} 9 | creds['username'] = os.environ['OMNITURE_USERNAME'] 10 | creds['secret'] = os.environ['OMNITURE_SECRET'] 11 | test_report_suite = "omniture.api-gateway" 12 | 13 | 14 | class AccountTest(unittest.TestCase): 15 | def setUp(self): 16 | with requests_mock.mock() as m: 17 | path = os.path.dirname(__file__) 18 | #read in mock response for Company.GetReportSuites to make tests faster 19 | with open(path+'/mock_objects/Company.GetReportSuites.json') as get_report_suites_file: 20 | report_suites = get_report_suites_file.read() 21 | 22 | with open(path+'/mock_objects/Report.GetMetrics.json') as get_metrics_file: 23 | metrics = get_metrics_file.read() 24 | 25 | with open(path+'/mock_objects/Report.GetElements.json') as get_elements_file: 26 | elements = get_elements_file.read() 27 | 28 | with open(path+'/mock_objects/Segments.Get.json') as get_segments_file: 29 | segments = get_segments_file.read() 30 | 31 | #setup mock responses 32 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Company.GetReportSuites', text=report_suites) 33 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.GetMetrics', text=metrics) 34 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.GetElements', text=elements) 35 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Segments.Get', text=segments) 36 | 37 | 38 | self.analytics = omniture.authenticate(creds['username'], creds['secret']) 39 | #force requests to happen in this method so they are cached 40 | self.analytics.suites[test_report_suite].metrics 41 | self.analytics.suites[test_report_suite].elements 42 | self.analytics.suites[test_report_suite].segments 43 | 44 | 45 | def test_os_environ(self): 46 | test = omniture.authenticate({'OMNITURE_USERNAME':creds['username'], 47 | 'OMNITURE_SECRET':creds['secret']}) 48 | self.assertEqual(test.username,creds['username'], 49 | "The username isn't getting set right: {}" 50 | .format(test.username)) 51 | 52 | self.assertEqual(test.secret,creds['secret'], 53 | "The secret isn't getting set right: {}" 54 | .format(test.secret)) 55 | 56 | def test_suites(self): 57 | self.assertIsInstance(self.analytics.suites, omniture.utils.AddressableList, "There are no suites being returned") 58 | self.assertIsInstance(self.analytics.suites[test_report_suite], omniture.account.Suite, "There are no suites being returned") 59 | 60 | def test_simple_request(self): 61 | """ simplest request possible. Company.GetEndpoint is not an authenticated method 62 | """ 63 | urls = ["https://api.omniture.com/admin/1.4/rest/", 64 | "https://api2.omniture.com/admin/1.4/rest/", 65 | "https://api3.omniture.com/admin/1.4/rest/", 66 | "https://api4.omniture.com/admin/1.4/rest/", 67 | "https://api5.omniture.com/admin/1.4/rest/"] 68 | self.assertIn(self.analytics.request('Company', 'GetEndpoint'),urls, "Company.GetEndpoint failed" ) 69 | 70 | def test_authenticated_request(self): 71 | """ Request that requires authentication to make sure the auth is working 72 | """ 73 | reportsuites = self.analytics.request('Company','GetReportSuites') 74 | self.assertIsInstance(reportsuites, dict, "Didn't get a valid response back") 75 | self.assertIsInstance(reportsuites['report_suites'], list, "Response doesn't contain the list of report suites might be an authentication issue") 76 | 77 | def test_metrics(self): 78 | """ Makes sure the suite properties can get the list of metrics 79 | """ 80 | self.assertIsInstance(self.analytics.suites[test_report_suite].metrics, omniture.utils.AddressableList) 81 | 82 | def test_elements(self): 83 | """ Makes sure the suite properties can get the list of elements 84 | """ 85 | self.assertIsInstance(self.analytics.suites[test_report_suite].elements, omniture.utils.AddressableList) 86 | 87 | def test_basic_report(self): 88 | """ Make sure a basic report can be run 89 | """ 90 | report = self.analytics.suites[test_report_suite].report 91 | queue = [] 92 | queue.append(report) 93 | response = omniture.sync(queue) 94 | self.assertIsInstance(response, list) 95 | 96 | def test_json_report(self): 97 | """Make sure reports can be generated from JSON objects""" 98 | report = self.analytics.suites[test_report_suite].report\ 99 | .element('page')\ 100 | .metric('pageviews')\ 101 | .sortBy('pageviews')\ 102 | .filter("s4157_55b1ba24e4b0a477f869b912")\ 103 | .range("2016-08-01","2016-08-31")\ 104 | .set('sortMethod',"top")\ 105 | .json() 106 | self.assertEqual(report, self.analytics.jsonReport(report).json(), "The reports aren't serializating or de-serializing correctly in JSON") 107 | 108 | 109 | def test_account_repr_html_(self): 110 | """Make sure the account are printing out in 111 | HTML correctly for ipython notebooks""" 112 | html = self.analytics._repr_html_() 113 | test_html = "Username: jgrover:Justin Grover Demo
Secret: ***************
Report Suites: 2
Endpoint: https://api.omniture.com/admin/1.4/rest/
" 114 | self.assertEqual(html, test_html) 115 | 116 | def test_account__str__(self): 117 | """ Make sure the custom str works """ 118 | mystr = self.analytics.__str__() 119 | test_str = "Analytics Account -------------\n Username: jgrover:Justin Grover Demo \n Report Suites: 2 \n Endpoint: https://api.omniture.com/admin/1.4/rest/" 120 | self.assertEqual(mystr, test_str) 121 | 122 | def test_suite_repr_html_(self): 123 | """Make sure the Report Suites are printing okay for 124 | ipython notebooks """ 125 | html = self.analytics.suites[0]._repr_html_() 126 | test_html = "omniture.api-gatewaytest_suite" 127 | self.assertEqual(html, test_html) 128 | 129 | def test_suite__str__(self): 130 | """Make sure the str represntation is working """ 131 | mystr = self.analytics.suites[0].__str__() 132 | test_str = "ID omniture.api-gateway | Name: test_suite \n" 133 | self.assertEqual(mystr,test_str) 134 | 135 | 136 | if __name__ == '__main__': 137 | unittest.main() 138 | -------------------------------------------------------------------------------- /build/lib/tests/testAll.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import unittest 4 | 5 | from omniture.tests import AccountTest 6 | from .testQuery import QueryTest 7 | from .testReports import ReportTest 8 | from .testElement import ElementTest 9 | import sys 10 | 11 | 12 | def test_suite(): 13 | """ Test Suite for omnitue module """ 14 | 15 | test_suite = unittest.TestSuite() 16 | test_suite.addTest(unittest.makeSuite(AccountTest)) 17 | test_suite.addTest(unittest.makeSuite(QueryTest)) 18 | test_suite.addTest(unittest.makeSuite(ReportTest)) 19 | test_suite.addTest(unittest.makeSuite(AccountTest)) 20 | 21 | return test_suite 22 | 23 | mySuite = test_suite() 24 | 25 | runner = unittest.TextTestRunner() 26 | ret = runner.run(mySuite).wasSuccessful() 27 | sys.exit(not ret) 28 | -------------------------------------------------------------------------------- /build/lib/tests/testElement.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import unittest 4 | import omniture 5 | import os 6 | 7 | creds = {} 8 | creds['username'] = os.environ['OMNITURE_USERNAME'] 9 | creds['secret'] = os.environ['OMNITURE_SECRET'] 10 | 11 | class ElementTest(unittest.TestCase): 12 | def setUp(self): 13 | fake_list = [{"id":"123","title":"ABC"},{"id":"456","title":"DEF"}] 14 | self.valueList = omniture.elements.Value.list("metrics",fake_list,"test") 15 | 16 | def test__repr__(self): 17 | self.assertEqual(self.valueList.__repr__(),"",\ 18 | "The value for __repr__ on the AddressableList was {}"\ 19 | .format(self.valueList.__repr__())) 20 | 21 | def test_value__repr__(self): 22 | self.assertEqual(self.valueList[0].__repr__(),"", \ 23 | "The value of the first item in the AddressableList \ 24 | was {}".format(self.valueList[0].__repr__())) 25 | 26 | def test_value__copy__(self): 27 | value = self.valueList[0].copy() 28 | self.assertEqual(value.__repr__(), self.valueList[0].__repr__(),\ 29 | "The copied value was: {} the original was: {}"\ 30 | .format(value, self.valueList[0])) 31 | 32 | def test_repr_html_(self): 33 | self.assertEqual(str(self.valueList[0]._repr_html_()),\ 34 | "123ABC",\ 35 | "The html value was: {}"\ 36 | .format(self.valueList[0]._repr_html_())) 37 | 38 | def test__str__(self): 39 | self.assertEqual(self.valueList[0].__str__(),\ 40 | "ID 123 | Name: ABC \n",\ 41 | "__str__ returned: {}"\ 42 | .format(self.valueList[0].__str__())) 43 | 44 | if __name__ == '__main__': 45 | unittest.main() 46 | -------------------------------------------------------------------------------- /build/lib/tests/testReports.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from __future__ import print_function 3 | 4 | 5 | import unittest 6 | import omniture 7 | import os 8 | from datetime import date 9 | import pandas 10 | import datetime 11 | import requests_mock 12 | 13 | 14 | creds = {} 15 | creds['username'] = os.environ['OMNITURE_USERNAME'] 16 | creds['secret'] = os.environ['OMNITURE_SECRET'] 17 | test_report_suite = 'omniture.api-gateway' 18 | 19 | 20 | class ReportTest(unittest.TestCase): 21 | def setUp(self): 22 | self.maxDiff = None 23 | with requests_mock.mock() as m: 24 | path = os.path.dirname(__file__) 25 | #read in mock response for Company.GetReportSuites to make tests faster 26 | with open(path+'/mock_objects/Company.GetReportSuites.json') as get_report_suites_file: 27 | report_suites = get_report_suites_file.read() 28 | 29 | with open(path+'/mock_objects/Report.GetMetrics.json') as get_metrics_file: 30 | metrics = get_metrics_file.read() 31 | 32 | with open(path+'/mock_objects/Report.GetElements.json') as get_elements_file: 33 | elements = get_elements_file.read() 34 | 35 | with open(path+'/mock_objects/Segments.Get.json') as get_segments_file: 36 | segments = get_segments_file.read() 37 | 38 | #setup mock responses 39 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Company.GetReportSuites', text=report_suites) 40 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.GetMetrics', text=metrics) 41 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.GetElements', text=elements) 42 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Segments.Get', text=segments) 43 | 44 | 45 | self.analytics = omniture.authenticate(creds['username'], creds['secret']) 46 | #force requests to happen in this method so they are cached 47 | self.analytics.suites[test_report_suite].metrics 48 | self.analytics.suites[test_report_suite].elements 49 | self.analytics.suites[test_report_suite].segments 50 | 51 | def tearDown(self): 52 | self.analytics = None 53 | 54 | @requests_mock.mock() 55 | def test_basic_report(self,m): 56 | """ Make sure a basic report can be run 57 | """ 58 | 59 | path = os.path.dirname(__file__) 60 | 61 | with open(path+'/mock_objects/basic_report.json') as data_file: 62 | json_response = data_file.read() 63 | 64 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 65 | report_queue = queue_file.read() 66 | 67 | #setup mock object 68 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 69 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 70 | 71 | response = self.analytics.suites[test_report_suite].report.run() 72 | 73 | self.assertIsInstance(response.data, list, "Something went wrong with the report") 74 | 75 | #Timing Info 76 | self.assertIsInstance(response.timing['queue'], float, "waitSeconds info is missing") 77 | self.assertIsInstance(response.timing['execution'], float, "Execution info is missing") 78 | #Raw Reports 79 | self.assertIsInstance(response.report, dict, "The raw report hasn't been populated") 80 | #Check Metrics 81 | self.assertIsInstance(response.metrics, list, "The metrics weren't populated") 82 | self.assertEqual(response.metrics[0].id,"pageviews", "Wrong Metric") 83 | #Check Elements 84 | self.assertIsInstance(response.elements, list, "The elements is the wrong type") 85 | self.assertEqual(response.elements[0].id,"datetime", "There are elements when there shouldn't be") 86 | 87 | #check time range 88 | checkdate = date(2016,9,4).strftime("%a. %e %h. %Y") 89 | self.assertEqual(response.period, checkdate) 90 | 91 | #check segmetns 92 | self.assertIsNone(response.segments) 93 | 94 | #Check Data 95 | self.assertIsInstance(response.data, list, "Data isn't getting populated right") 96 | self.assertIsInstance(response.data[0] , dict, "The data isn't getting into the dict") 97 | self.assertIsInstance(response.data[0]['datetime'], datetime.datetime, "The date isn't getting populated in the data") 98 | self.assertIsInstance(response.data[0]['pageviews'], int, "The pageviews aren't getting populated in the data") 99 | 100 | @requests_mock.mock() 101 | def test_ranked_report(self, m): 102 | """ Make sure the ranked report is being processed 103 | """ 104 | 105 | path = os.path.dirname(__file__) 106 | 107 | with open(path+'/mock_objects/ranked_report.json') as data_file: 108 | json_response = data_file.read() 109 | 110 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 111 | report_queue = queue_file.read() 112 | 113 | #setup mock object 114 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 115 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 116 | 117 | ranked = self.analytics.suites[test_report_suite].report.element("page").metric("pageviews").metric("visits") 118 | queue = [] 119 | queue.append(ranked) 120 | response = omniture.sync(queue) 121 | 122 | for report in response: 123 | #Check Data 124 | self.assertIsInstance(report.data, list, "Data isn't getting populated right") 125 | self.assertIsInstance(report.data[0] , dict, "The data isn't getting into the dict") 126 | self.assertIsInstance(report.data[0]['page'], str, "The page isn't getting populated in the data") 127 | self.assertIsInstance(report.data[0]['pageviews'], int, "The pageviews aren't getting populated in the data") 128 | self.assertIsInstance(report.data[0]['visits'], int, "The visits aren't getting populated in the data") 129 | 130 | @requests_mock.mock() 131 | def test_ranked_inf_report(self, m): 132 | """ Make sure the ranked report is being processed 133 | """ 134 | 135 | path = os.path.dirname(__file__) 136 | 137 | with open(path+'/mock_objects/ranked_report_inf.json') as data_file: 138 | json_response = data_file.read() 139 | 140 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 141 | report_queue = queue_file.read() 142 | 143 | #setup mock object 144 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 145 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 146 | 147 | ranked = self.analytics.suites[test_report_suite].report.element("page").metric("pageviews").metric("visits") 148 | queue = [] 149 | queue.append(ranked) 150 | response = omniture.sync(queue) 151 | 152 | for report in response: 153 | #Check Data 154 | 155 | self.assertEqual(report.data[-1]["visits"], 0) 156 | self.assertEqual(report.data[-1]["pageviews"], 0) 157 | 158 | 159 | 160 | @requests_mock.mock() 161 | def test_trended_report(self,m): 162 | """Make sure the trended reports are being processed corretly""" 163 | 164 | path = os.path.dirname(__file__) 165 | 166 | with open(path+'/mock_objects/trended_report.json') as data_file: 167 | json_response = data_file.read() 168 | 169 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 170 | report_queue = queue_file.read() 171 | 172 | #setup mock object 173 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 174 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 175 | 176 | 177 | trended = self.analytics.suites[test_report_suite].report.element("page").metric("pageviews").granularity('hour').run() 178 | self.assertIsInstance(trended.data, list, "Treneded Reports don't work") 179 | self.assertIsInstance(trended.data[0] , dict, "The data isn't getting into the dict") 180 | self.assertIsInstance(trended.data[0]['datetime'], datetime.datetime, "The date isn't getting propulated correctly") 181 | self.assertIsInstance(trended.data[0]['page'], str, "The page isn't getting populated in the data") 182 | self.assertIsInstance(trended.data[0]['pageviews'], int, "The pageviews aren't getting populated in the data") 183 | 184 | @requests_mock.mock() 185 | def test_dataframe(self,m): 186 | """Make sure the pandas data frame object can be generated""" 187 | 188 | 189 | path = os.path.dirname(__file__) 190 | 191 | with open(path+'/mock_objects/trended_report.json') as data_file: 192 | json_response = data_file.read() 193 | 194 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 195 | report_queue = queue_file.read() 196 | 197 | #setup mock object 198 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 199 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 200 | 201 | trended = self.analytics.suites[test_report_suite].report.element("page").metric("pageviews").granularity('hour').run() 202 | self.assertIsInstance(trended.dataframe, pandas.DataFrame, "Data Frame Object doesn't work") 203 | 204 | @requests_mock.mock() 205 | def test_segments_id(self,m): 206 | """ Make sure segments can be added """ 207 | 208 | path = os.path.dirname(__file__) 209 | 210 | with open(path+'/mock_objects/segmented_report.json') as data_file: 211 | json_response = data_file.read() 212 | 213 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 214 | report_queue = queue_file.read() 215 | 216 | #setup mock object 217 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 218 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 219 | 220 | suite = self.analytics.suites[test_report_suite] 221 | report = suite.report.filter(suite.segments[0]).run() 222 | 223 | self.assertEqual(report.segments[0], suite.segments[0], "The segments don't match") 224 | 225 | @unittest.skip("skip inline segments because checked in Query") 226 | def test_inline_segment(self): 227 | """ Make sure inline segments work """ 228 | #pretty poor check but need to make it work with any report suite 229 | report = self.analytics.suites[0].report.element('page').metric('pageviews').metric('visits').filter(element='browser', selected=["::unspecified::"]).run() 230 | self.assertIsInstance(report.data, list, "inline segments don't work") 231 | 232 | @requests_mock.mock() 233 | def test_multiple_classifications(self, m): 234 | """Makes sure the report can parse multiple classifications correctly since they have the same element ID""" 235 | #load sample file 236 | path = os.path.dirname(__file__) 237 | 238 | with open(path+'/mock_objects/multi_classifications.json') as data_file: 239 | json_response = data_file.read() 240 | 241 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 242 | ReportQueue = queue_file.read() 243 | 244 | #setup mock object 245 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 246 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=ReportQueue) 247 | 248 | report = self.analytics.suites[0].report\ 249 | .element('evar2',classification="Classification 1", disable_validation=True)\ 250 | .element('evar2',classification="Classification 2", disable_validation=True)\ 251 | 252 | report = report.run() 253 | 254 | self.assertTrue('evar2 | Classification 1' in report.data[0], "The Value of report.data[0] was:{}".format(report.data[0])) 255 | self.assertTrue('evar2 | Classification 2' in report.data[0], "The Value of report.data[0] was:{}".format(report.data[0])) 256 | 257 | @requests_mock.mock() 258 | def test_mixed_classifications(self, m): 259 | """Makes sure the report can parse reports with classifications and 260 | regular dimensionscorrectly since they have the same element ID""" 261 | #load sample files with responses for mock objects 262 | path = os.path.dirname(__file__) 263 | 264 | with open(path+'/mock_objects/mixed_classifications.json') as data_file: 265 | json_response = data_file.read() 266 | 267 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 268 | ReportQueue = queue_file.read() 269 | 270 | #setup mock object 271 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 272 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=ReportQueue) 273 | 274 | report = self.analytics.suites[0].report\ 275 | .element('evar3',classification="Classification 1", disable_validation=True)\ 276 | .element('evar5', disable_validation=True)\ 277 | 278 | report = report.run() 279 | 280 | self.assertTrue('evar3 | Classification 1' in report.data[0], "The Value of report.data[0] was:{}".format(report.data[0])) 281 | self.assertTrue('evar5' in report.data[0], "The Value of report.data[0] was:{}".format(report.data[0])) 282 | 283 | @requests_mock.mock() 284 | def test_repr_html_(self,m): 285 | """Test the _repr_html_ method used by iPython for notebook display""" 286 | path = os.path.dirname(__file__) 287 | 288 | with open(path+'/mock_objects/trended_report.json') as data_file: 289 | json_response = data_file.read() 290 | 291 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 292 | report_queue = queue_file.read() 293 | 294 | with open(path+'/mock_objects/trended_report.html') as basic_html_file: 295 | basic_html = basic_html_file.read() 296 | 297 | #setup mock object 298 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 299 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 300 | 301 | trended = self.analytics.suites[test_report_suite].report\ 302 | .element("page").metric("pageviews").granularity('hour').run() 303 | 304 | 305 | self.assertEqual(trended._repr_html_(),basic_html) 306 | 307 | @requests_mock.mock() 308 | def test__dir__(self,m): 309 | """Test the __div__ method for tab autocompletion""" 310 | path = os.path.dirname(__file__) 311 | 312 | with open(path+'/mock_objects/basic_report.json') as data_file: 313 | json_response = data_file.read() 314 | 315 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 316 | report_queue = queue_file.read() 317 | 318 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 319 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 320 | 321 | response = self.analytics.suites[test_report_suite].report.run() 322 | self.assertEqual(response.__dir__(), \ 323 | ['data','dataframe', 'metrics','elements', 'segments', 'period', 'type', 'timing'], 324 | "the __dir__ method broke: {}".format(response.__dir__())) 325 | 326 | @requests_mock.mock() 327 | def test__repr__(self,m): 328 | path = os.path.dirname(__file__) 329 | 330 | with open(path+'/mock_objects/basic_report.json') as data_file: 331 | json_response = data_file.read() 332 | 333 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 334 | report_queue = queue_file.read() 335 | 336 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 337 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 338 | 339 | response = self.analytics.suites[test_report_suite].report.run() 340 | test_string = """""" 346 | self.assertEqual(response.__repr__(),test_string) 347 | 348 | 349 | if __name__ == '__main__': 350 | unittest.main() 351 | -------------------------------------------------------------------------------- /build/lib/tests/testUtils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import unittest 4 | import omniture 5 | 6 | 7 | 8 | 9 | class UtilsTest(unittest.TestCase): 10 | def setUp(self): 11 | fakelist = [{"id":"123", "title":"abc"},{"id":"456","title":"abc"}] 12 | 13 | self.alist = omniture.Value.list("segemnts",fakelist,{}) 14 | 15 | 16 | def tearDown(self): 17 | del self.alist 18 | 19 | def test_addressable_list_repr_html_(self): 20 | """Test the _repr_html_ for AddressableList this is used in ipython """ 21 | outlist = '
IDTitle
123abc
456abc
' 22 | self.assertEqual(self.alist._repr_html_(),outlist,\ 23 | "The _repr_html_ isn't working: {}"\ 24 | .format(self.alist._repr_html_())) 25 | 26 | def test_addressable_list_str_(self): 27 | """Test _str_ method """ 28 | outstring = 'ID 123 | Name: abc \nID 456 | Name: abc \n' 29 | self.assertEqual(self.alist.__str__(),outstring,\ 30 | "The __str__ isn't working: {}"\ 31 | .format(self.alist.__str__())) 32 | 33 | def test_addressable_list_get_time(self): 34 | """ Test the custom get item raises a problem when there are duplicate names """ 35 | with self.assertRaises(KeyError): 36 | self.alist['abc'] 37 | 38 | def test_wrap(self): 39 | """Test the wrap method """ 40 | self.assertIsInstance(omniture.utils.wrap("test"),list) 41 | self.assertIsInstance(omniture.utils.wrap(["test"]),list) 42 | self.assertEqual(omniture.utils.wrap("test"),["test"]) 43 | self.assertEqual(omniture.utils.wrap(["test"]),["test"]) 44 | 45 | def test_date(self): 46 | """Test the Date Method""" 47 | test_date = "2016-09-01" 48 | self.assertEqual(omniture.utils.date(None), None) 49 | self.assertEqual(omniture.utils.date(test_date).strftime("%Y-%m-%d"), 50 | test_date) 51 | d = datetime.date(2016,9,1) 52 | self.assertEqual(omniture.utils.date(d).strftime("%Y-%m-%d"), 53 | test_date) 54 | 55 | t = datetime.datetime(2016,9,1) 56 | self.assertEqual(omniture.utils.date(t).strftime("%Y-%m-%d"), 57 | test_date) 58 | 59 | self.assertEqual(omniture.utils.date(u"2016-09-01").strftime("%Y-%m-%d"), 60 | test_date) 61 | with self.assertRaises(ValueError): 62 | omniture.utils.date({}) 63 | 64 | def test_affix(self): 65 | """Test the Affix method to make sure it handles things correctly""" 66 | p = "pre" 67 | s = "suf" 68 | v = "val" 69 | con = "+" 70 | 71 | self.assertEqual(omniture.utils.affix(p,v,connector=con), 72 | con.join([p,v])) 73 | self.assertEqual(omniture.utils.affix(base=v,suffix=s,connector=con), 74 | con.join([v,s])) 75 | self.assertEqual(omniture.utils.affix(p,v,s,connector=con), 76 | con.join([p,v,s])) 77 | self.assertEqual(omniture.utils.affix(base=v,connector=con), 78 | con.join([v])) 79 | 80 | def test_translate(self): 81 | """Test the translate method """ 82 | t = {"product":"cat_collar", "price":100, "location":"no where"} 83 | m = {"product":"Product_Name","price":"Cost","date":"Date"} 84 | s = {"Product_Name":"cat_collar", "Cost":100, "location":"no where"} 85 | self.assertEqual(omniture.utils.translate(t,m),s) 86 | 87 | 88 | -------------------------------------------------------------------------------- /logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "disable_existing_loggers": true, 4 | "formatters": { 5 | "simple": { 6 | 7 | } 8 | }, 9 | 10 | "handlers": { 11 | "console": { 12 | "class": "logging.StreamHandler", 13 | "level": "INFO", 14 | "formatter": "simple", 15 | "stream": "ext://sys.stdout" 16 | }, 17 | 18 | "info_file_handler": { 19 | "class": "logging.handlers.RotatingFileHandler", 20 | "level": "INFO", 21 | "formatter": "simple", 22 | "filename": "info.log", 23 | "maxBytes": 10485760, 24 | "backupCount": 20, 25 | "encoding": "utf8", 26 | "delay":true 27 | }, 28 | 29 | "error_file_handler": { 30 | "class": "logging.handlers.RotatingFileHandler", 31 | "level": "ERROR", 32 | "formatter": "simple", 33 | "filename": "errors.log", 34 | "maxBytes": 10485760, 35 | "backupCount": 20, 36 | "encoding": "utf8", 37 | "delay":true 38 | } 39 | }, 40 | 41 | "loggers": { 42 | "my_module": { 43 | "level": "DEBUG", 44 | "handlers": ["console"], 45 | "propagate": "no" 46 | } 47 | }, 48 | 49 | "root": { 50 | "level": "INFO", 51 | "handlers": ["info_file_handler", "error_file_handler"] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /omniture/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import absolute_import 3 | 4 | import os 5 | import json 6 | import logging.config 7 | import io 8 | 9 | from .account import Account, Suite 10 | from .elements import Value 11 | from .query import Query, ReportNotSubmittedError 12 | from .reports import InvalidReportError, Report, DataWarehouseReport 13 | from .version import __version__ 14 | from .utils import AddressableList, affix 15 | 16 | 17 | def authenticate(username, secret=None, endpoint=Account.DEFAULT_ENDPOINT, prefix='', suffix=''): 18 | """ Authenticate to the Adobe API using WSSE """ 19 | #setup logging 20 | setup_logging() 21 | # if no secret is specified, we will assume that instead 22 | # we have received a dictionary with credentials (such as 23 | # from os.environ) 24 | if not secret: 25 | source = username 26 | key_to_username = affix(prefix, 'OMNITURE_USERNAME', suffix) 27 | key_to_secret = affix(prefix, 'OMNITURE_SECRET', suffix) 28 | username = source[key_to_username] 29 | secret = source[key_to_secret] 30 | 31 | return Account(username, secret, endpoint) 32 | 33 | 34 | def queue(queries): 35 | if isinstance(queries, dict): 36 | queries = queries.values() 37 | 38 | for query in queries: 39 | query.queue() 40 | 41 | 42 | def sync(queries, heartbeat=None, interval=1): 43 | """ 44 | `omniture.sync` will queue a number of reports and then 45 | block until the results are all ready. 46 | 47 | Queueing reports is idempotent, meaning that you can also 48 | use `omniture.sync` to fetch the results for queries that 49 | have already been queued: 50 | 51 | query = mysuite.report.range('2013-06-06').over_time('pageviews', 'page') 52 | omniture.queue(query) 53 | omniture.sync(query) 54 | 55 | The interval will operate under an exponetial decay until it reaches 5 minutes. At which point it will ping every 5 minutes 56 | """ 57 | 58 | queue(queries) 59 | 60 | if isinstance(queries, list): 61 | return [query.sync(heartbeat, interval) for query in queries] 62 | elif isinstance(queries, dict): 63 | return {key: query.sync(heartbeat, interval) for key, query in queries.items()} 64 | else: 65 | message = "Queries should be a list or a dictionary, received: {}".format( 66 | queries.__class__) 67 | raise ValueError(message) 68 | 69 | 70 | def setup_logging(default_path='logging.json', default_level=logging.INFO, env_key='LOG_CFG'): 71 | """Setup logging configuration. """ 72 | path = default_path 73 | value = os.getenv(env_key, None) 74 | if value: 75 | path = value 76 | if os.path.exists(path): 77 | with io.open(path, 'rt') as f: 78 | config = json.load(f) 79 | f.close() 80 | logging.config.dictConfig(config) 81 | requests_log = logging.getLogger("requests") 82 | requests_log.setLevel(logging.WARNING) 83 | 84 | else: 85 | logging.basicConfig(level=default_level) 86 | -------------------------------------------------------------------------------- /omniture/account.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import print_function 3 | from future.utils import python_2_unicode_compatible 4 | 5 | import requests 6 | import binascii 7 | import json 8 | from datetime import datetime, date 9 | import logging 10 | import uuid 11 | import hashlib 12 | import base64 13 | import os 14 | from datetime import datetime 15 | 16 | from .elements import Value 17 | from .query import Query 18 | from . import reports 19 | from . import utils 20 | 21 | @python_2_unicode_compatible 22 | class Account(object): 23 | """ A wrapper for the Adobe Analytics API. Allows you to query the reporting API """ 24 | DEFAULT_ENDPOINT = 'https://api.omniture.com/admin/1.4/rest/' 25 | 26 | def __init__(self, username, secret, endpoint=DEFAULT_ENDPOINT, cache=False, cache_key=None): 27 | """Authentication to make requests.""" 28 | self.log = logging.getLogger(__name__) 29 | self.log.info(datetime.now().strftime("%Y-%m-%d %I%p:%M:%S")) 30 | self.username = username 31 | self.secret = secret 32 | self.endpoint = endpoint 33 | #Allow someone to set a custom cache key 34 | self.cache = cache 35 | if cache_key: 36 | self.cache_key = cache_key 37 | else: 38 | self.cache_key = date.today().isoformat() 39 | if self.cache: 40 | data = self.request_cached('Company', 'GetReportSuites')['report_suites'] 41 | else: 42 | data = self.request('Company', 'GetReportSuites')['report_suites'] 43 | suites = [Suite(suite['site_title'], suite['rsid'], self) for suite in data] 44 | self.suites = utils.AddressableList(suites) 45 | 46 | def request_cached(self, api, method, query={}, cache_key=None): 47 | if cache_key: 48 | key = cache_key 49 | else: 50 | key = self.cache_key 51 | 52 | #Generate a shortened hash of the query string so that method don't collide 53 | query_hash = base64.urlsafe_b64encode(hashlib.md5(query).digest()) 54 | 55 | try: 56 | with open(self.file_path+'/data_'+api+'_'+method+'_'+query_hash+'_'+key+'.txt') as fp: 57 | for line in fp: 58 | if line: 59 | data = ast.literal_eval(line) 60 | 61 | except IOError as e: 62 | data = self.request(api, method, query) 63 | 64 | # Capture all other old text files 65 | #TODO decide if the query should be included in the file list to be cleared out when the cache key changes 66 | filelist = [f for f in os.listdir(self.file_path) if f.startswith('data_'+api+'_'+method)] 67 | 68 | # Delete them 69 | for f in filelist: 70 | os.remove(self.file_path+'/'+f) 71 | 72 | # Build the new data 73 | the_file = open(self.file_path+'/data_'+api+'_'+method+'_'+query_hash+'_'+key+'.txt', 'w') 74 | the_file.write(str(data)) 75 | the_file.close() 76 | 77 | 78 | def request(self, api, method, query={}): 79 | """ 80 | Make a request to the Adobe APIs. 81 | 82 | * api -- the class of APIs you would like to call (e.g. Report, 83 | ReportSuite, Company, etc.) 84 | * method -- the method you would like to call inside that class 85 | of api 86 | * query -- a python object representing the parameters you would 87 | like to pass to the API 88 | """ 89 | self.log.info("Request: %s.%s Parameters: %s", api, method, query) 90 | response = requests.post( 91 | self.endpoint, 92 | params={'method': api + '.' + method}, 93 | data=json.dumps(query), 94 | headers=self._build_token() 95 | ) 96 | self.log.debug("Response for %s.%s:%s", api, method, response.text) 97 | json_response = response.json() 98 | 99 | if type(json_response) == dict: 100 | self.log.debug("Error Code %s", json_response.get('error')) 101 | if json_response.get('error') == 'report_not_ready': 102 | raise reports.ReportNotReadyError(json_response) 103 | elif json_response.get('error') != None: 104 | raise reports.InvalidReportError(json_response) 105 | else: 106 | return json_response 107 | else: 108 | return json_response 109 | 110 | def jsonReport(self, reportJSON): 111 | """Generates a Report from the JSON (including selecting the report suite)""" 112 | if type(reportJSON) == str: 113 | reportJSON = json.loads(reportJSON) 114 | suiteID = reportJSON['reportDescription']['reportSuiteID'] 115 | suite = self.suites[suiteID] 116 | return suite.jsonReport(reportJSON) 117 | 118 | 119 | def _serialize_header(self, properties): 120 | header = [] 121 | for key, value in properties.items(): 122 | header.append('{key}="{value}"'.format(key=key, value=value)) 123 | return ', '.join(header) 124 | 125 | def _build_token(self): 126 | nonce = str(uuid.uuid4()) 127 | base64nonce = binascii.b2a_base64(binascii.a2b_qp(nonce)) 128 | created_date = datetime.utcnow().isoformat() + 'Z' 129 | sha = nonce + created_date + self.secret 130 | sha_object = hashlib.sha1(sha.encode()) 131 | password_64 = binascii.b2a_base64(sha_object.digest()) 132 | 133 | properties = { 134 | "Username": self.username, 135 | "PasswordDigest": password_64.decode().strip(), 136 | "Nonce": base64nonce.decode().strip(), 137 | "Created": created_date, 138 | } 139 | header = 'UsernameToken ' + self._serialize_header(properties) 140 | 141 | return {'X-WSSE': header} 142 | 143 | def _repr_html_(self): 144 | """ Format in HTML for iPython Users """ 145 | html = "" 146 | html += "{0}: {1}
".format("Username", self.username) 147 | html += "{0}: {1}
".format("Secret", "***************") 148 | html += "{0}: {1}
".format("Report Suites", len(self.suites)) 149 | html += "{0}: {1}
".format("Endpoint", self.endpoint) 150 | return html 151 | 152 | def __str__(self): 153 | return "Analytics Account -------------\n Username: \ 154 | {0} \n Report Suites: {1} \n Endpoint: {2}" \ 155 | .format(self.username, len(self.suites), self.endpoint) 156 | 157 | 158 | class Suite(Value): 159 | """Lets you query a specific report suite. """ 160 | def request(self, api, method, query={}): 161 | raw_query = {} 162 | raw_query.update(query) 163 | if method == 'GetMetrics' or method == 'GetElements': 164 | raw_query['reportSuiteID'] = self.id 165 | 166 | return self.account.request(api, method, raw_query) 167 | 168 | def __init__(self, title, id, account, cache=False): 169 | self.log = logging.getLogger(__name__) 170 | super(Suite, self).__init__(title, id, account) 171 | self.account = account 172 | 173 | @property 174 | @utils.memoize 175 | def metrics(self): 176 | """ Return the list of valid metricsfor the current report suite""" 177 | if self.account.cache: 178 | data = self.request_cache('Report', 'GetMetrics') 179 | else: 180 | data = self.request('Report', 'GetMetrics') 181 | return Value.list('metrics', data, self, 'name', 'id') 182 | 183 | @property 184 | @utils.memoize 185 | def elements(self): 186 | """ Return the list of valid elementsfor the current report suite """ 187 | if self.account.cache: 188 | data = self.request_cached('Report', 'GetElements') 189 | else: 190 | data = self.request('Report', 'GetElements') 191 | return Value.list('elements', data, self, 'name', 'id') 192 | 193 | @property 194 | @utils.memoize 195 | def segments(self): 196 | """ Return the list of valid segments for the current report suite """ 197 | try: 198 | if self.account.cache: 199 | data = self.request_cached('Segments', 'Get',{"accessLevel":"shared"}) 200 | else: 201 | data = self.request('Segments', 'Get',{"accessLevel":"shared"}) 202 | return Value.list('segments', data, self, 'name', 'id',) 203 | except reports.InvalidReportError: 204 | data = [] 205 | return Value.list('segments', data, self, 'name', 'id',) 206 | 207 | @property 208 | def report(self): 209 | """ Return a report to be run on this report suite """ 210 | return Query(self) 211 | 212 | def jsonReport(self,reportJSON): 213 | """Creates a report from JSON. Accepts either JSON or a string. Useful for deserializing requests""" 214 | q = Query(self) 215 | #TODO: Add a method to the Account Object to populate the report suite this call will ignore it on purpose 216 | if type(reportJSON) == str: 217 | reportJSON = json.loads(reportJSON) 218 | 219 | reportJSON = reportJSON['reportDescription'] 220 | 221 | if 'dateFrom' in reportJSON and 'dateTo' in reportJSON: 222 | q = q.range(reportJSON['dateFrom'],reportJSON['dateTo']) 223 | elif 'dateFrom' in reportJSON: 224 | q = q.range(reportJSON['dateFrom']) 225 | elif 'date' in reportJSON: 226 | q = q.range(reportJSON['date']) 227 | else: 228 | q = q 229 | 230 | if 'dateGranularity' in reportJSON: 231 | q = q.granularity(reportJSON['dateGranularity']) 232 | 233 | if 'source' in reportJSON: 234 | q = q.set('source',reportJSON['source']) 235 | 236 | if 'metrics' in reportJSON: 237 | for m in reportJSON['metrics']: 238 | q = q.metric(m['id']) 239 | 240 | if 'elements' in reportJSON: 241 | for e in reportJSON['elements']: 242 | id = e['id'] 243 | del e['id'] 244 | q= q.element(id, **e) 245 | 246 | if 'locale' in reportJSON: 247 | q = q.set('locale',reportJSON['locale']) 248 | 249 | if 'sortMethod' in reportJSON: 250 | q = q.set('sortMethod',reportJSON['sortMethod']) 251 | 252 | if 'sortBy' in reportJSON: 253 | q = q.sortBy(reportJSON['sortBy']) 254 | 255 | #WARNING This doesn't carry over segment IDs meaning you can't manipulate the segments in the new object 256 | #TODO Loop through and add segment ID with filter method (need to figure out how to handle combined) 257 | if 'segments' in reportJSON: 258 | q = q.set('segments', reportJSON['segments']) 259 | 260 | if 'anomalyDetection' in reportJSON: 261 | q = q.set('anomalyDetection',reportJSON['anomalyDetection']) 262 | 263 | if 'currentData' in reportJSON: 264 | q = q.set('currentData',reportJSON['currentData']) 265 | 266 | if 'elementDataEncoding' in reportJSON: 267 | q = q.set('elementDataEncoding',reportJSON['elementDataEncoding']) 268 | return q 269 | 270 | def _repr_html_(self): 271 | """ Format in HTML for iPython Users """ 272 | return "{0}{1}".format(self.id, self.title) 273 | 274 | def __str__(self): 275 | return "ID {0:25} | Name: {1} \n".format(self.id, self.title) 276 | -------------------------------------------------------------------------------- /omniture/elements.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | from future.utils import python_2_unicode_compatible 5 | 6 | import copy 7 | import logging 8 | 9 | from .import utils 10 | 11 | @python_2_unicode_compatible 12 | class Value(object): 13 | """ Searchable Dict. Can search on both the key and the value """ 14 | def __init__(self, title, id, parent, extra={}): 15 | self.log = logging.getLogger(__name__) 16 | self.title = str(title) 17 | self.id = id 18 | self.parent = parent 19 | self.properties = {'id': id} 20 | 21 | for k, v in extra.items(): 22 | setattr(self, k, v) 23 | 24 | @classmethod 25 | def list(cls, name, items, parent, title='title', id='id'): 26 | values = [cls(item[title], str(item[id]), parent, item) for item in items] 27 | return utils.AddressableList(values, name) 28 | 29 | def __repr__(self): 30 | print(self) 31 | return "<{title}: {id} in {parent}>".format(**self.__dict__) 32 | 33 | def copy(self): 34 | value = self.__class__(self.title, self.id, self.parent) 35 | value.properties = copy.copy(self.properties) 36 | return value 37 | 38 | def serialize(self): 39 | return self.properties 40 | 41 | def _repr_html_(self): 42 | """ Format in HTML for iPython Users """ 43 | return "{0}{1}".format(self.id, self.title) 44 | 45 | 46 | def __str__(self): 47 | """ allows users to print this out in a user friendly using print 48 | """ 49 | return "ID {0:25} | Name: {1} \n".format(self.id, self.title) 50 | -------------------------------------------------------------------------------- /omniture/query.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | from future.utils import python_2_unicode_compatible 5 | 6 | import time 7 | from copy import copy, deepcopy 8 | import functools 9 | from dateutil.relativedelta import relativedelta 10 | import json 11 | import logging 12 | import sys 13 | 14 | from .elements import Value 15 | from . import reports 16 | from . import utils 17 | 18 | 19 | def immutable(method): 20 | @functools.wraps(method) 21 | def wrapped_method(self, *vargs, **kwargs): 22 | obj = self.clone() 23 | method(obj, *vargs, **kwargs) 24 | return obj 25 | 26 | return wrapped_method 27 | 28 | class ReportNotSubmittedError(Exception): 29 | """ Exception that is raised when a is requested by hasn't been submitted 30 | to Adobe 31 | """ 32 | def __init__(self,error): 33 | self.log = logging.getLogger(__name__) 34 | self.log.debug("Report Has not been submitted, call asynch() or run()") 35 | super(ReportNotSubmittedError, self).__init__("Report Not Submitted") 36 | 37 | @python_2_unicode_compatible 38 | class Query(object): 39 | """ Lets you build a query to the Reporting API for Adobe Analytics. 40 | 41 | Methods in this object are chainable. For example 42 | >>> report = report.element("page").element("prop1"). 43 | metric("pageviews").granularity("day").run() 44 | Making it easy to create a report. 45 | 46 | To see the raw definition use 47 | >>> print report 48 | """ 49 | 50 | GRANULARITY_LEVELS = ['hour', 'day', 'week', 'month', 'quarter', 'year'] 51 | STATUSES = ["Not Submitted","Not Ready","Done"] 52 | 53 | def __init__(self, suite): 54 | """ Setup the basic structure of the report query. """ 55 | self.log = logging.getLogger(__name__) 56 | self.suite = suite 57 | self.raw = {} 58 | #Put the report suite in so the user can print 59 | #the raw query and have it work as is 60 | self.raw['reportSuiteID'] = str(self.suite.id) 61 | self.id = None 62 | self.method = "Get" 63 | self.status = self.STATUSES[0] 64 | #The report object 65 | self.report = reports.Report 66 | #The fully hydrated report object 67 | self.processed_response = None 68 | self.unprocessed_response = None 69 | 70 | def _normalize_value(self, value, category): 71 | if isinstance(value, Value): 72 | return value 73 | else: 74 | return getattr(self.suite, category)[value] 75 | 76 | def _serialize_value(self, value, category): 77 | return self._normalize_value(value, category).serialize() 78 | 79 | def _serialize_values(self, values, category): 80 | if not isinstance(values, list): 81 | values = [values] 82 | 83 | return [self._serialize_value(value, category) for value in values] 84 | 85 | def _serialize(self, obj): 86 | if isinstance(obj, list): 87 | return [self._serialize(el) for el in obj] 88 | elif isinstance(obj, Value): 89 | return obj.serialize() 90 | else: 91 | return obj 92 | 93 | def clone(self): 94 | """ Return a copy of the current object. """ 95 | query = Query(self.suite) 96 | query.raw = copy(self.raw) 97 | query.report = self.report 98 | query.status = self.status 99 | query.processed_response = self.processed_response 100 | query.unprocessed_response = self.unprocessed_response 101 | return query 102 | 103 | @immutable 104 | def range(self, start, stop=None, months=0, days=0, granularity=None): 105 | """ 106 | Define a date range for the report. 107 | 108 | * start -- The start date of the report. If stop is not present 109 | it is assumed to be the to and from dates. 110 | * stop (optional) -- the end date of the report (inclusive). 111 | * months (optional, named) -- months to run used for relative dates 112 | * days (optional, named)-- days to run used for relative dates) 113 | * granulartiy (optional, named) -- set the granularity for the report 114 | """ 115 | start = utils.date(start) 116 | stop = utils.date(stop) 117 | 118 | if days or months: 119 | stop = start + relativedelta(days=days-1, months=months) 120 | else: 121 | stop = stop or start 122 | 123 | if start == stop: 124 | self.raw['date'] = start.isoformat() 125 | else: 126 | self.raw.update({ 127 | 'dateFrom': start.isoformat(), 128 | 'dateTo': stop.isoformat(), 129 | }) 130 | 131 | if granularity: 132 | self.raw = self.granularity(granularity).raw 133 | 134 | return self 135 | 136 | @immutable 137 | def granularity(self, granularity): 138 | """ 139 | Set the granulartiy for the report. 140 | 141 | Values are one of the following 142 | 'hour', 'day', 'week', 'month', 'quarter', 'year' 143 | """ 144 | if granularity not in self.GRANULARITY_LEVELS: 145 | levels = ", ".join(self.GRANULARITY_LEVELS) 146 | raise ValueError("Granularity should be one of: " + levels) 147 | 148 | self.raw['dateGranularity'] = granularity 149 | 150 | return self 151 | 152 | @immutable 153 | def set(self, key=None, value=None, **kwargs): 154 | """ 155 | Set a custom property in the report 156 | 157 | `set` is a way to add raw properties to the request, 158 | for features that python-omniture does not support but the 159 | SiteCatalyst API does support. For convenience's sake, 160 | it will serialize Value and Element objects but will 161 | leave any other kind of value alone. 162 | """ 163 | 164 | if key and value: 165 | self.raw[key] = self._serialize(value) 166 | elif key or kwargs: 167 | properties = key or kwargs 168 | for key, value in properties.items(): 169 | self.raw[key] = self._serialize(value) 170 | else: 171 | raise ValueError("Query#set requires a key and value, \ 172 | a properties dictionary or keyword arguments.") 173 | 174 | return self 175 | 176 | @immutable 177 | def filter(self, segment=None, segments=None, disable_validation=False, **kwargs): 178 | """ Set Add a segment to the report. """ 179 | # It would appear to me that 'segment_id' has a strict subset 180 | # of the functionality of 'segments', but until I find out for 181 | # sure, I'll provide both options. 182 | if 'segments' not in self.raw: 183 | self.raw['segments'] = [] 184 | 185 | if disable_validation == False: 186 | if segments: 187 | self.raw['segments'].extend(self._serialize_values(segments, 'segments')) 188 | elif segment: 189 | self.raw['segments'].append({"id":self._normalize_value(segment, 190 | 'segments').id}) 191 | elif kwargs: 192 | self.raw['segments'].append(kwargs) 193 | else: 194 | raise ValueError() 195 | 196 | else: 197 | if segments: 198 | self.raw['segments'].extend([{"id":segment} for segment in segments]) 199 | elif segment: 200 | self.raw['segments'].append({"id":segment}) 201 | elif kwargs: 202 | self.raw['segments'].append(kwargs) 203 | else: 204 | raise ValueError() 205 | return self 206 | 207 | @immutable 208 | def element(self, element, disable_validation=False, **kwargs): 209 | """ 210 | Add an element to the report. 211 | 212 | This method is intended to be called multiple time. Each time it will 213 | add an element as a breakdown 214 | After the first element, each additional element is considered 215 | a breakdown 216 | """ 217 | 218 | if self.raw.get('elements', None) == None: 219 | self.raw['elements'] = [] 220 | 221 | if disable_validation == False: 222 | element = self._serialize_value(element, 'elements') 223 | else: 224 | element = {"id":element} 225 | 226 | if kwargs != None: 227 | element.update(kwargs) 228 | self.raw['elements'].append(deepcopy(element)) 229 | 230 | #TODO allow this method to accept a list 231 | return self 232 | 233 | 234 | def breakdown(self, element, **kwargs): 235 | """ Pass through for element. Adds an element to the report. """ 236 | return self.element(element, **kwargs) 237 | 238 | 239 | def elements(self, *args, **kwargs): 240 | """ Shortcut for adding multiple elements. Doesn't support arguments """ 241 | obj = self 242 | for e in args: 243 | obj = obj.element(e, **kwargs) 244 | 245 | return obj 246 | 247 | @immutable 248 | def metric(self, metric, disable_validation=False): 249 | """ 250 | Add an metric to the report. 251 | 252 | This method is intended to be called multiple time. 253 | Each time a metric will be added to the report 254 | """ 255 | if self.raw.get('metrics', None) == None: 256 | self.raw['metrics'] = [] 257 | 258 | # If the metric provided is a list, return after list is read 259 | if isinstance(metric, list): 260 | 261 | for m in metric: 262 | 263 | if disable_validation == False: 264 | self.raw['metrics'].append(self._serialize_value(m, 'metrics')) 265 | else: 266 | self.raw['metrics'].append({"id":m}) 267 | 268 | 269 | return self 270 | 271 | # Process single metric 272 | if disable_validation == False: 273 | self.raw['metrics'].append(self._serialize_value(metric, 'metrics')) 274 | else: 275 | self.raw['metrics'].append({"id":metric}) 276 | 277 | #self.raw['metrics'] = self._serialize_values(metric, 'metrics') 278 | #TODO allow this metric to accept a list 279 | return self 280 | 281 | def metrics(self, *args, **kwargs): 282 | """ Shortcut for adding multiple metrics """ 283 | obj = self 284 | for m in args: 285 | obj = obj.metric(m, **kwargs) 286 | 287 | return obj 288 | 289 | @immutable 290 | def sortBy(self, metric): 291 | """ Specify the sortBy Metric """ 292 | self.raw['sortBy'] = metric 293 | return self 294 | 295 | @immutable 296 | def currentData(self): 297 | """ Set the currentData flag """ 298 | self.raw['currentData'] = True 299 | return self 300 | 301 | 302 | def build(self): 303 | """ Return the report descriptoin as an object """ 304 | return {'reportDescription': self.raw} 305 | 306 | def queue(self): 307 | """ Submits the report to the Queue on the Adobe side. """ 308 | q = self.build() 309 | self.log.debug("Suite Object: %s Method: %s, Query %s", 310 | self.suite, self.report.method, q) 311 | self.id = self.suite.request('Report', 312 | self.report.method, 313 | q)['reportID'] 314 | self.status = self.STATUSES[1] 315 | return self 316 | 317 | def probe(self, heartbeat=None, interval=1, soak=False): 318 | """ Keep checking until the report is done""" 319 | #Loop until the report is done 320 | while self.is_ready() == False: 321 | if heartbeat: 322 | heartbeat() 323 | time.sleep(interval) 324 | #Use a back off up to 30 seconds to play nice with the APIs 325 | if interval < 1: 326 | interval = 1 327 | elif interval < 30: 328 | interval = round(interval * 1.5) 329 | else: 330 | interval = 30 331 | self.log.debug("Check Interval: %s seconds", interval) 332 | 333 | def is_ready(self): 334 | """ inspects the response to see if the report is ready """ 335 | if self.status == self.STATUSES[0]: 336 | raise ReportNotSubmittedError('{"message":"Doh! the report needs to be submitted first"}') 337 | elif self.status == self.STATUSES[1]: 338 | try: 339 | # the request method catches the report and populates it automatically 340 | response = self.suite.request('Report','Get',{'reportID': self.id}) 341 | self.status = self.STATUSES[2] 342 | self.unprocessed_response = response 343 | self.processed_response = self.report(response, self) 344 | return True 345 | except reports.ReportNotReadyError: 346 | self.status = self.STATUSES[1] 347 | #raise reports.InvalidReportError(response) 348 | return False 349 | elif self.status == self.STATUSES[2]: 350 | return True 351 | 352 | 353 | def sync(self, heartbeat=None, interval=0.01): 354 | """ Run the report synchronously,""" 355 | if self.status == self.STATUSES[0]: 356 | self.queue() 357 | self.probe(heartbeat, interval) 358 | if self.status == self.STATUSES[1]: 359 | self.probe() 360 | return self.processed_response 361 | 362 | def asynch(self, callback=None, heartbeat=None, interval=1): 363 | """ Run the Report Asynchrnously """ 364 | if self.status == self.STATUSES[0]: 365 | self.queue() 366 | return self 367 | 368 | def get_report(self): 369 | self.is_ready() 370 | if self.status == self.STATUSES[2]: 371 | return self.processed_response 372 | else: 373 | raise reports.ReportNotReadyError('{"message":"Doh! the report is not ready yet"}') 374 | 375 | def run(self, defaultheartbeat=True, heartbeat=None, interval=0.01): 376 | """Shortcut for sync(). Runs the current report synchronously. """ 377 | if defaultheartbeat == True: 378 | rheartbeat = self.heartbeat 379 | else: 380 | rheartbeat = heartbeat 381 | 382 | return self.sync(rheartbeat, interval) 383 | 384 | def heartbeat(self): 385 | """ A default heartbeat method that prints a dot for each request """ 386 | sys.stdout.write('.') 387 | sys.stdout.flush() 388 | 389 | 390 | def check(self): 391 | """ 392 | Basically an alias to is ready to make the interface a bit better 393 | """ 394 | return self.is_ready() 395 | 396 | def cancel(self): 397 | """ Cancels a the report from the Queue on the Adobe side. """ 398 | return self.suite.request('Report', 399 | 'CancelReport', 400 | {'reportID': self.id}) 401 | def json(self): 402 | """ Return a JSON string of the Request """ 403 | return str(json.dumps(self.build(), indent=4, separators=(',', ': '), sort_keys=True)) 404 | 405 | def __str__(self): 406 | return self.json() 407 | 408 | def _repr_html_(self): 409 | """ Format in HTML for iPython Users """ 410 | report = { str(key):value for key,value in self.raw.items() } 411 | html = "Current Report Settings
" 412 | for k,v in sorted(list(report.items())): 413 | html += "{0}: {1}
".format(k,v) 414 | if self.id: 415 | html += "This report has been submitted
" 416 | html += "{0}: {1}
".format("ReportId", self.id) 417 | return html 418 | 419 | 420 | def __dir__(self): 421 | """ Give sensible options for Tab Completion mostly for iPython """ 422 | return ['asynch','breakdown','cancel','clone','currentData', 'element', 423 | 'filter', 'granularity', 'id','json' ,'metric', 'queue', 'range', 'raw', 'report', 424 | 'request', 'run', 'set', 'sortBy', 'suite'] -------------------------------------------------------------------------------- /omniture/reports.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import absolute_import 3 | from __future__ import print_function 4 | from future.utils import python_2_unicode_compatible 5 | 6 | import logging 7 | from datetime import datetime 8 | import json 9 | 10 | from .elements import Value 11 | 12 | import warnings 13 | 14 | class InvalidReportError(Exception): 15 | """ 16 | Exception raised when the API says a report defintion is 17 | invalid 18 | """ 19 | def normalize(self, error): 20 | print ('error', error) 21 | return { 22 | 'error': error.get('error'), 23 | 'error_description': error.get('error_description'), 24 | 'error_uri': error.get('error_uri', ''), 25 | } 26 | 27 | def __init__(self, error): 28 | self.log = logging.getLogger(__name__) 29 | error = self.normalize(error) 30 | self.message = "{error}: {error_description} ({error_uri})".format(**error) 31 | super(InvalidReportError, self).__init__(self.message) 32 | 33 | class ReportNotReadyError(Exception): 34 | """ Exception that is raised when a report is not ready to be downloaded 35 | """ 36 | def __init__(self,error): 37 | self.log = logging.getLogger(__name__) 38 | self.log.debug("Report Not Ready") 39 | super(ReportNotReadyError, self).__init__("Report Not Ready") 40 | 41 | 42 | # TODO: also make this iterable (go through rows) 43 | @python_2_unicode_compatible 44 | class Report(object): 45 | """ 46 | Object to parse the responses of the report 47 | 48 | To get the data use 49 | >>> report.data 50 | 51 | To get a Pandas DataFrame use 52 | >>> report.dataframe 53 | 54 | To get the raw response use 55 | >>> print report 56 | 57 | """ 58 | def process(self): 59 | """ Parse out the relevant data from the report and store it for easy access 60 | Should only be used internally to the class 61 | """ 62 | self.timing = { 63 | 'queue': float(self.raw['waitSeconds']), 64 | 'execution': float(self.raw['runSeconds']), 65 | } 66 | self.log.debug("Report Wait Time: %s, Report Execution Time: %s", self.timing['queue'], self.timing['execution']) 67 | self.report = report = self.raw['report'] 68 | self.metrics = Value.list('metrics', report['metrics'], self.suite, 'name', 'id') 69 | self.elements = Value.list('elements', report['elements'], self.suite, 'name', 'id') 70 | self.period = str(report['period']) 71 | self.type = str(report['type']) 72 | 73 | segments = report.get('segments') 74 | if segments: 75 | self.segments = [] 76 | for s in segments: 77 | try: 78 | self.segments.append(self.query.suite.segments[s['id']]) 79 | except KeyError as e: 80 | warnings.warn(repr(e)) 81 | self.segments.append(s['id']) 82 | else: 83 | self.segments = None 84 | 85 | #Set as none until it is actually used 86 | self.dict_data = None 87 | self.pandas_data = None 88 | 89 | @property 90 | def data(self): 91 | """ Returns the report data as a set of dicts for easy quering 92 | It generates the dicts on the 1st call then simply returns the reference to the data in subsequent calls 93 | """ 94 | #If the data hasn't been generate it generate the data 95 | if self.dict_data == None: 96 | self.dict_data = self.parse_rows(self.report['data']) 97 | 98 | return self.dict_data 99 | 100 | def parse_rows(self,row, level=0, upperlevels=None): 101 | """ 102 | Parse through the data returned by a repor. Return a list of dicts. 103 | 104 | This method is recursive. 105 | """ 106 | #self.log.debug("Level %s, Upperlevels %s, Row Type %s, Row: %s", level,upperlevels, type(row), row) 107 | data = {} 108 | data_set = [] 109 | 110 | #merge in the upper levels 111 | if upperlevels != None: 112 | data.update(upperlevels) 113 | 114 | 115 | if type(row) == list: 116 | for r in row: 117 | #on the first call set add to the empty list 118 | pr = self.parse_rows(r,level, data.copy()) 119 | if type(pr) == dict: 120 | data_set.append(pr) 121 | #otherwise add to the existing list 122 | else: 123 | data_set.extend(pr) 124 | 125 | #pull out the metrics from the lowest level 126 | if type(row) == dict: 127 | #pull out any relevant data from the current record 128 | #Handle datetime isn't in the elements list for trended reports 129 | if level == 0 and self.type == "trended": 130 | element = "datetime" 131 | elif self.type == "trended": 132 | if hasattr(self.elements[level-1], 'classification'): 133 | #handle the case where there are multiple classifications 134 | element = str(self.elements[level-1].id) + ' | ' + str(self.elements[level-1].classification) 135 | else: 136 | element = str(self.elements[level-1].id) 137 | else: 138 | if hasattr(self.elements[level], 'classification'): 139 | #handle the case where there are multiple classifications 140 | element = str(self.elements[level].id) + ' | ' + str(self.elements[level].classification) 141 | else: 142 | element = str(self.elements[level].id) 143 | 144 | 145 | if element == "datetime": 146 | data[element] = datetime(int(row.get('year',0)),int(row.get('month',0)),int(row.get('day',0)),int(row.get('hour',0))) 147 | data["datetime_friendly"] = str(row['name']) 148 | else: 149 | try: 150 | data[element] = str(row['name']) 151 | 152 | # If the name value is Null or non-encodable value, return null 153 | except: 154 | data[element] = "null" 155 | #parse out any breakdowns and add to the data set 156 | if 'breakdown' in row and len(row['breakdown']) > 0: 157 | data_set.extend(self.parse_rows(row['breakdown'], level+1, data)) 158 | elif 'counts' in row: 159 | for index, metric in enumerate(row['counts']): 160 | #decide what type of event 161 | if metric == 'INF': 162 | data[str(self.metrics[index].id)] = 0 163 | elif self.metrics[index].decimals > 0 or metric.find('.') >-1: 164 | data[str(self.metrics[index].id)] = float(metric) 165 | else: 166 | data[str(self.metrics[index].id)] = int(metric) 167 | 168 | 169 | 170 | if len(data_set)>0: 171 | return data_set 172 | else: 173 | return data 174 | 175 | @property 176 | def dataframe(self): 177 | """ 178 | Returns pandas DataFrame for additional analysis. 179 | 180 | Will generate the data the first time it is called otherwise passes a cached version 181 | """ 182 | 183 | if self.pandas_data is None: 184 | self.pandas_data = self.to_dataframe() 185 | 186 | return self.pandas_data 187 | 188 | 189 | def to_dataframe(self): 190 | import pandas as pd 191 | return pd.DataFrame.from_dict(self.data) 192 | 193 | def __init__(self, raw, query): 194 | self.log = logging.getLogger(__name__) 195 | self.raw = raw 196 | self.query = query 197 | self.suite = query.suite 198 | self.process() 199 | 200 | def __repr__(self): 201 | info = { 202 | 'metrics': ", ".join(map(str, self.metrics)), 203 | 'elements': ", ".join(map(str, self.elements)), 204 | } 205 | return "".format(**info) 206 | 207 | def __dir__(self): 208 | """ Give sensible options for Tab Completion mostly for iPython """ 209 | return ['data','dataframe', 'metrics','elements', 'segments', 'period', 'type', 'timing'] 210 | 211 | def _repr_html_(self): 212 | """ Format in HTML for iPython Users """ 213 | html = "" 214 | for index, item in enumerate(self.data): 215 | html += "" 216 | #populate header Row 217 | if index < 1: 218 | html += "" 219 | if 'datetime' in item: 220 | html += "".format('datetime') 221 | for key in sorted(list(item.keys())): 222 | if key != 'datetime': 223 | html += "".format(key) 224 | html += "" 225 | 226 | #Make sure date time is alway listed first 227 | if 'datetime' in item: 228 | html += "".format(item['datetime']) 229 | for key, value in sorted(list(item.items())): 230 | if key != 'datetime': 231 | html += "".format(value) 232 | html += "" 233 | return html 234 | 235 | def __str__(self): 236 | return json.dumps(self.raw,indent=4, separators=(',', ': '),sort_keys=True) 237 | 238 | Report.method = "Queue" 239 | 240 | 241 | class DataWarehouseReport(object): 242 | pass 243 | 244 | DataWarehouseReport.method = 'Request' 245 | -------------------------------------------------------------------------------- /omniture/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from copy import copy 4 | import datetime 5 | from dateutil.parser import parse as parse_date 6 | import six 7 | 8 | 9 | class memoize: 10 | def __init__(self, function): 11 | self.function = function 12 | self.memoized = {} 13 | 14 | def __call__(self, *args): 15 | try: 16 | return self.memoized[args] 17 | except KeyError: 18 | self.memoized[args] = self.function(*args) 19 | return self.memoized[args] 20 | 21 | 22 | class AddressableList(list): 23 | """ List of items addressable either by id or by name """ 24 | def __init__(self, items, name='items'): 25 | super(AddressableList, self).__init__(items) 26 | self.name = name 27 | 28 | def __getitem__(self, key): 29 | if isinstance(key, int): 30 | return super(AddressableList, self).__getitem__(key) 31 | else: 32 | matches = [item for item in self if item.title == key or item.id == key] 33 | count = len(matches) 34 | if count > 1: 35 | matches = list(map(repr, matches)) 36 | error = "Found multiple matches for {key}: {matches}. ".format( 37 | key=key, matches=", ".join(matches)) 38 | advice = "Use the identifier instead." 39 | raise KeyError(error + advice) 40 | elif count == 1: 41 | return matches[0] 42 | else: 43 | raise KeyError("Cannot find {key} among the available {name}" 44 | .format(key=key, name=self.name)) 45 | 46 | def _repr_html_(self): 47 | """ HTML formating for iPython users """ 48 | html = "
{0}{0}
{0}{0}
" 49 | html += "".format("ID", "Title") 50 | for i in self: 51 | html += "" 52 | html += i._repr_html_() 53 | html += "" 54 | html +="
{0}{1}
" 55 | return html 56 | 57 | def __str__(self): 58 | string = "" 59 | for i in self: 60 | string += i.__str__() 61 | return string 62 | 63 | def __repr__(self): 64 | return "" 65 | 66 | 67 | def date(obj): 68 | #used to ensure compatibility with Python3 without having to user six 69 | try: 70 | basestring 71 | except NameError: 72 | basestring = str 73 | 74 | if obj is None: 75 | return None 76 | elif isinstance(obj, datetime.date): 77 | if hasattr(obj, 'date'): 78 | return obj.date() 79 | else: 80 | return obj 81 | elif isinstance(obj, six.string_types): 82 | return parse_date(obj).date() 83 | elif isinstance(obj, six.text_type): 84 | return parse_date(str(obj)).date() 85 | else: 86 | raise ValueError("Can only convert strings into dates, received {}" 87 | .format(obj.__class__)) 88 | 89 | 90 | def wrap(obj): 91 | if isinstance(obj, list): 92 | return obj 93 | else: 94 | return [obj] 95 | 96 | 97 | def affix(prefix=None, base=None, suffix=None, connector='_'): 98 | if prefix: 99 | prefix = prefix + connector 100 | else: 101 | prefix = '' 102 | 103 | if suffix: 104 | suffix = connector + suffix 105 | else: 106 | suffix = '' 107 | 108 | return prefix + base + suffix 109 | 110 | 111 | def translate(d, mapping): 112 | d = copy(d) 113 | 114 | for src, dest in mapping.items(): 115 | if src in d: 116 | d[dest] = d[src] 117 | del d[src] 118 | 119 | return d 120 | -------------------------------------------------------------------------------- /omniture/version.py: -------------------------------------------------------------------------------- 1 | # 1) we don't load dependencies by storing it in __init__.py 2 | # 2) we can import it in setup.py for the same reason 3 | # 3) we can import it into your module module 4 | __version__ = '0.5.2' 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pandas 3 | six 4 | future 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | exec(open('omniture/version.py').read()) 4 | setup(name='omniture', 5 | description='A wrapper for the Adobe Analytics (Omniture and SiteCatalyst) web analytics API.', 6 | long_description=open('README.md').read(), 7 | author='Stijn Debrouwere', 8 | author_email='stijn@stdout.be', 9 | url='http://stdbrouw.github.com/python-omniture/', 10 | download_url='http://www.github.com/stdbrouw/python-omniture/tarball/master', 11 | version=__version__, 12 | license='MIT', 13 | packages=find_packages(), 14 | keywords='data analytics api wrapper adobe omniture', 15 | install_requires=[ 16 | 'requests', 17 | 'python-dateutil', 18 | ], 19 | classifiers=['Development Status :: 4 - Beta', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Operating System :: OS Independent', 23 | 'Programming Language :: Python', 24 | 'Topic :: Scientific/Engineering :: Information Analysis', 25 | 'Programming Language :: Python :: 2.7', 26 | 'Programming Language :: Python :: 3' 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dancingcactus/python-omniture/572b6c6ae4126631053e981404eb84cad16bc1ec/tests/__init__.py -------------------------------------------------------------------------------- /tests/mock_objects/Company.GetReportSuites.json: -------------------------------------------------------------------------------- 1 | {"report_suites": 2 | [ 3 | {"rsid": "omniture.api-gateway", "site_title": "test_suite"}, 4 | {"rsid": "testdev", "site_title": "Dev Suite"} 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/mock_objects/Report.Get.NotReady.json: -------------------------------------------------------------------------------- 1 | { 2 | "error":"report_not_ready", 3 | "error_description":"Report not ready", 4 | "error_uri":null 5 | } 6 | -------------------------------------------------------------------------------- /tests/mock_objects/Report.GetElements.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "subrelation": false, 4 | "correlation": true, 5 | "id": "page", 6 | "name": "Page" 7 | }, 8 | { 9 | "subrelation": false, 10 | "correlation": true, 11 | "id": "sitesection", 12 | "name": "Site Section" 13 | }, 14 | { 15 | "subrelation": false, 16 | "correlation": true, 17 | "id": "browser", 18 | "name": "Browser" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /tests/mock_objects/Report.GetMetrics.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "formula": null, 4 | "decimals": 0, 5 | "type": "number", 6 | "id": "pageviews", 7 | "name": "Page Views" 8 | }, 9 | { 10 | "formula": null, 11 | "decimals": 0, 12 | "type": "number", 13 | "id": "visits", 14 | "name": "Visits" 15 | }, 16 | { 17 | "formula": null, 18 | "decimals": 0, 19 | "type": "number", 20 | "id": "uniquevisitors", 21 | "name": "Unique Visitors" 22 | }, 23 | { 24 | "formula": null, 25 | "decimals": 0, 26 | "type": "number", 27 | "id": "orders", 28 | "name": "Orders" 29 | }, 30 | { 31 | "formula": null, 32 | "decimals": 0, 33 | "type": "decimal", 34 | "id": "cm4157_55884bf6e4b018bc0b732e55", 35 | "name": "Page Views / Visit" 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /tests/mock_objects/Report.Queue.json: -------------------------------------------------------------------------------- 1 | { 2 | "reportID":"123456789" 3 | } 4 | -------------------------------------------------------------------------------- /tests/mock_objects/Segments.Get.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "s4157_55b1ba24e4b0a477f869b912", 4 | "name": "Segment 1" 5 | }, 6 | { 7 | "id": "s4157_56097427e4b0ff9bcc064952", 8 | "name": "Segment 2" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /tests/mock_objects/basic_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "report": { 3 | "elements": [ 4 | { 5 | "id": "datetime", 6 | "name": "Date" 7 | } 8 | ], 9 | "data": [ 10 | { 11 | "counts": [ 12 | "14617259" 13 | ], 14 | "month": 9, 15 | "name": "Sun. 4 Sep. 2016", 16 | "day": 4, 17 | "year": 2016 18 | } 19 | ], 20 | "period": "Sun. 4 Sep. 2016", 21 | "totals": [ 22 | "14617259" 23 | ], 24 | "metrics": [ 25 | { 26 | "latency": 1955, 27 | "name": "Page Views", 28 | "type": "number", 29 | "current": false, 30 | "decimals": 0, 31 | "id": "pageviews" 32 | } 33 | ], 34 | "version": "1.4.16.7", 35 | "reportSuite": { 36 | "id": "omniture.api-gateway", 37 | "name": "test_report_suite" 38 | }, 39 | "type": "overtime" 40 | }, 41 | "waitSeconds": "0.442", 42 | "runSeconds": "3.752" 43 | } 44 | -------------------------------------------------------------------------------- /tests/mock_objects/invalid_element.json: -------------------------------------------------------------------------------- 1 | { 2 | "error_uri": "https://marketing.adobe.com/developer/en_US/documentation/analytics-reporting-1-4/elements", 3 | "error_description": "Element 'bad_element' not found", 4 | "error": "element_id_invalid" 5 | } 6 | -------------------------------------------------------------------------------- /tests/mock_objects/invalid_metric.json: -------------------------------------------------------------------------------- 1 | { 2 | "error_uri": "https://marketing.adobe.com/developer/en_US/documentation/analytics-reporting-1-4/metrics", 3 | "error_description": "Metric 'bad_metric' not found", 4 | "error": "metric_id_invalid" 5 | } 6 | -------------------------------------------------------------------------------- /tests/mock_objects/invalid_segment.json: -------------------------------------------------------------------------------- 1 | { 2 | "error_uri": "https://marketing.adobe.com/developer/en_US/documentation/analytics-administration-1-4/r-getsegments-1", 3 | "error_description": "Segment 'bad_segment' not valid for this company", 4 | "error": "segment_invalid" 5 | } 6 | -------------------------------------------------------------------------------- /tests/mock_objects/mixed_classifications.json: -------------------------------------------------------------------------------- 1 | { 2 | "report": { 3 | "elements": [ 4 | { 5 | "id": "evar3", 6 | "classification": "Classification 1", 7 | "name": "Custom Conversion 3" 8 | }, 9 | { 10 | "id": "evar5", 11 | "name": "Custom Conversion 5" 12 | } 13 | ], 14 | "data": [ 15 | { 16 | "url": "", 17 | "breakdown": [ 18 | { 19 | "url": "", 20 | "counts": [ 21 | "2633608" 22 | ], 23 | "name": "Val1" 24 | }, 25 | { 26 | "url": "", 27 | "counts": [ 28 | "10180" 29 | ], 30 | "name": "Val2" 31 | }, 32 | { 33 | "url": "", 34 | "counts": [ 35 | "9320" 36 | ], 37 | "name": "Val3" 38 | } 39 | ], 40 | "counts": [ 41 | "2663111" 42 | ], 43 | "name": "::unspecified::" 44 | }, 45 | { 46 | "url": "", 47 | "breakdown": [ 48 | { 49 | "url": "", 50 | "counts": [ 51 | "26935" 52 | ], 53 | "name": "Val1" 54 | }, 55 | { 56 | "url": "", 57 | "counts": [ 58 | "56868" 59 | ], 60 | "name": "Val2" 61 | }, 62 | { 63 | "url": "", 64 | "counts": [ 65 | "3894" 66 | ], 67 | "name": "Val3" 68 | } 69 | ], 70 | "counts": [ 71 | "92574" 72 | ], 73 | "name": "bc_name r1" 74 | }, 75 | { 76 | "url": "", 77 | "breakdown": [ 78 | { 79 | "url": "", 80 | "counts": [ 81 | "2322" 82 | ], 83 | "name": "Val2" 84 | }, 85 | { 86 | "url": "", 87 | "counts": [ 88 | "642" 89 | ], 90 | "name": "Val3" 91 | }, 92 | { 93 | "url": "", 94 | "counts": [ 95 | "622" 96 | ], 97 | "name": "Val2" 98 | } 99 | ], 100 | "counts": [ 101 | "52799" 102 | ], 103 | "name": "bc_name r3" 104 | }, 105 | { 106 | "url": "", 107 | "breakdown": [ 108 | { 109 | "url": "", 110 | "counts": [ 111 | "1532" 112 | ], 113 | "name": "SC" 114 | }, 115 | { 116 | "url": "", 117 | "counts": [ 118 | "47139" 119 | ], 120 | "name": "API" 121 | }, 122 | { 123 | "url": "", 124 | "counts": [ 125 | "382" 126 | ], 127 | "name": "SC-DL" 128 | }, 129 | { 130 | "url": "", 131 | "counts": [ 132 | "125" 133 | ], 134 | "name": "DW" 135 | }, 136 | { 137 | "url": "", 138 | "counts": [ 139 | "37" 140 | ], 141 | "name": "Reportlet" 142 | }, 143 | { 144 | "url": "", 145 | "counts": [ 146 | "33" 147 | ], 148 | "name": "Reportlet-DL" 149 | } 150 | ], 151 | "counts": [ 152 | "49248" 153 | ], 154 | "name": "bc_name r4" 155 | } 156 | ], 157 | "period": "Fri. 1 Jul. 2016", 158 | "totals": [ 159 | "4740015" 160 | ], 161 | "metrics": [ 162 | { 163 | "latency": 693, 164 | "name": "Metric 1", 165 | "type": "number", 166 | "current": false, 167 | "decimals": 0, 168 | "id": "f:347467" 169 | } 170 | ], 171 | "version": "1.4.16.7", 172 | "reportSuite": { 173 | "id": "test_rs", 174 | "name": "test_rs" 175 | }, 176 | "type": "ranked" 177 | }, 178 | "waitSeconds": "0.498", 179 | "runSeconds": "6.585" 180 | } 181 | -------------------------------------------------------------------------------- /tests/mock_objects/multi_classifications.json: -------------------------------------------------------------------------------- 1 | { 2 | "report": { 3 | "elements": [ 4 | { 5 | "id": "evar2", 6 | "classification": "Classification 1", 7 | "name": "Custom Conversion 2" 8 | }, 9 | { 10 | "id": "evar2", 11 | "classification": "Classification 2", 12 | "name": "Custom Conversion 2" 13 | } 14 | ], 15 | "data": [ 16 | { 17 | "url": "", 18 | "breakdown": [ 19 | { 20 | "url": "", 21 | "counts": [ 22 | "641829" 23 | ], 24 | "name": "::unspecified::" 25 | }, 26 | { 27 | "url": "", 28 | "counts": [ 29 | "32480" 30 | ], 31 | "name": "Test" 32 | } 33 | ], 34 | "counts": [ 35 | "743577" 36 | ], 37 | "name": "::unspecified::" 38 | }, 39 | { 40 | "url": "", 41 | "breakdown": [ 42 | { 43 | "url": "", 44 | "counts": [ 45 | "97184" 46 | ], 47 | "name": "test" 48 | }, 49 | { 50 | "url": "", 51 | "counts": [ 52 | "9661" 53 | ], 54 | "name": "test2" 55 | } 56 | ], 57 | "counts": [ 58 | "114328" 59 | ], 60 | "name": "t1" 61 | } 62 | ], 63 | "period": "Thu. 21 Jul. 2016", 64 | "totals": [ 65 | "1061171" 66 | ], 67 | "metrics": [ 68 | { 69 | "latency": 2149, 70 | "name": "Page Views", 71 | "type": "number", 72 | "current": false, 73 | "decimals": 0, 74 | "id": "pageviews" 75 | } 76 | ], 77 | "version": "1.4.16.7", 78 | "reportSuite": { 79 | "id": "test_rs", 80 | "name": "test_RS" 81 | }, 82 | "type": "ranked" 83 | }, 84 | "waitSeconds": "0.718", 85 | "runSeconds": "10.076" 86 | } 87 | -------------------------------------------------------------------------------- /tests/mock_objects/ranked_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "report": { 3 | "elements": [ 4 | { 5 | "id": "page", 6 | "name": "Page" 7 | } 8 | ], 9 | "data": [ 10 | { 11 | "url": "http:/example.com/page1.html", 12 | "counts": [ 13 | "13464027", 14 | "13401805" 15 | ], 16 | "name": "page1" 17 | }, 18 | { 19 | "url": "http:/example.com/page2.html", 20 | "counts": [ 21 | "31320", 22 | "31298" 23 | ], 24 | "name": "page2" 25 | }, 26 | { 27 | "url": "http:/example.com/page3.html", 28 | "counts": [ 29 | "501", 30 | "501" 31 | ], 32 | "name": "page3" 33 | }, 34 | { 35 | "url": "http:/example.com/page4.html", 36 | "counts": [ 37 | "249", 38 | "249" 39 | ], 40 | "name": "page4" 41 | }, 42 | { 43 | "url": "http:/example.com/page5.html", 44 | "counts": [ 45 | "99", 46 | "99" 47 | ], 48 | "name": "page5" 49 | }, 50 | { 51 | "url": "http:/example.com/page5.html", 52 | "counts": [ 53 | "10", 54 | "5" 55 | ], 56 | "name": "page5" 57 | } 58 | ], 59 | "period": "Sun. 4 Sep. 2016", 60 | "totals": [ 61 | "13496206", 62 | "13433952" 63 | ], 64 | "metrics": [ 65 | { 66 | "latency": 2291, 67 | "name": "Page Views", 68 | "type": "number", 69 | "current": false, 70 | "decimals": 0, 71 | "id": "pageviews" 72 | }, 73 | { 74 | "latency": 2291, 75 | "name": "Visits", 76 | "type": "number", 77 | "current": false, 78 | "decimals": 0, 79 | "id": "visits" 80 | } 81 | ], 82 | "version": "1.4.16.7", 83 | "reportSuite": { 84 | "id": "omniture.api-gateway", 85 | "name": "test_report_suite" 86 | }, 87 | "type": "ranked" 88 | }, 89 | "waitSeconds": "0.557", 90 | "runSeconds": "3.877" 91 | } 92 | -------------------------------------------------------------------------------- /tests/mock_objects/ranked_report_inf.json: -------------------------------------------------------------------------------- 1 | { 2 | "report": { 3 | "elements": [ 4 | { 5 | "id": "page", 6 | "name": "Page" 7 | } 8 | ], 9 | "data": [ 10 | { 11 | "url": "http:/example.com/page1.html", 12 | "counts": [ 13 | "13464027", 14 | "13401805" 15 | ], 16 | "name": "page1" 17 | }, 18 | { 19 | "url": "http:/example.com/page2.html", 20 | "counts": [ 21 | "31320", 22 | "31298" 23 | ], 24 | "name": "page2" 25 | }, 26 | { 27 | "url": "http:/example.com/page3.html", 28 | "counts": [ 29 | "501", 30 | "501" 31 | ], 32 | "name": "page3" 33 | }, 34 | { 35 | "url": "http:/example.com/page4.html", 36 | "counts": [ 37 | "249", 38 | "249" 39 | ], 40 | "name": "page4" 41 | }, 42 | { 43 | "url": "http:/example.com/page5.html", 44 | "counts": [ 45 | "99", 46 | "99" 47 | ], 48 | "name": "page5" 49 | }, 50 | { 51 | "url": "http:/example.com/page5.html", 52 | "counts": [ 53 | "INF", 54 | "INF" 55 | ], 56 | "name": "page5" 57 | } 58 | ], 59 | "period": "Sun. 4 Sep. 2016", 60 | "totals": [ 61 | "13496206", 62 | "13433952" 63 | ], 64 | "metrics": [ 65 | { 66 | "latency": 2291, 67 | "name": "Page Views", 68 | "type": "number", 69 | "current": false, 70 | "decimals": 0, 71 | "id": "pageviews" 72 | }, 73 | { 74 | "latency": 2291, 75 | "name": "Visits", 76 | "type": "number", 77 | "current": false, 78 | "decimals": 0, 79 | "id": "visits" 80 | } 81 | ], 82 | "version": "1.4.16.7", 83 | "reportSuite": { 84 | "id": "omniture.api-gateway", 85 | "name": "test_report_suite" 86 | }, 87 | "type": "ranked" 88 | }, 89 | "waitSeconds": "0.557", 90 | "runSeconds": "3.877" 91 | } 92 | -------------------------------------------------------------------------------- /tests/mock_objects/segmented_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "report": { 3 | "elements": [ 4 | { 5 | "id": "datetime", 6 | "name": "Date" 7 | } 8 | ], 9 | "data": [ 10 | { 11 | "counts": [ 12 | "0" 13 | ], 14 | "month": 9, 15 | "name": "Sun. 4 Sep. 2016", 16 | "day": 4, 17 | "year": 2016 18 | } 19 | ], 20 | "segments": [ 21 | { 22 | "id": "s4157_55b1ba24e4b0a477f869b912", 23 | "name": "Segment 1" 24 | } 25 | ], 26 | "period": "Sun. 4 Sep. 2016", 27 | "totals": [ 28 | "0" 29 | ], 30 | "metrics": [ 31 | { 32 | "latency": 2308, 33 | "name": "Page Views", 34 | "type": "number", 35 | "current": false, 36 | "decimals": 0, 37 | "id": "pageviews" 38 | } 39 | ], 40 | "version": "1.4.16.7", 41 | "reportSuite": { 42 | "id": "omniture.api-gateway", 43 | "name": "test_report_suite" 44 | }, 45 | "type": "overtime" 46 | }, 47 | "waitSeconds": "1.219", 48 | "runSeconds": "2.717" 49 | } 50 | -------------------------------------------------------------------------------- /tests/mock_objects/trended_report.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testAccount.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import unittest 4 | import requests_mock 5 | import omniture 6 | import os 7 | 8 | creds = {} 9 | creds['username'] = os.environ['OMNITURE_USERNAME'] 10 | creds['secret'] = os.environ['OMNITURE_SECRET'] 11 | test_report_suite = "omniture.api-gateway" 12 | 13 | 14 | class AccountTest(unittest.TestCase): 15 | def setUp(self): 16 | with requests_mock.mock() as m: 17 | path = os.path.dirname(__file__) 18 | #read in mock response for Company.GetReportSuites to make tests faster 19 | with open(path+'/mock_objects/Company.GetReportSuites.json') as get_report_suites_file: 20 | report_suites = get_report_suites_file.read() 21 | 22 | with open(path+'/mock_objects/Report.GetMetrics.json') as get_metrics_file: 23 | metrics = get_metrics_file.read() 24 | 25 | with open(path+'/mock_objects/Report.GetElements.json') as get_elements_file: 26 | elements = get_elements_file.read() 27 | 28 | with open(path+'/mock_objects/Segments.Get.json') as get_segments_file: 29 | segments = get_segments_file.read() 30 | 31 | #setup mock responses 32 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Company.GetReportSuites', text=report_suites) 33 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.GetMetrics', text=metrics) 34 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.GetElements', text=elements) 35 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Segments.Get', text=segments) 36 | 37 | 38 | self.analytics = omniture.authenticate(creds['username'], creds['secret']) 39 | #force requests to happen in this method so they are cached 40 | self.analytics.suites[test_report_suite].metrics 41 | self.analytics.suites[test_report_suite].elements 42 | self.analytics.suites[test_report_suite].segments 43 | 44 | 45 | def test_os_environ(self): 46 | test = omniture.authenticate({'OMNITURE_USERNAME':creds['username'], 47 | 'OMNITURE_SECRET':creds['secret']}) 48 | self.assertEqual(test.username,creds['username'], 49 | "The username isn't getting set right: {}" 50 | .format(test.username)) 51 | 52 | self.assertEqual(test.secret,creds['secret'], 53 | "The secret isn't getting set right: {}" 54 | .format(test.secret)) 55 | 56 | def test_suites(self): 57 | self.assertIsInstance(self.analytics.suites, omniture.utils.AddressableList, "There are no suites being returned") 58 | self.assertIsInstance(self.analytics.suites[test_report_suite], omniture.account.Suite, "There are no suites being returned") 59 | 60 | def test_simple_request(self): 61 | """ simplest request possible. Company.GetEndpoint is not an authenticated method 62 | """ 63 | urls = ["https://api.omniture.com/admin/1.4/rest/", 64 | "https://api2.omniture.com/admin/1.4/rest/", 65 | "https://api3.omniture.com/admin/1.4/rest/", 66 | "https://api4.omniture.com/admin/1.4/rest/", 67 | "https://api5.omniture.com/admin/1.4/rest/"] 68 | self.assertIn(self.analytics.request('Company', 'GetEndpoint'),urls, "Company.GetEndpoint failed" ) 69 | 70 | def test_authenticated_request(self): 71 | """ Request that requires authentication to make sure the auth is working 72 | """ 73 | reportsuites = self.analytics.request('Company','GetReportSuites') 74 | self.assertIsInstance(reportsuites, dict, "Didn't get a valid response back") 75 | self.assertIsInstance(reportsuites['report_suites'], list, "Response doesn't contain the list of report suites might be an authentication issue") 76 | 77 | def test_metrics(self): 78 | """ Makes sure the suite properties can get the list of metrics 79 | """ 80 | self.assertIsInstance(self.analytics.suites[test_report_suite].metrics, omniture.utils.AddressableList) 81 | 82 | def test_elements(self): 83 | """ Makes sure the suite properties can get the list of elements 84 | """ 85 | self.assertIsInstance(self.analytics.suites[test_report_suite].elements, omniture.utils.AddressableList) 86 | 87 | def test_basic_report(self): 88 | """ Make sure a basic report can be run 89 | """ 90 | report = self.analytics.suites[test_report_suite].report 91 | queue = [] 92 | queue.append(report) 93 | response = omniture.sync(queue) 94 | self.assertIsInstance(response, list) 95 | 96 | def test_json_report(self): 97 | """Make sure reports can be generated from JSON objects""" 98 | report = self.analytics.suites[test_report_suite].report\ 99 | .element('page')\ 100 | .metric('pageviews')\ 101 | .sortBy('pageviews')\ 102 | .filter("s4157_55b1ba24e4b0a477f869b912")\ 103 | .range("2016-08-01","2016-08-31")\ 104 | .set('sortMethod',"top")\ 105 | .json() 106 | self.assertEqual(report, self.analytics.jsonReport(report).json(), "The reports aren't serializating or de-serializing correctly in JSON") 107 | 108 | 109 | def test_account_repr_html_(self): 110 | """Make sure the account are printing out in 111 | HTML correctly for ipython notebooks""" 112 | html = self.analytics._repr_html_() 113 | test_html = "Username: jgrover:Justin Grover Demo
Secret: ***************
Report Suites: 2
Endpoint: https://api.omniture.com/admin/1.4/rest/
" 114 | self.assertEqual(html, test_html) 115 | 116 | def test_account__str__(self): 117 | """ Make sure the custom str works """ 118 | mystr = self.analytics.__str__() 119 | test_str = "Analytics Account -------------\n Username: jgrover:Justin Grover Demo \n Report Suites: 2 \n Endpoint: https://api.omniture.com/admin/1.4/rest/" 120 | self.assertEqual(mystr, test_str) 121 | 122 | def test_suite_repr_html_(self): 123 | """Make sure the Report Suites are printing okay for 124 | ipython notebooks """ 125 | html = self.analytics.suites[0]._repr_html_() 126 | test_html = "" 127 | self.assertEqual(html, test_html) 128 | 129 | def test_suite__str__(self): 130 | """Make sure the str represntation is working """ 131 | mystr = self.analytics.suites[0].__str__() 132 | test_str = "ID omniture.api-gateway | Name: test_suite \n" 133 | self.assertEqual(mystr,test_str) 134 | 135 | 136 | if __name__ == '__main__': 137 | unittest.main() 138 | -------------------------------------------------------------------------------- /tests/testElement.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import unittest 4 | import omniture 5 | import os 6 | 7 | creds = {} 8 | creds['username'] = os.environ['OMNITURE_USERNAME'] 9 | creds['secret'] = os.environ['OMNITURE_SECRET'] 10 | 11 | class ElementTest(unittest.TestCase): 12 | def setUp(self): 13 | fake_list = [{"id":"123","title":"ABC"},{"id":"456","title":"DEF"}] 14 | self.valueList = omniture.elements.Value.list("metrics",fake_list,"test") 15 | 16 | def test__repr__(self): 17 | self.assertEqual(self.valueList.__repr__(),"",\ 18 | "The value for __repr__ on the AddressableList was {}"\ 19 | .format(self.valueList.__repr__())) 20 | 21 | def test_value__repr__(self): 22 | self.assertEqual(self.valueList[0].__repr__(),"", \ 23 | "The value of the first item in the AddressableList \ 24 | was {}".format(self.valueList[0].__repr__())) 25 | 26 | def test_value__copy__(self): 27 | value = self.valueList[0].copy() 28 | self.assertEqual(value.__repr__(), self.valueList[0].__repr__(),\ 29 | "The copied value was: {} the original was: {}"\ 30 | .format(value, self.valueList[0])) 31 | 32 | def test_repr_html_(self): 33 | self.assertEqual(self.valueList[0]._repr_html_(),\ 34 | "",\ 35 | "The html value was: {}"\ 36 | .format(self.valueList[0]._repr_html_())) 37 | 38 | def test__str__(self): 39 | self.assertEqual(self.valueList[0].__str__(),\ 40 | "ID 123 | Name: ABC \n",\ 41 | "__str__ returned: {}"\ 42 | .format(self.valueList[0].__str__())) 43 | 44 | if __name__ == '__main__': 45 | unittest.main() 46 | -------------------------------------------------------------------------------- /tests/testReports.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from __future__ import print_function 3 | 4 | 5 | import unittest 6 | import omniture 7 | import os 8 | from datetime import date 9 | import pandas 10 | import datetime 11 | import requests_mock 12 | 13 | 14 | creds = {} 15 | creds['username'] = os.environ['OMNITURE_USERNAME'] 16 | creds['secret'] = os.environ['OMNITURE_SECRET'] 17 | test_report_suite = 'omniture.api-gateway' 18 | 19 | 20 | class ReportTest(unittest.TestCase): 21 | def setUp(self): 22 | self.maxDiff = None 23 | with requests_mock.mock() as m: 24 | path = os.path.dirname(__file__) 25 | #read in mock response for Company.GetReportSuites to make tests faster 26 | with open(path+'/mock_objects/Company.GetReportSuites.json') as get_report_suites_file: 27 | report_suites = get_report_suites_file.read() 28 | 29 | with open(path+'/mock_objects/Report.GetMetrics.json') as get_metrics_file: 30 | metrics = get_metrics_file.read() 31 | 32 | with open(path+'/mock_objects/Report.GetElements.json') as get_elements_file: 33 | elements = get_elements_file.read() 34 | 35 | with open(path+'/mock_objects/Segments.Get.json') as get_segments_file: 36 | segments = get_segments_file.read() 37 | 38 | #setup mock responses 39 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Company.GetReportSuites', text=report_suites) 40 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.GetMetrics', text=metrics) 41 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.GetElements', text=elements) 42 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Segments.Get', text=segments) 43 | 44 | 45 | self.analytics = omniture.authenticate(creds['username'], creds['secret']) 46 | #force requests to happen in this method so they are cached 47 | self.analytics.suites[test_report_suite].metrics 48 | self.analytics.suites[test_report_suite].elements 49 | self.analytics.suites[test_report_suite].segments 50 | 51 | def tearDown(self): 52 | self.analytics = None 53 | 54 | @requests_mock.mock() 55 | def test_basic_report(self,m): 56 | """ Make sure a basic report can be run 57 | """ 58 | 59 | path = os.path.dirname(__file__) 60 | 61 | with open(path+'/mock_objects/basic_report.json') as data_file: 62 | json_response = data_file.read() 63 | 64 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 65 | report_queue = queue_file.read() 66 | 67 | #setup mock object 68 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 69 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 70 | 71 | response = self.analytics.suites[test_report_suite].report.run() 72 | 73 | self.assertIsInstance(response.data, list, "Something went wrong with the report") 74 | 75 | #Timing Info 76 | self.assertIsInstance(response.timing['queue'], float, "waitSeconds info is missing") 77 | self.assertIsInstance(response.timing['execution'], float, "Execution info is missing") 78 | #Raw Reports 79 | self.assertIsInstance(response.report, dict, "The raw report hasn't been populated") 80 | #Check Metrics 81 | self.assertIsInstance(response.metrics, list, "The metrics weren't populated") 82 | self.assertEqual(response.metrics[0].id,"pageviews", "Wrong Metric") 83 | #Check Elements 84 | self.assertIsInstance(response.elements, list, "The elements is the wrong type") 85 | self.assertEqual(response.elements[0].id,"datetime", "There are elements when there shouldn't be") 86 | 87 | #check time range 88 | checkdate = date(2016,9,4).strftime("%a. %e %h. %Y") 89 | self.assertEqual(response.period, checkdate) 90 | 91 | #check segmetns 92 | self.assertIsNone(response.segments) 93 | 94 | #Check Data 95 | self.assertIsInstance(response.data, list, "Data isn't getting populated right") 96 | self.assertIsInstance(response.data[0] , dict, "The data isn't getting into the dict") 97 | self.assertIsInstance(response.data[0]['datetime'], datetime.datetime, "The date isn't getting populated in the data") 98 | self.assertIsInstance(response.data[0]['pageviews'], int, "The pageviews aren't getting populated in the data") 99 | 100 | @requests_mock.mock() 101 | def test_ranked_report(self, m): 102 | """ Make sure the ranked report is being processed 103 | """ 104 | 105 | path = os.path.dirname(__file__) 106 | 107 | with open(path+'/mock_objects/ranked_report.json') as data_file: 108 | json_response = data_file.read() 109 | 110 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 111 | report_queue = queue_file.read() 112 | 113 | #setup mock object 114 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 115 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 116 | 117 | ranked = self.analytics.suites[test_report_suite].report.element("page").metric("pageviews").metric("visits") 118 | queue = [] 119 | queue.append(ranked) 120 | response = omniture.sync(queue) 121 | 122 | for report in response: 123 | #Check Data 124 | self.assertIsInstance(report.data, list, "Data isn't getting populated right") 125 | self.assertIsInstance(report.data[0] , dict, "The data isn't getting into the dict") 126 | self.assertIsInstance(report.data[0]['page'], str, "The page isn't getting populated in the data") 127 | self.assertIsInstance(report.data[0]['pageviews'], int, "The pageviews aren't getting populated in the data") 128 | self.assertIsInstance(report.data[0]['visits'], int, "The visits aren't getting populated in the data") 129 | 130 | @requests_mock.mock() 131 | def test_ranked_inf_report(self, m): 132 | """ Make sure the ranked report is being processed 133 | """ 134 | 135 | path = os.path.dirname(__file__) 136 | 137 | with open(path+'/mock_objects/ranked_report_inf.json') as data_file: 138 | json_response = data_file.read() 139 | 140 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 141 | report_queue = queue_file.read() 142 | 143 | #setup mock object 144 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 145 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 146 | 147 | ranked = self.analytics.suites[test_report_suite].report.element("page").metric("pageviews").metric("visits") 148 | queue = [] 149 | queue.append(ranked) 150 | response = omniture.sync(queue) 151 | 152 | for report in response: 153 | #Check Data 154 | self.assertEqual(report.data[-1]["visits"], 0) 155 | self.assertEqual(report.data[-1]["pageviews"], 0) 156 | 157 | 158 | @requests_mock.mock() 159 | def test_trended_report(self,m): 160 | """Make sure the trended reports are being processed corretly""" 161 | 162 | path = os.path.dirname(__file__) 163 | 164 | with open(path+'/mock_objects/trended_report.json') as data_file: 165 | json_response = data_file.read() 166 | 167 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 168 | report_queue = queue_file.read() 169 | 170 | #setup mock object 171 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 172 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 173 | 174 | 175 | trended = self.analytics.suites[test_report_suite].report.element("page").metric("pageviews").granularity('hour').run() 176 | self.assertIsInstance(trended.data, list, "Treneded Reports don't work") 177 | self.assertIsInstance(trended.data[0] , dict, "The data isn't getting into the dict") 178 | self.assertIsInstance(trended.data[0]['datetime'], datetime.datetime, "The date isn't getting propulated correctly") 179 | self.assertIsInstance(trended.data[0]['page'], str, "The page isn't getting populated in the data") 180 | self.assertIsInstance(trended.data[0]['pageviews'], int, "The pageviews aren't getting populated in the data") 181 | 182 | @requests_mock.mock() 183 | def test_dataframe(self,m): 184 | """Make sure the pandas data frame object can be generated""" 185 | 186 | 187 | path = os.path.dirname(__file__) 188 | 189 | with open(path+'/mock_objects/trended_report.json') as data_file: 190 | json_response = data_file.read() 191 | 192 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 193 | report_queue = queue_file.read() 194 | 195 | #setup mock object 196 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 197 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 198 | 199 | trended = self.analytics.suites[test_report_suite].report.element("page").metric("pageviews").granularity('hour').run() 200 | self.assertIsInstance(trended.dataframe, pandas.DataFrame, "Data Frame Object doesn't work") 201 | 202 | @requests_mock.mock() 203 | def test_segments_id(self,m): 204 | """ Make sure segments can be added """ 205 | 206 | path = os.path.dirname(__file__) 207 | 208 | with open(path+'/mock_objects/segmented_report.json') as data_file: 209 | json_response = data_file.read() 210 | 211 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 212 | report_queue = queue_file.read() 213 | 214 | #setup mock object 215 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 216 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 217 | 218 | suite = self.analytics.suites[test_report_suite] 219 | report = suite.report.filter(suite.segments[0]).run() 220 | 221 | self.assertEqual(report.segments[0], suite.segments[0], "The segments don't match") 222 | 223 | @unittest.skip("skip inline segments because checked in Query") 224 | def test_inline_segment(self): 225 | """ Make sure inline segments work """ 226 | #pretty poor check but need to make it work with any report suite 227 | report = self.analytics.suites[0].report.element('page').metric('pageviews').metric('visits').filter(element='browser', selected=["::unspecified::"]).run() 228 | self.assertIsInstance(report.data, list, "inline segments don't work") 229 | 230 | @requests_mock.mock() 231 | def test_multiple_classifications(self, m): 232 | """Makes sure the report can parse multiple classifications correctly since they have the same element ID""" 233 | #load sample file 234 | path = os.path.dirname(__file__) 235 | 236 | with open(path+'/mock_objects/multi_classifications.json') as data_file: 237 | json_response = data_file.read() 238 | 239 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 240 | ReportQueue = queue_file.read() 241 | 242 | #setup mock object 243 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 244 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=ReportQueue) 245 | 246 | report = self.analytics.suites[0].report\ 247 | .element('evar2',classification="Classification 1", disable_validation=True)\ 248 | .element('evar2',classification="Classification 2", disable_validation=True)\ 249 | 250 | report = report.run() 251 | 252 | self.assertTrue('evar2 | Classification 1' in report.data[0], "The Value of report.data[0] was:{}".format(report.data[0])) 253 | self.assertTrue('evar2 | Classification 2' in report.data[0], "The Value of report.data[0] was:{}".format(report.data[0])) 254 | 255 | @requests_mock.mock() 256 | def test_mixed_classifications(self, m): 257 | """Makes sure the report can parse reports with classifications and 258 | regular dimensionscorrectly since they have the same element ID""" 259 | #load sample files with responses for mock objects 260 | path = os.path.dirname(__file__) 261 | 262 | with open(path+'/mock_objects/mixed_classifications.json') as data_file: 263 | json_response = data_file.read() 264 | 265 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 266 | ReportQueue = queue_file.read() 267 | 268 | #setup mock object 269 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 270 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=ReportQueue) 271 | 272 | report = self.analytics.suites[0].report\ 273 | .element('evar3',classification="Classification 1", disable_validation=True)\ 274 | .element('evar5', disable_validation=True)\ 275 | 276 | report = report.run() 277 | 278 | self.assertTrue('evar3 | Classification 1' in report.data[0], "The Value of report.data[0] was:{}".format(report.data[0])) 279 | self.assertTrue('evar5' in report.data[0], "The Value of report.data[0] was:{}".format(report.data[0])) 280 | 281 | @requests_mock.mock() 282 | def test_repr_html_(self,m): 283 | """Test the _repr_html_ method used by iPython for notebook display""" 284 | path = os.path.dirname(__file__) 285 | 286 | with open(path+'/mock_objects/trended_report.json') as data_file: 287 | json_response = data_file.read() 288 | 289 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 290 | report_queue = queue_file.read() 291 | 292 | with open(path+'/mock_objects/trended_report.html') as basic_html_file: 293 | basic_html = basic_html_file.read() 294 | 295 | #setup mock object 296 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 297 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 298 | 299 | trended = self.analytics.suites[test_report_suite].report\ 300 | .element("page").metric("pageviews").granularity('hour').run() 301 | 302 | 303 | self.assertEqual(trended._repr_html_(),basic_html) 304 | 305 | @requests_mock.mock() 306 | def test__dir__(self,m): 307 | """Test the __div__ method for tab autocompletion""" 308 | path = os.path.dirname(__file__) 309 | 310 | with open(path+'/mock_objects/basic_report.json') as data_file: 311 | json_response = data_file.read() 312 | 313 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 314 | report_queue = queue_file.read() 315 | 316 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 317 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 318 | 319 | response = self.analytics.suites[test_report_suite].report.run() 320 | self.assertEqual(response.__dir__(), \ 321 | ['data','dataframe', 'metrics','elements', 'segments', 'period', 'type', 'timing'], 322 | "the __dir__ method broke: {}".format(response.__dir__())) 323 | 324 | @requests_mock.mock() 325 | def test__repr__(self,m): 326 | path = os.path.dirname(__file__) 327 | 328 | with open(path+'/mock_objects/basic_report.json') as data_file: 329 | json_response = data_file.read() 330 | 331 | with open(path+'/mock_objects/Report.Queue.json') as queue_file: 332 | report_queue = queue_file.read() 333 | 334 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Get', text=json_response) 335 | m.post('https://api.omniture.com/admin/1.4/rest/?method=Report.Queue', text=report_queue) 336 | 337 | response = self.analytics.suites[test_report_suite].report.run() 338 | test_string = """""" 344 | self.assertEqual(response.__repr__(),test_string) 345 | 346 | 347 | if __name__ == '__main__': 348 | unittest.main() 349 | -------------------------------------------------------------------------------- /tests/testUtils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import unittest 4 | import omniture 5 | 6 | 7 | 8 | 9 | class UtilsTest(unittest.TestCase): 10 | def setUp(self): 11 | fakelist = [{"id":"123", "title":"abc"},{"id":"456","title":"abc"}] 12 | 13 | self.alist = omniture.Value.list("segemnts",fakelist,{}) 14 | 15 | 16 | def tearDown(self): 17 | del self.alist 18 | 19 | def test_addressable_list_repr_html_(self): 20 | """Test the _repr_html_ for AddressableList this is used in ipython """ 21 | outlist = '
datetimedatetime_friendlypagepageviews
2016-09-04 00:00:00Sun. 4 Sep. 2016page1731750
2016-09-04 00:00:00Sun. 4 Sep. 2016page21756
2016-09-04 00:00:00Sun. 4 Sep. 2016page344
2016-09-04 00:00:00Sun. 4 Sep. 2016authorize|prod15
2016-09-04 00:00:00Sun. 4 Sep. 2016page514
2016-09-04 00:00:00Sun. 4 Sep. 2016page60
2016-09-04 01:00:00Sun. 4 Sep. 2016 (Hour 1)page1748879
2016-09-04 01:00:00Sun. 4 Sep. 2016 (Hour 1)page21697
2016-09-04 01:00:00Sun. 4 Sep. 2016 (Hour 1)page355
2016-09-04 01:00:00Sun. 4 Sep. 2016 (Hour 1)page410
2016-09-04 01:00:00Sun. 4 Sep. 2016 (Hour 1)page51
2016-09-04 01:00:00Sun. 4 Sep. 2016 (Hour 1)page61
2016-09-04 02:00:00Sun. 4 Sep. 2016 (Hour 2)page1791212
2016-09-04 02:00:00Sun. 4 Sep. 2016 (Hour 2)page21800
2016-09-04 02:00:00Sun. 4 Sep. 2016 (Hour 2)page330
2016-09-04 02:00:00Sun. 4 Sep. 2016 (Hour 2)authorize|prod15
2016-09-04 02:00:00Sun. 4 Sep. 2016 (Hour 2)page51
2016-09-04 02:00:00Sun. 4 Sep. 2016 (Hour 2)page60
2016-09-04 03:00:00Sun. 4 Sep. 2016 (Hour 3)page1784806
2016-09-04 03:00:00Sun. 4 Sep. 2016 (Hour 3)page21812
2016-09-04 03:00:00Sun. 4 Sep. 2016 (Hour 3)page324
2016-09-04 03:00:00Sun. 4 Sep. 2016 (Hour 3)authorize|prod10
2016-09-04 03:00:00Sun. 4 Sep. 2016 (Hour 3)page51
2016-09-04 03:00:00Sun. 4 Sep. 2016 (Hour 3)page60
2016-09-04 04:00:00Sun. 4 Sep. 2016 (Hour 4)page1869530
2016-09-04 04:00:00Sun. 4 Sep. 2016 (Hour 4)page21822
2016-09-04 04:00:00Sun. 4 Sep. 2016 (Hour 4)page329
2016-09-04 04:00:00Sun. 4 Sep. 2016 (Hour 4)authorize|prod7
2016-09-04 04:00:00Sun. 4 Sep. 2016 (Hour 4)page55
2016-09-04 04:00:00Sun. 4 Sep. 2016 (Hour 4)page60
2016-09-04 05:00:00Sun. 4 Sep. 2016 (Hour 5)page1840170
2016-09-04 05:00:00Sun. 4 Sep. 2016 (Hour 5)page21781
2016-09-04 05:00:00Sun. 4 Sep. 2016 (Hour 5)page326
2016-09-04 05:00:00Sun. 4 Sep. 2016 (Hour 5)authorize|prod11
2016-09-04 05:00:00Sun. 4 Sep. 2016 (Hour 5)page51
2016-09-04 05:00:00Sun. 4 Sep. 2016 (Hour 5)page60
2016-09-04 06:00:00Sun. 4 Sep. 2016 (Hour 6)page1814745
2016-09-04 06:00:00Sun. 4 Sep. 2016 (Hour 6)page21751
2016-09-04 06:00:00Sun. 4 Sep. 2016 (Hour 6)page324
2016-09-04 06:00:00Sun. 4 Sep. 2016 (Hour 6)authorize|prod15
2016-09-04 06:00:00Sun. 4 Sep. 2016 (Hour 6)page50
2016-09-04 06:00:00Sun. 4 Sep. 2016 (Hour 6)page60
2016-09-04 07:00:00Sun. 4 Sep. 2016 (Hour 7)page1741276
2016-09-04 07:00:00Sun. 4 Sep. 2016 (Hour 7)page21777
2016-09-04 07:00:00Sun. 4 Sep. 2016 (Hour 7)page324
2016-09-04 07:00:00Sun. 4 Sep. 2016 (Hour 7)authorize|prod16
2016-09-04 07:00:00Sun. 4 Sep. 2016 (Hour 7)page51
2016-09-04 07:00:00Sun. 4 Sep. 2016 (Hour 7)page62
2016-09-04 08:00:00Sun. 4 Sep. 2016 (Hour 8)page1770364
2016-09-04 08:00:00Sun. 4 Sep. 2016 (Hour 8)page21855
2016-09-04 08:00:00Sun. 4 Sep. 2016 (Hour 8)page326
2016-09-04 08:00:00Sun. 4 Sep. 2016 (Hour 8)authorize|prod9
2016-09-04 08:00:00Sun. 4 Sep. 2016 (Hour 8)page52
2016-09-04 08:00:00Sun. 4 Sep. 2016 (Hour 8)page60
2016-09-04 09:00:00Sun. 4 Sep. 2016 (Hour 9)page1703279
2016-09-04 09:00:00Sun. 4 Sep. 2016 (Hour 9)page21800
2016-09-04 09:00:00Sun. 4 Sep. 2016 (Hour 9)page324
2016-09-04 09:00:00Sun. 4 Sep. 2016 (Hour 9)authorize|prod21
2016-09-04 09:00:00Sun. 4 Sep. 2016 (Hour 9)page50
2016-09-04 09:00:00Sun. 4 Sep. 2016 (Hour 9)page60
2016-09-04 10:00:00Sun. 4 Sep. 2016 (Hour 10)page1762225
2016-09-04 10:00:00Sun. 4 Sep. 2016 (Hour 10)page21735
2016-09-04 10:00:00Sun. 4 Sep. 2016 (Hour 10)page324
2016-09-04 10:00:00Sun. 4 Sep. 2016 (Hour 10)authorize|prod13
2016-09-04 10:00:00Sun. 4 Sep. 2016 (Hour 10)page58
2016-09-04 10:00:00Sun. 4 Sep. 2016 (Hour 10)page60
2016-09-04 11:00:00Sun. 4 Sep. 2016 (Hour 11)page1707441
2016-09-04 11:00:00Sun. 4 Sep. 2016 (Hour 11)page21730
2016-09-04 11:00:00Sun. 4 Sep. 2016 (Hour 11)page324
2016-09-04 11:00:00Sun. 4 Sep. 2016 (Hour 11)authorize|prod10
2016-09-04 11:00:00Sun. 4 Sep. 2016 (Hour 11)page519
2016-09-04 11:00:00Sun. 4 Sep. 2016 (Hour 11)page60
2016-09-04 12:00:00Sun. 4 Sep. 2016 (Hour 12)page1671911
2016-09-04 12:00:00Sun. 4 Sep. 2016 (Hour 12)page21687
2016-09-04 12:00:00Sun. 4 Sep. 2016 (Hour 12)page324
2016-09-04 12:00:00Sun. 4 Sep. 2016 (Hour 12)authorize|prod9
2016-09-04 12:00:00Sun. 4 Sep. 2016 (Hour 12)page513
2016-09-04 12:00:00Sun. 4 Sep. 2016 (Hour 12)page62
2016-09-04 13:00:00Sun. 4 Sep. 2016 (Hour 13)page1670532
2016-09-04 13:00:00Sun. 4 Sep. 2016 (Hour 13)page21645
2016-09-04 13:00:00Sun. 4 Sep. 2016 (Hour 13)page324
2016-09-04 13:00:00Sun. 4 Sep. 2016 (Hour 13)authorize|prod8
2016-09-04 13:00:00Sun. 4 Sep. 2016 (Hour 13)page50
2016-09-04 13:00:00Sun. 4 Sep. 2016 (Hour 13)page60
2016-09-04 14:00:00Sun. 4 Sep. 2016 (Hour 14)page1691776
2016-09-04 14:00:00Sun. 4 Sep. 2016 (Hour 14)page21672
2016-09-04 14:00:00Sun. 4 Sep. 2016 (Hour 14)page327
2016-09-04 14:00:00Sun. 4 Sep. 2016 (Hour 14)authorize|prod9
2016-09-04 14:00:00Sun. 4 Sep. 2016 (Hour 14)page521
2016-09-04 14:00:00Sun. 4 Sep. 2016 (Hour 14)page60
2016-09-04 15:00:00Sun. 4 Sep. 2016 (Hour 15)page1721072
2016-09-04 15:00:00Sun. 4 Sep. 2016 (Hour 15)page21667
2016-09-04 15:00:00Sun. 4 Sep. 2016 (Hour 15)page324
2016-09-04 15:00:00Sun. 4 Sep. 2016 (Hour 15)authorize|prod21
2016-09-04 15:00:00Sun. 4 Sep. 2016 (Hour 15)page53
2016-09-04 15:00:00Sun. 4 Sep. 2016 (Hour 15)page60
2016-09-04 16:00:00Sun. 4 Sep. 2016 (Hour 16)page1749614
2016-09-04 16:00:00Sun. 4 Sep. 2016 (Hour 16)page21672
2016-09-04 16:00:00Sun. 4 Sep. 2016 (Hour 16)page324
2016-09-04 16:00:00Sun. 4 Sep. 2016 (Hour 16)authorize|prod26
2016-09-04 16:00:00Sun. 4 Sep. 2016 (Hour 16)page55
2016-09-04 16:00:00Sun. 4 Sep. 2016 (Hour 16)page60
2016-09-04 17:00:00Sun. 4 Sep. 2016 (Hour 17)page1693445
2016-09-04 17:00:00Sun. 4 Sep. 2016 (Hour 17)page21661
2016-09-04 17:00:00Sun. 4 Sep. 2016 (Hour 17)page324
2016-09-04 17:00:00Sun. 4 Sep. 2016 (Hour 17)authorize|prod24
2016-09-04 17:00:00Sun. 4 Sep. 2016 (Hour 17)page54
2016-09-04 17:00:00Sun. 4 Sep. 2016 (Hour 17)page66
2016-09-04 18:00:00Sun. 4 Sep. 2016 (Hour 18)page10
2016-09-04 18:00:00Sun. 4 Sep. 2016 (Hour 18)page20
2016-09-04 18:00:00Sun. 4 Sep. 2016 (Hour 18)page30
2016-09-04 18:00:00Sun. 4 Sep. 2016 (Hour 18)authorize|prod0
2016-09-04 18:00:00Sun. 4 Sep. 2016 (Hour 18)page50
2016-09-04 18:00:00Sun. 4 Sep. 2016 (Hour 18)page60
2016-09-04 19:00:00Sun. 4 Sep. 2016 (Hour 19)page10
2016-09-04 19:00:00Sun. 4 Sep. 2016 (Hour 19)page20
2016-09-04 19:00:00Sun. 4 Sep. 2016 (Hour 19)page30
2016-09-04 19:00:00Sun. 4 Sep. 2016 (Hour 19)authorize|prod0
2016-09-04 19:00:00Sun. 4 Sep. 2016 (Hour 19)page50
2016-09-04 19:00:00Sun. 4 Sep. 2016 (Hour 19)page60
2016-09-04 20:00:00Sun. 4 Sep. 2016 (Hour 20)page10
2016-09-04 20:00:00Sun. 4 Sep. 2016 (Hour 20)page20
2016-09-04 20:00:00Sun. 4 Sep. 2016 (Hour 20)page30
2016-09-04 20:00:00Sun. 4 Sep. 2016 (Hour 20)authorize|prod0
2016-09-04 20:00:00Sun. 4 Sep. 2016 (Hour 20)page50
2016-09-04 20:00:00Sun. 4 Sep. 2016 (Hour 20)page60
2016-09-04 21:00:00Sun. 4 Sep. 2016 (Hour 21)page10
2016-09-04 21:00:00Sun. 4 Sep. 2016 (Hour 21)page20
2016-09-04 21:00:00Sun. 4 Sep. 2016 (Hour 21)page30
2016-09-04 21:00:00Sun. 4 Sep. 2016 (Hour 21)authorize|prod0
2016-09-04 21:00:00Sun. 4 Sep. 2016 (Hour 21)page50
2016-09-04 21:00:00Sun. 4 Sep. 2016 (Hour 21)page60
2016-09-04 22:00:00Sun. 4 Sep. 2016 (Hour 22)page10
2016-09-04 22:00:00Sun. 4 Sep. 2016 (Hour 22)page20
2016-09-04 22:00:00Sun. 4 Sep. 2016 (Hour 22)page30
2016-09-04 22:00:00Sun. 4 Sep. 2016 (Hour 22)authorize|prod0
2016-09-04 22:00:00Sun. 4 Sep. 2016 (Hour 22)page50
2016-09-04 22:00:00Sun. 4 Sep. 2016 (Hour 22)page60
2016-09-04 23:00:00Sun. 4 Sep. 2016 (Hour 23)page10
2016-09-04 23:00:00Sun. 4 Sep. 2016 (Hour 23)page20
2016-09-04 23:00:00Sun. 4 Sep. 2016 (Hour 23)page30
2016-09-04 23:00:00Sun. 4 Sep. 2016 (Hour 23)authorize|prod0
2016-09-04 23:00:00Sun. 4 Sep. 2016 (Hour 23)page50
2016-09-04 23:00:00Sun. 4 Sep. 2016 (Hour 23)page60omniture.api-gatewaytest_suite123ABC
IDTitle
123abc
456abc
' 22 | self.assertEqual(self.alist._repr_html_(),outlist,\ 23 | "The _repr_html_ isn't working: {}"\ 24 | .format(self.alist._repr_html_())) 25 | 26 | def test_addressable_list_str_(self): 27 | """Test _str_ method """ 28 | outstring = 'ID 123 | Name: abc \nID 456 | Name: abc \n' 29 | self.assertEqual(self.alist.__str__(),outstring,\ 30 | "The __str__ isn't working: {}"\ 31 | .format(self.alist.__str__())) 32 | 33 | def test_addressable_list_get_time(self): 34 | """ Test the custom get item raises a problem when there are duplicate names """ 35 | with self.assertRaises(KeyError): 36 | self.alist['abc'] 37 | 38 | def test_wrap(self): 39 | """Test the wrap method """ 40 | self.assertIsInstance(omniture.utils.wrap("test"),list) 41 | self.assertIsInstance(omniture.utils.wrap(["test"]),list) 42 | self.assertEqual(omniture.utils.wrap("test"),["test"]) 43 | self.assertEqual(omniture.utils.wrap(["test"]),["test"]) 44 | 45 | def test_date(self): 46 | """Test the Date Method""" 47 | test_date = "2016-09-01" 48 | self.assertEqual(omniture.utils.date(None), None) 49 | self.assertEqual(omniture.utils.date(test_date).strftime("%Y-%m-%d"), 50 | test_date) 51 | d = datetime.date(2016,9,1) 52 | self.assertEqual(omniture.utils.date(d).strftime("%Y-%m-%d"), 53 | test_date) 54 | 55 | t = datetime.datetime(2016,9,1) 56 | self.assertEqual(omniture.utils.date(t).strftime("%Y-%m-%d"), 57 | test_date) 58 | 59 | self.assertEqual(omniture.utils.date(u"2016-09-01").strftime("%Y-%m-%d"), 60 | test_date) 61 | with self.assertRaises(ValueError): 62 | omniture.utils.date({}) 63 | 64 | def test_affix(self): 65 | """Test the Affix method to make sure it handles things correctly""" 66 | p = "pre" 67 | s = "suf" 68 | v = "val" 69 | con = "+" 70 | 71 | self.assertEqual(omniture.utils.affix(p,v,connector=con), 72 | con.join([p,v])) 73 | self.assertEqual(omniture.utils.affix(base=v,suffix=s,connector=con), 74 | con.join([v,s])) 75 | self.assertEqual(omniture.utils.affix(p,v,s,connector=con), 76 | con.join([p,v,s])) 77 | self.assertEqual(omniture.utils.affix(base=v,connector=con), 78 | con.join([v])) 79 | 80 | def test_translate(self): 81 | """Test the translate method """ 82 | t = {"product":"cat_collar", "price":100, "location":"no where"} 83 | m = {"product":"Product_Name","price":"Cost","date":"Date"} 84 | s = {"Product_Name":"cat_collar", "Cost":100, "location":"no where"} 85 | self.assertEqual(omniture.utils.translate(t,m),s) 86 | 87 | 88 | --------------------------------------------------------------------------------