├── .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 | [](https://travis-ci.org/dancingcactus/python-omniture) 3 | [](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} | ".format('datetime') 221 | for key in sorted(list(item.keys())): 222 | if key != 'datetime': 223 | html += "{0} | ".format(key) 224 | html += "
{0} | ".format(item['datetime']) 229 | for key, value in sorted(list(item.items())): 230 | if key != 'datetime': 231 | html += "{0} | ".format(value) 232 | html += "