From a6fbb1ad9a762fd0f3ccc678e94916636fb06036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Thu, 10 Mar 2016 14:51:07 -0500 Subject: [PATCH 1/4] Fix style and add Py3 compatibility with six --- UniversalAnalytics/HTTPLog.py | 69 ++--- UniversalAnalytics/Tracker.py | 505 +++++++++++++++++++-------------- UniversalAnalytics/__init__.py | 2 +- setup.py | 5 +- test/test_everything.py | 71 ++--- 5 files changed, 369 insertions(+), 283 deletions(-) diff --git a/UniversalAnalytics/HTTPLog.py b/UniversalAnalytics/HTTPLog.py index 4fd8893..6ab035f 100644 --- a/UniversalAnalytics/HTTPLog.py +++ b/UniversalAnalytics/HTTPLog.py @@ -1,17 +1,24 @@ #!/usr/bin/python + ############################################################################### +# # Formatting filter for urllib2's HTTPHandler(debuglevel=1) output # Copyright (c) 2013, Analytics Pros -# -# This project is free software, distributed under the BSD license. -# Analytics Pros offers consulting and integration services if your firm needs +# +# This project is free software, distributed under the BSD license. +# Analytics Pros offers consulting and integration services if your firm needs # assistance in strategy, implementation, or auditing existing work. +# ############################################################################### +# Standard library imports +from __future__ import division, print_function, with_statement +# import os +import re +import sys -import sys, re, os -from cStringIO import StringIO - +# Third party libraries +from six.moves import cStringIO as StringIO class BufferTranslator(object): @@ -19,15 +26,6 @@ class BufferTranslator(object): """ parsers = [] - def __init__(self, output): - self.output = output - self.encoding = getattr(output, 'encoding', None) - - def write(self, content): - content = self.translate(content) - self.output.write(content) - - @staticmethod def stripslashes(content): return content.decode('string_escape') @@ -36,38 +34,45 @@ def stripslashes(content): def addslashes(content): return content.encode('string_escape') + def __init__(self, output): + self.output = output + self.encoding = getattr(output, 'encoding', None) + + def write(self, content): + content = self.translate(content) + self.output.write(content) + def translate(self, line): for pattern, method in self.parsers: match = pattern.match(line) if match: return method(match) - return line - class LineBufferTranslator(BufferTranslator): - """ Line buffer implementation supports translation of line-format input - even when input is not already line-buffered. Caches input until newlines - occur, and then dispatches translated input to output buffer. + """ + Line buffer implementation supports translation of line-format input + even when input is not already line-buffered. Caches input until newlines + occur, and then dispatches translated input to output buffer. """ def __init__(self, *a, **kw): self._linepending = [] super(LineBufferTranslator, self).__init__(*a, **kw) - + def write(self, _input): lines = _input.splitlines(True) for i in range(0, len(lines)): last = i if lines[i].endswith('\n'): - prefix = len(self._linepending) and ''.join(self._linepending) or '' + prefix = (len(self._linepending) and + ''.join(self._linepending) or '') self.output.write(self.translate(prefix + lines[i])) del self._linepending[0:] last = -1 - - if last >= 0: - self._linepending.append(lines[ last ]) + if last >= 0: + self._linepending.append(lines[last]) def __del__(self): if len(self._linepending): @@ -75,8 +80,9 @@ def __del__(self): class HTTPTranslator(LineBufferTranslator): - """ Translates output from |urllib2| HTTPHandler(debuglevel = 1) into - HTTP-compatible, readible text structures for human analysis. + """ + Translates output from |urllib2| HTTPHandler(debuglevel = 1) into + HTTP-compatible, readible text structures for human analysis. """ RE_LINE_PARSER = re.compile(r'^(?:([a-z]+):)\s*(\'?)([^\r\n]*)\2(?:[\r\n]*)$') @@ -96,7 +102,7 @@ def translate(self, line): value = parsed.group(3) stage = parsed.group(1) - if stage == 'send': # query string is rendered here + if stage == 'send': # query string is rendered here return '\n# HTTP Request:\n' + self.stripslashes(value) elif stage == 'reply': return '\n\n# HTTP Response:\n' + self.stripslashes(value) @@ -105,17 +111,14 @@ def translate(self, line): else: return value - return line -def consume(outbuffer = None): # Capture standard output +def consume(outbuffer=None): # Capture standard output sys.stdout = HTTPTranslator(outbuffer or sys.stdout) return sys.stdout if __name__ == '__main__': consume(sys.stdout).write(sys.stdin.read()) - print '\n' - -# vim: set nowrap tabstop=4 shiftwidth=4 softtabstop=0 expandtab textwidth=0 filetype=python foldmethod=indent foldcolumn=4 + print('\n') diff --git a/UniversalAnalytics/Tracker.py b/UniversalAnalytics/Tracker.py index 9306d64..eaa7223 100644 --- a/UniversalAnalytics/Tracker.py +++ b/UniversalAnalytics/Tracker.py @@ -1,107 +1,127 @@ ############################################################################### +# # Universal Analytics for Python # Copyright (c) 2013, Analytics Pros -# -# This project is free software, distributed under the BSD license. -# Analytics Pros offers consulting and integration services if your firm needs +# +# This project is free software, distributed under the BSD license. +# Analytics Pros offers consulting and integration services if your firm needs # assistance in strategy, implementation, or auditing existing work. +# ############################################################################### -from urllib2 import urlopen, build_opener, install_opener -from urllib2 import Request, HTTPSHandler -from urllib2 import URLError, HTTPError -from urllib import urlencode - -import random +# Standard library imports +from __future__ import division, print_function, with_statement import datetime +import hashlib +# import random +# import socket import time import uuid -import hashlib -import socket +# Third party libraries +from six.moves.urllib.error import HTTPError, URLError +from six.moves.urllib.parse import urlencode +from six.moves.urllib.request import build_opener, install_opener, urlopen +from six.moves.urllib.request import HTTPSHandler, Request +import six -def generate_uuid(basedata = None): - """ Provides a _random_ UUID with no input, or a UUID4-format MD5 checksum of any input data provided """ +def generate_uuid(basedata=None): + """ + Provides a _random_ UUID with no input, or a UUID4-format MD5 checksum of + any input data provided. + """ if basedata is None: return str(uuid.uuid4()) - elif isinstance(basedata, basestring): +# elif isinstance(basedata, basestring): + elif isinstance(basedata, six.string_types): checksum = hashlib.md5(basedata).hexdigest() - return '%8s-%4s-%4s-%4s-%12s' % (checksum[0:8], checksum[8:12], checksum[12:16], checksum[16:20], checksum[20:32]) + return '%8s-%4s-%4s-%4s-%12s' % (checksum[0:8], checksum[8:12], + checksum[12:16], checksum[16:20], + checksum[20:32]) class Time(datetime.datetime): - """ Wrappers and convenience methods for processing various time representations """ - + """ + Wrappers and convenience methods for processing various time + representations. + """ + @classmethod - def from_unix(cls, seconds, milliseconds = 0): - """ Produce a full |datetime.datetime| object from a Unix timestamp """ + def from_unix(cls, seconds, milliseconds=0): + """ + Produce a full |datetime.datetime| object from a Unix timestamp + """ base = list(time.gmtime(seconds))[0:6] - base.append(milliseconds * 1000) # microseconds + base.append(milliseconds * 1000) # microseconds return cls(* base) @classmethod def to_unix(cls, timestamp): - """ Wrapper over time module to produce Unix epoch time as a float """ + """ + Wrapper over time module to produce Unix epoch time as a float. + """ if not isinstance(timestamp, datetime.datetime): - raise TypeError, 'Time.milliseconds expects a datetime object' + raise TypeError('Time.milliseconds expects a datetime object') base = time.mktime(timestamp.timetuple()) return base @classmethod - def milliseconds_offset(cls, timestamp, now = None): - """ Offset time (in milliseconds) from a |datetime.datetime| object to now """ + def milliseconds_offset(cls, timestamp, now=None): + """ + Offset time (in milliseconds) from a |datetime.datetime| object to now. + """ if isinstance(timestamp, (int, float)): base = timestamp else: - base = cls.to_unix(timestamp) + base = cls.to_unix(timestamp) base = base + (timestamp.microsecond / 1000000) if now is None: now = time.time() return (now - base) * 1000 - class HTTPRequest(object): - """ URL Construction and request handling abstraction. - This is not intended to be used outside this module. + """ + URL Construction and request handling abstraction. + This is not intended to be used outside this module. - Automates mapping of persistent state (i.e. query parameters) - onto transcient datasets for each query. + Automates mapping of persistent state (i.e. query parameters) + onto transcient datasets for each query. """ endpoint = 'https://www.google-analytics.com/collect' - + # Store properties for all requests + def __init__(self, user_agent=None, *args, **opts): + local_user_agent = 'Analytics Pros - Universal Analytics (Python)' + self.user_agent = user_agent or local_user_agent + @staticmethod def debug(): - """ Activate debugging on urllib2 """ - handler = HTTPSHandler(debuglevel = 1) + """ + Activate debugging on urllib2. + """ + handler = HTTPSHandler(debuglevel=1) opener = build_opener(handler) install_opener(opener) - # Store properties for all requests - def __init__(self, user_agent = None, *args, **opts): - self.user_agent = user_agent or 'Analytics Pros - Universal Analytics (Python)' - - @classmethod - def fixUTF8(cls, data): # Ensure proper encoding for UA's servers... - """ Convert all strings to UTF-8 """ + def fixUTF8(cls, data): # Ensure proper encoding for UA's servers... + """ + Convert all strings to UTF-8. + """ for key in data: - if isinstance(data[ key ], basestring): - data[ key ] = data[ key ].encode('utf-8') + if isinstance(data[key], six.string_types): + data[key] = data[key].encode('utf-8') return data - - - # Apply stored properties to the given dataset & POST to the configured endpoint - def send(self, data): + # Apply stored properties to the given dataset & POST to the configured + # endpoint + def send(self, data): request = Request( - self.endpoint + '?' + urlencode(self.fixUTF8(data)), - headers = { - 'User-Agent': self.user_agent - } + self.endpoint + '?' + urlencode(self.fixUTF8(data)), + headers={'User-Agent': self.user_agent}, ) self.open(request) @@ -115,217 +135,223 @@ def open(self, request): return False def cache_request(self, request): - # TODO: implement a proper caching mechanism here for re-transmitting hits - # record = (Time.now(), request.get_full_url(), request.get_data(), request.headers) + # TODO: implement a proper caching mechanism here for re-transmitting + # hits + # record = (Time.now(), request.get_full_url(), request.get_data(), + # request.headers) pass - - class HTTPPost(HTTPRequest): - - # Apply stored properties to the given dataset & POST to the configured endpoint + # Apply stored properties to the given dataset & POST to the configured + # endpoint def send(self, data): request = Request( - self.endpoint, - data = urlencode(self.fixUTF8(data)), - headers = { - 'User-Agent': self.user_agent - } + self.endpoint, + data=urlencode(self.fixUTF8(data)).encode('UTF-8'), + headers={'User-Agent': self.user_agent}, ) self.open(request) - - - - class Tracker(object): """ Primary tracking interface for Universal Analytics """ + option_sequence = { + 'pageview': [(six.string_types, 'dp')], + 'event': [(six.string_types, 'ec'), (six.string_types, 'ea'), + (six.string_types, 'el'), (int, 'ev')], + 'social': [(six.string_types, 'sn'), (six.string_types, 'sa'), + (six.string_types, 'st')], + 'timing': [(six.string_types, 'utc'), (six.string_types, 'utv'), + (six.string_types, 'utt'), (six.string_types, 'utl')] + } params = None parameter_alias = {} - valid_hittypes = ('pageview', 'event', 'social', 'screenview', 'transaction', 'item', 'exception', 'timing') - + valid_hittypes = ('pageview', 'event', 'social', 'screenview', + 'transaction', 'item', 'exception', 'timing') @classmethod def alias(cls, typemap, base, *names): - """ Declare an alternate (humane) name for a measurement protocol parameter """ - cls.parameter_alias[ base ] = (typemap, base) + """ + Declare an alternate (humane) name for a measurement protocol parameter + """ + cls.parameter_alias[base] = (typemap, base) for i in names: - cls.parameter_alias[ i ] = (typemap, base) + cls.parameter_alias[i] = (typemap, base) @classmethod - def coerceParameter(cls, name, value = None): - if isinstance(name, basestring) and name[0] == '&': + def coerceParameter(cls, name, value=None): + if isinstance(name, six.string_types) and name[0] == '&': return name[1:], str(value) elif name in cls.parameter_alias: typecast, param_name = cls.parameter_alias.get(name) return param_name, typecast(value) else: - raise KeyError, 'Parameter "{0}" is not recognized'.format(name) - - - def payload(self, data): - for key, value in data.iteritems(): - try: - yield self.coerceParameter(key, value) - except KeyError: - continue - - - - option_sequence = { - 'pageview': [ (basestring, 'dp') ], - 'event': [ (basestring, 'ec'), (basestring, 'ea'), (basestring, 'el'), (int, 'ev') ], - 'social': [ (basestring, 'sn'), (basestring, 'sa'), (basestring, 'st') ], - 'timing': [ (basestring, 'utc'), (basestring, 'utv'), (basestring, 'utt'), (basestring, 'utl') ] - } + raise KeyError('Parameter "{0}" is not recognized'.format(name)) @classmethod def consume_options(cls, data, hittype, args): - """ Interpret sequential arguments related to known hittypes based on declared structures """ + """ + Interpret sequential arguments related to known hittypes based on + declared structures. + """ opt_position = 0 - data[ 't' ] = hittype # integrate hit type parameter + data['t'] = hittype # integrate hit type parameter if hittype in cls.option_sequence: - for expected_type, optname in cls.option_sequence[ hittype ]: - if opt_position < len(args) and isinstance(args[opt_position], expected_type): - data[ optname ] = args[ opt_position ] + for expected_type, optname in cls.option_sequence[hittype]: + if opt_position < len(args) and isinstance(args[opt_position], + expected_type): + data[optname] = args[opt_position] opt_position += 1 - - - @classmethod - def hittime(cls, timestamp = None, age = None, milliseconds = None): - """ Returns an integer represeting the milliseconds offset for a given hit (relative to now) """ + def hittime(cls, timestamp=None, age=None, milliseconds=None): + """ + Returns an integer represeting the milliseconds offset for a given hit + (relative to now). + """ if isinstance(timestamp, (int, float)): - return int(Time.milliseconds_offset(Time.from_unix(timestamp, milliseconds = milliseconds))) + return int(Time.milliseconds_offset( + Time.from_unix(timestamp, milliseconds=milliseconds)) + ) if isinstance(timestamp, datetime.datetime): return int(Time.milliseconds_offset(timestamp)) if isinstance(age, (int, float)): return int(age * 1000) + (milliseconds or 0) - + def __init__(self, account, name=None, client_id=None, + hash_client_id=False, user_id=None, user_agent=None, + use_post=True): - @property - def account(self): - return self.params.get('tid', None) - - - def __init__(self, account, name = None, client_id = None, hash_client_id = False, user_id = None, user_agent = None, use_post = True): - if use_post is False: - self.http = HTTPRequest(user_agent = user_agent) - else: - self.http = HTTPPost(user_agent = user_agent) + self.http = HTTPRequest(user_agent=user_agent) + else: + self.http = HTTPPost(user_agent=user_agent) - self.params = { 'v': 1, 'tid': account } + self.params = {'v': 1, 'tid': account} if client_id is None: client_id = generate_uuid() - self.params[ 'cid' ] = client_id + self.params['cid'] = client_id self.hash_client_id = hash_client_id if user_id is not None: - self.params[ 'uid' ] = user_id + self.params['uid'] = user_id + def __getitem__(self, name): + param, value = self.coerceParameter(name, None) + return self.params.get(param, None) - def set_timestamp(self, data): - """ Interpret time-related options, apply queue-time parameter as needed """ - if 'hittime' in data: # an absolute timestamp - data['qt'] = self.hittime(timestamp = data.pop('hittime', None)) - if 'hitage' in data: # a relative age (in seconds) - data['qt'] = self.hittime(age = data.pop('hitage', None)) + def __setitem__(self, name, value): + param, value = self.coerceParameter(name, value) + self.params[param] = value + def __delitem__(self, name): + param, value = self.coerceParameter(name, None) + if param in self.params: + del self.params[param] - def send(self, hittype, *args, **data): - """ Transmit HTTP requests to Google Analytics using the measurement protocol """ + @property + def account(self): + return self.params.get('tid', None) + + def payload(self, data): + for key, value in six.iteritems(data): + try: + yield self.coerceParameter(key, value) + except KeyError: + continue + + def set_timestamp(self, data): + """ + Interpret time-related options, apply queue-time parameter as needed. + """ + if 'hittime' in data: # an absolute timestamp + data['qt'] = self.hittime(timestamp=data.pop('hittime', None)) + if 'hitage' in data: # a relative age (in seconds) + data['qt'] = self.hittime(age=data.pop('hitage', None)) + def send(self, hittype, *args, **data): + """ + Transmit HTTP requests to Google Analytics using the measurement + protocol. + """ if hittype not in self.valid_hittypes: - raise KeyError('Unsupported Universal Analytics Hit Type: {0}'.format(repr(hittype))) + raise KeyError('Unsupported Universal Analytics Hit Type: ' + '{0}'.format(repr(hittype))) self.set_timestamp(data) self.consume_options(data, hittype, args) - for item in args: # process dictionary-object arguments of transcient data + # Process dictionary-object arguments of transcient data + for item in args: if isinstance(item, dict): for key, val in self.payload(item): - data[ key ] = val + data[key] = val - for k, v in self.params.iteritems(): # update only absent parameters + # Update only absent parameters + for k, v in six.iteritems(self.params): if k not in data: - data[ k ] = v + data[k] = v - data = dict(self.payload(data)) if self.hash_client_id: - data[ 'cid' ] = generate_uuid(data[ 'cid' ]) + data['cid'] = generate_uuid(data['cid']) # Transmit the hit to Google... self.http.send(data) - - - - # Setting persistent attibutes of the session/hit/etc (inc. custom dimensions/metrics) - def set(self, name, value = None): + # Setting persistent attibutes of the session/hit/etc (inc. custom + # dimensions/metrics) + def set(self, name, value=None): if isinstance(name, dict): for key, value in name.iteritems(): try: param, value = self.coerceParameter(key, value) self.params[param] = value except KeyError: - pass - elif isinstance(name, basestring): + pass + elif isinstance(name, six.string_types): try: param, value = self.coerceParameter(name, value) self.params[param] = value except KeyError: - pass - - - - def __getitem__(self, name): - param, value = self.coerceParameter(name, None) - return self.params.get(param, None) - - def __setitem__(self, name, value): - param, value = self.coerceParameter(name, value) - self.params[param] = value + pass - def __delitem__(self, name): - param, value = self.coerceParameter(name, None) - if param in self.params: - del self.params[param] def safe_unicode(obj): - """ Safe convertion to the Unicode string version of the object """ + """ + Safe convertion to the Unicode string version of the object. + """ try: - return unicode(obj) + return six.text_type(obj) except UnicodeDecodeError: return obj.decode('utf-8') # Declaring name mappings for Measurement Protocol parameters MAX_CUSTOM_DEFINITIONS = 200 -MAX_EC_LISTS = 11 # 1-based index -MAX_EC_PRODUCTS = 11 # 1-based index -MAX_EC_PROMOTIONS = 11 # 1-based index +MAX_EC_LISTS = 11 # 1-based index +MAX_EC_PRODUCTS = 11 # 1-based index +MAX_EC_PROMOTIONS = 11 # 1-based index Tracker.alias(int, 'v', 'protocol-version') Tracker.alias(safe_unicode, 'cid', 'client-id', 'clientId', 'clientid') Tracker.alias(safe_unicode, 'tid', 'trackingId', 'account') Tracker.alias(safe_unicode, 'uid', 'user-id', 'userId', 'userid') Tracker.alias(safe_unicode, 'uip', 'user-ip', 'userIp', 'ipaddr') -Tracker.alias(safe_unicode, 'ua', 'userAgent', 'userAgentOverride', 'user-agent') +Tracker.alias(safe_unicode, 'ua', 'userAgent', 'userAgentOverride', + 'user-agent') Tracker.alias(safe_unicode, 'dp', 'page', 'path') -Tracker.alias(safe_unicode, 'dt', 'title', 'pagetitle', 'pageTitle' 'page-title') +Tracker.alias(safe_unicode, 'dt', 'title', 'pagetitle', 'pageTitle', + 'page-title') Tracker.alias(safe_unicode, 'dl', 'location') Tracker.alias(safe_unicode, 'dh', 'hostname') -Tracker.alias(safe_unicode, 'sc', 'sessioncontrol', 'session-control', 'sessionControl') +Tracker.alias(safe_unicode, 'sc', 'sessioncontrol', 'session-control', + 'sessionControl') Tracker.alias(safe_unicode, 'dr', 'referrer', 'referer') Tracker.alias(int, 'qt', 'queueTime', 'queue-time') Tracker.alias(safe_unicode, 't', 'hitType', 'hittype') @@ -333,46 +359,64 @@ def safe_unicode(obj): # Campaign attribution -Tracker.alias(safe_unicode, 'cn', 'campaign', 'campaignName', 'campaign-name') -Tracker.alias(safe_unicode, 'cs', 'source', 'campaignSource', 'campaign-source') -Tracker.alias(safe_unicode, 'cm', 'medium', 'campaignMedium', 'campaign-medium') -Tracker.alias(safe_unicode, 'ck', 'keyword', 'campaignKeyword', 'campaign-keyword') -Tracker.alias(safe_unicode, 'cc', 'content', 'campaignContent', 'campaign-content') -Tracker.alias(safe_unicode, 'ci', 'campaignId', 'campaignID', 'campaign-id') +Tracker.alias(safe_unicode, 'cn', 'campaign', 'campaignName', + 'campaign-name') +Tracker.alias(safe_unicode, 'cs', 'source', 'campaignSource', + 'campaign-source') +Tracker.alias(safe_unicode, 'cm', 'medium', 'campaignMedium', + 'campaign-medium') +Tracker.alias(safe_unicode, 'ck', 'keyword', 'campaignKeyword', + 'campaign-keyword') +Tracker.alias(safe_unicode, 'cc', 'content', 'campaignContent', + 'campaign-content') +Tracker.alias(safe_unicode, 'ci', 'campaignId', 'campaignID', + 'campaign-id') # Technical specs -Tracker.alias(safe_unicode, 'sr', 'screenResolution', 'screen-resolution', 'resolution') +Tracker.alias(safe_unicode, 'sr', 'screenResolution', 'screen-resolution', + 'resolution') Tracker.alias(safe_unicode, 'vp', 'viewport', 'viewportSize', 'viewport-size') -Tracker.alias(safe_unicode, 'de', 'encoding', 'documentEncoding', 'document-encoding') +Tracker.alias(safe_unicode, 'de', 'encoding', 'documentEncoding', + 'document-encoding') Tracker.alias(int, 'sd', 'colors', 'screenColors', 'screen-colors') Tracker.alias(safe_unicode, 'ul', 'language', 'user-language', 'userLanguage') # Mobile app Tracker.alias(safe_unicode, 'an', 'appName', 'app-name', 'app') -Tracker.alias(safe_unicode, 'cd', 'contentDescription', 'screenName', 'screen-name', 'content-description') +Tracker.alias(safe_unicode, 'cd', 'contentDescription', 'screenName', + 'screen-name', 'content-description') Tracker.alias(safe_unicode, 'av', 'appVersion', 'app-version', 'version') -Tracker.alias(safe_unicode, 'aid', 'appID', 'appId', 'application-id', 'app-id', 'applicationId') +Tracker.alias(safe_unicode, 'aid', 'appID', 'appId', 'application-id', + 'app-id', 'applicationId') Tracker.alias(safe_unicode, 'aiid', 'appInstallerId', 'app-installer-id') # Ecommerce -Tracker.alias(safe_unicode, 'ta', 'affiliation', 'transactionAffiliation', 'transaction-affiliation') -Tracker.alias(safe_unicode, 'ti', 'transaction', 'transactionId', 'transaction-id') -Tracker.alias(float, 'tr', 'revenue', 'transactionRevenue', 'transaction-revenue') -Tracker.alias(float, 'ts', 'shipping', 'transactionShipping', 'transaction-shipping') +Tracker.alias(safe_unicode, 'ta', 'affiliation', 'transactionAffiliation', + 'transaction-affiliation') +Tracker.alias(safe_unicode, 'ti', 'transaction', 'transactionId', + 'transaction-id') +Tracker.alias(float, 'tr', 'revenue', 'transactionRevenue', + 'transaction-revenue') +Tracker.alias(float, 'ts', 'shipping', 'transactionShipping', + 'transaction-shipping') Tracker.alias(float, 'tt', 'tax', 'transactionTax', 'transaction-tax') -Tracker.alias(safe_unicode, 'cu', 'currency', 'transactionCurrency', 'transaction-currency') # Currency code, e.g. USD, EUR +Tracker.alias(safe_unicode, 'cu', 'currency', 'transactionCurrency', + 'transaction-currency') # Currency code, e.g. USD, EUR Tracker.alias(safe_unicode, 'in', 'item-name', 'itemName') Tracker.alias(float, 'ip', 'item-price', 'itemPrice') Tracker.alias(float, 'iq', 'item-quantity', 'itemQuantity') Tracker.alias(safe_unicode, 'ic', 'item-code', 'sku', 'itemCode') -Tracker.alias(safe_unicode, 'iv', 'item-variation', 'item-category', 'itemCategory', 'itemVariation') +Tracker.alias(safe_unicode, 'iv', 'item-variation', 'item-category', + 'itemCategory', 'itemVariation') # Events -Tracker.alias(safe_unicode, 'ec', 'event-category', 'eventCategory', 'category') +Tracker.alias(safe_unicode, 'ec', 'event-category', 'eventCategory', + 'category') Tracker.alias(safe_unicode, 'ea', 'event-action', 'eventAction', 'action') Tracker.alias(safe_unicode, 'el', 'event-label', 'eventLabel', 'label') Tracker.alias(int, 'ev', 'event-value', 'eventValue', 'value') -Tracker.alias(int, 'ni', 'noninteractive', 'nonInteractive', 'noninteraction', 'nonInteraction') +Tracker.alias(int, 'ni', 'noninteractive', 'nonInteractive', 'noninteraction', + 'nonInteraction') # Social @@ -381,7 +425,8 @@ def safe_unicode(obj): Tracker.alias(safe_unicode, 'st', 'social-target', 'socialTarget') # Exceptions -Tracker.alias(safe_unicode, 'exd', 'exception-description', 'exceptionDescription', 'exDescription') +Tracker.alias(safe_unicode, 'exd', 'exception-description', + 'exceptionDescription', 'exDescription') Tracker.alias(int, 'exf', 'exception-fatal', 'exceptionFatal', 'exFatal') # User Timing @@ -393,62 +438,94 @@ def safe_unicode(obj): Tracker.alias(float, 'pdt', 'timingPageLoad', 'timing-page-load') Tracker.alias(float, 'rrt', 'timingRedirect', 'timing-redirect') Tracker.alias(safe_unicode, 'tcp', 'timingTCPConnect', 'timing-tcp-connect') -Tracker.alias(safe_unicode, 'srt', 'timingServerResponse', 'timing-server-response') +Tracker.alias(safe_unicode, 'srt', 'timingServerResponse', + 'timing-server-response') # Custom dimensions and metrics -for i in range(0,200): +for i in range(0, 200): Tracker.alias(safe_unicode, 'cd{0}'.format(i), 'dimension{0}'.format(i)) Tracker.alias(int, 'cm{0}'.format(i), 'metric{0}'.format(i)) # Enhanced Ecommerce -Tracker.alias(str, 'pa') # Product action -Tracker.alias(str, 'tcc') # Coupon code -Tracker.alias(unicode, 'pal') # Product action list -Tracker.alias(int, 'cos') # Checkout step -Tracker.alias(str, 'col') # Checkout step option +Tracker.alias(str, 'pa') # Product action +Tracker.alias(str, 'tcc') # Coupon code +Tracker.alias(six.text_type, 'pal') # Product action list +Tracker.alias(int, 'cos') # Checkout step +Tracker.alias(str, 'col') # Checkout step option Tracker.alias(str, 'promoa') # Promotion action for product_index in range(1, MAX_EC_PRODUCTS): - Tracker.alias(str, 'pr{0}id'.format(product_index)) # Product SKU - Tracker.alias(unicode, 'pr{0}nm'.format(product_index)) # Product name - Tracker.alias(unicode, 'pr{0}br'.format(product_index)) # Product brand - Tracker.alias(unicode, 'pr{0}ca'.format(product_index)) # Product category - Tracker.alias(unicode, 'pr{0}va'.format(product_index)) # Product variant - Tracker.alias(str, 'pr{0}pr'.format(product_index)) # Product price - Tracker.alias(int, 'pr{0}qt'.format(product_index)) # Product quantity - Tracker.alias(str, 'pr{0}cc'.format(product_index)) # Product coupon code - Tracker.alias(int, 'pr{0}ps'.format(product_index)) # Product position - + # Product SKU + Tracker.alias(str, 'pr{0}id'.format(product_index)) + # Product name + Tracker.alias(six.text_type, 'pr{0}nm'.format(product_index)) + # Product brand + Tracker.alias(six.text_type, 'pr{0}br'.format(product_index)) + # Product category + Tracker.alias(six.text_type, 'pr{0}ca'.format(product_index)) + # Product variant + Tracker.alias(six.text_type, 'pr{0}va'.format(product_index)) + # Product price + Tracker.alias(str, 'pr{0}pr'.format(product_index)) + # Product quantity + Tracker.alias(int, 'pr{0}qt'.format(product_index)) + # Product coupon + Tracker.alias(str, 'pr{0}cc'.format(product_index)) + # Product position + Tracker.alias(int, 'pr{0}ps'.format(product_index)) + for custom_index in range(MAX_CUSTOM_DEFINITIONS): - Tracker.alias(str, 'pr{0}cd{1}'.format(product_index, custom_index)) # Product custom dimension - Tracker.alias(int, 'pr{0}cm{1}'.format(product_index, custom_index)) # Product custom metric + # Product custom dimension + Tracker.alias(str, 'pr{0}cd{1}'.format(product_index, custom_index)) + # Product custom metric + Tracker.alias(int, 'pr{0}cm{1}'.format(product_index, custom_index)) for list_index in range(1, MAX_EC_LISTS): - Tracker.alias(str, 'il{0}pi{1}id'.format(list_index, product_index)) # Product impression SKU - Tracker.alias(unicode, 'il{0}pi{1}nm'.format(list_index, product_index)) # Product impression name - Tracker.alias(unicode, 'il{0}pi{1}br'.format(list_index, product_index)) # Product impression brand - Tracker.alias(unicode, 'il{0}pi{1}ca'.format(list_index, product_index)) # Product impression category - Tracker.alias(unicode, 'il{0}pi{1}va'.format(list_index, product_index)) # Product impression variant - Tracker.alias(int, 'il{0}pi{1}ps'.format(list_index, product_index)) # Product impression position - Tracker.alias(int, 'il{0}pi{1}pr'.format(list_index, product_index)) # Product impression price + # Product impression SKU + Tracker.alias(str, 'il{0}pi{1}id'.format(list_index, product_index)) + # Product impression name + Tracker.alias(six.text_type, 'il{0}pi{1}nm'.format(list_index, + product_index)) + # Product impression brand + Tracker.alias(six.text_type, 'il{0}pi{1}br'.format(list_index, + product_index)) + # Product impression category + Tracker.alias(six.text_type, 'il{0}pi{1}ca'.format(list_index, + product_index)) + # Product impression variant + Tracker.alias(six.text_type, 'il{0}pi{1}va'.format(list_index, + product_index)) + # Product impression position + Tracker.alias(int, 'il{0}pi{1}ps'.format(list_index, product_index)) + # Product impression price + Tracker.alias(int, 'il{0}pi{1}pr'.format(list_index, product_index)) for custom_index in range(MAX_CUSTOM_DEFINITIONS): - Tracker.alias(str, 'il{0}pi{1}cd{2}'.format(list_index, product_index, custom_index)) # Product impression custom dimension - Tracker.alias(int, 'il{0}pi{1}cm{2}'.format(list_index, product_index, custom_index)) # Product impression custom metric + # Product impression custom dimension + Tracker.alias(str, 'il{0}pi{1}cd{2}'.format(list_index, + product_index, + custom_index)) + # Product impression custom metric + Tracker.alias(int, 'il{0}pi{1}cm{2}'.format(list_index, + product_index, + custom_index)) for list_index in range(1, MAX_EC_LISTS): - Tracker.alias(unicode, 'il{0}nm'.format(list_index)) # Product impression list name + # Product impression list name + Tracker.alias(six.text_type, 'il{0}nm'.format(list_index)) for promotion_index in range(1, MAX_EC_PROMOTIONS): - Tracker.alias(str, 'promo{0}id'.format(promotion_index)) # Promotion ID - Tracker.alias(unicode, 'promo{0}nm'.format(promotion_index)) # Promotion name - Tracker.alias(str, 'promo{0}cr'.format(promotion_index)) # Promotion creative - Tracker.alias(str, 'promo{0}ps'.format(promotion_index)) # Promotion position + # Promotion ID + Tracker.alias(str, 'promo{0}id'.format(promotion_index)) + # Promotion name + Tracker.alias(six.text_type, 'promo{0}nm'.format(promotion_index)) + # Promotion creative + Tracker.alias(str, 'promo{0}cr'.format(promotion_index)) + # Promotion position + Tracker.alias(str, 'promo{0}ps'.format(promotion_index)) # Shortcut for creating trackers def create(account, *args, **kwargs): return Tracker(account, *args, **kwargs) - -# vim: set nowrap tabstop=4 shiftwidth=4 softtabstop=0 expandtab textwidth=0 filetype=python foldmethod=indent foldcolumn=4 diff --git a/UniversalAnalytics/__init__.py b/UniversalAnalytics/__init__.py index 9c0c110..07fc03c 100644 --- a/UniversalAnalytics/__init__.py +++ b/UniversalAnalytics/__init__.py @@ -1 +1 @@ -import Tracker \ No newline at end of file +from UniversalAnalytics import Tracker diff --git a/setup.py b/setup.py index dd962e4..17c1134 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,9 @@ from setuptools import setup import sys -VERSION=open('commit-version').read().strip() -print >>sys.stderr, "Preparing version {0}\n".format(VERSION or "NOTFOUND") +VERSION='1.0.0' +#VERSION=open('commit-version').read().strip() +#print >>sys.stderr, "Preparing version {0}\n".format(VERSION or "NOTFOUND") try: diff --git a/test/test_everything.py b/test/test_everything.py index 9e0bb93..f7ed1c6 100644 --- a/test/test_everything.py +++ b/test/test_everything.py @@ -1,30 +1,38 @@ #!/usr/bin/python # -*- encoding: utf-8 -*- + ############################################################################### +# # Test and example kit for Universal Analytics for Python # Copyright (c) 2013, Analytics Pros -# -# This project is free software, distributed under the BSD license. -# Analytics Pros offers consulting and integration services if your firm needs +# +# This project is free software, distributed under the BSD license. +# Analytics Pros offers consulting and integration services if your firm needs # assistance in strategy, implementation, or auditing existing work. +# ############################################################################### +# Standard library imports import unittest -import urllib -from UniversalAnalytics import Tracker +# Third party imports +from six.moves import urllib + +# Local imports from UniversalAnalytics import HTTPLog +from UniversalAnalytics import Tracker class UAMPythonTestCase(unittest.TestCase): - + def setUp(self): self._buffer = HTTPLog.StringIO() - HTTPLog.consume(self._buffer) # Capture HTTP output in readible fashion - Tracker.HTTPPost.debug() # Enabled debugging from urllib2 - + # Capture HTTP output in readible fashion + HTTPLog.consume(self._buffer) + Tracker.HTTPPost.debug() # Enabled debugging from urllib2 + # Create the tracker - self.tracker = Tracker.create('UA-XXXXX-Y', use_post = True) + self.tracker = Tracker.create('UA-XXXXX-Y', use_post=True) def tearDown(self): self._buffer.truncate() @@ -32,10 +40,9 @@ def tearDown(self): del self._buffer @classmethod - def url_quote(cls, value, safe_chars = ''): + def url_quote(cls, value, safe_chars=''): return urllib.quote(value, safe_chars) - @property def buffer(self): return self._buffer.getvalue() @@ -43,33 +50,31 @@ def buffer(self): def reset(self): self._buffer.truncate() - def testTrackerOptionsBasic(self): - self.assertEqual('UA-XXXXX-Y', self.tracker.params['tid']) + self.assertEqual('UA-XXXXX-Y', self.tracker.params['tid']) def testPersistentCampaignSettings(self): - # Apply campaign settings self.tracker.set('campaignName', 'testing-campaign') self.tracker.set('campaignMedium', 'testing-medium') self.tracker['campaignSource'] = 'test-source' - - self.assertEqual(self.tracker.params['cn'], 'testing-campaign') + + self.assertEqual(self.tracker.params['cn'], 'testing-campaign') self.assertEqual(self.tracker.params['cm'], 'testing-medium') self.assertEqual(self.tracker.params['cs'], 'test-source') def testSendPageview(self): # Send a pageview self.tracker.send('pageview', '/test') - + print(self.buffer) self.assertIn('t=pageview', self.buffer) self.assertIn('dp={0}'.format(self.url_quote('/test')), self.buffer) self.reset() - def testSendInteractiveEvent(self): # Send an event - self.tracker.send('event', 'mycat', 'myact', 'mylbl', { 'noninteraction': 1, 'page': '/1' }) + self.tracker.send('event', 'mycat', 'myact', 'mylbl', + {'noninteraction': 1, 'page': '/1'}) self.assertIn('t=event', self.buffer) self.assertIn('ec=mycat', self.buffer) self.assertIn('ea=myact', self.buffer) @@ -80,16 +85,15 @@ def testSendInteractiveEvent(self): self.reset() def testSendUnicodeEvent(self): - # Send unicode data: # As unicode self.tracker.send('event', u'câtēgøry', u'åctîõn', u'låbęl', u'válüē') # As str self.tracker.send('event', 'câtēgøry', 'åctîõn', 'låbęl', 'válüē') - - # TODO write assertions for these... - # The output buffer should show both representations in UTF-8 for compatibility + # TODO write assertions for these... + # The output buffer should show both representations in UTF-8 for + # compatibility def testSocialHit(self): # Send a social hit @@ -97,7 +101,8 @@ def testSocialHit(self): self.assertIn('t=social', self.buffer) self.assertIn('sn=facebook', self.buffer) self.assertIn('sa=share', self.buffer) - self.assertIn('st={0}'.format(self.url_quote('/test#social')), self.buffer) + self.assertIn('st={0}'.format(self.url_quote('/test#social')), + self.buffer) self.reset() def testTransaction(self): @@ -109,7 +114,7 @@ def testTransaction(self): 'itemCode': 'abc', 'itemCategory': 'hawaiian', 'itemQuantity': 1 - }, hitage = 7200) + }, hitage=7200) # Then the transaction hit... self.tracker.send('transaction', { @@ -119,12 +124,12 @@ def testTransaction(self): 'transactionTax': 3.00, 'transactionShipping': 0.45, 'transactionCurrency': 'USD' - }, hitage = 7200) + }, hitage=7200) def testTimingAdjustedHits(self): - - # A few more hits for good measure, testing real-time support for time offset - self.tracker.send('pageview', '/test', { 'campaignName': 'testing2' }, hitage = 60 * 5) # 5 minutes ago - self.tracker.send('pageview', '/test', { 'campaignName': 'testing3' }, hitage = 60 * 20) # 20 minutes ago - -# vim: set nowrap tabstop=4 shiftwidth=4 softtabstop=0 expandtab textwidth=0 filetype=python foldmethod=indent foldcolumn=4 + # A few more hits for good measure, testing real-time support for time + # offset + self.tracker.send('pageview', '/test', {'campaignName': 'testing2'}, + hitage=60*5) # 5 minutes ago + self.tracker.send('pageview', '/test', {'campaignName': 'testing3'}, + hitage=60*20) # 20 minutes ago From 411db786dcff716b9e3c6c7bd40d20eb47e44417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Thu, 10 Mar 2016 16:57:14 -0500 Subject: [PATCH 2/4] Fix tests for py3 --- UniversalAnalytics/HTTPLog.py | 36 ++++++++++++++++++++++++----------- UniversalAnalytics/Tracker.py | 10 +++++++--- test/test_everything.py | 3 +-- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/UniversalAnalytics/HTTPLog.py b/UniversalAnalytics/HTTPLog.py index 6ab035f..37a5d4b 100644 --- a/UniversalAnalytics/HTTPLog.py +++ b/UniversalAnalytics/HTTPLog.py @@ -13,26 +13,34 @@ # Standard library imports from __future__ import division, print_function, with_statement -# import os import re import sys # Third party libraries -from six.moves import cStringIO as StringIO +from six.moves import cStringIO as StringIO # Used by tests +import six class BufferTranslator(object): - """ Provides a buffer-compatible interface for filtering buffer content. + """ + Provides a buffer-compatible interface for filtering buffer content. """ parsers = [] @staticmethod def stripslashes(content): - return content.decode('string_escape') + if six.PY3: + content = content.encode('UTF-8') + return content.decode('unicode_escape') + else: + return content.decode('string_escape') @staticmethod def addslashes(content): - return content.encode('string_escape') + if six.PY3: + return content.encode('unicode_escape') + else: + return content.encode('string_escape') def __init__(self, output): self.output = output @@ -49,6 +57,9 @@ def translate(self, line): return method(match) return line + def flush(self): + pass + class LineBufferTranslator(BufferTranslator): """ @@ -56,12 +67,13 @@ class LineBufferTranslator(BufferTranslator): even when input is not already line-buffered. Caches input until newlines occur, and then dispatches translated input to output buffer. """ - def __init__(self, *a, **kw): + def __init__(self, *args, **kwargs): self._linepending = [] - super(LineBufferTranslator, self).__init__(*a, **kw) + super(LineBufferTranslator, self).__init__(*args, **kwargs) def write(self, _input): lines = _input.splitlines(True) + last = 0 for i in range(0, len(lines)): last = i if lines[i].endswith('\n'): @@ -71,7 +83,7 @@ def write(self, _input): del self._linepending[0:] last = -1 - if last >= 0: + if lines and last >= 0: self._linepending.append(lines[last]) def __del__(self): @@ -95,7 +107,6 @@ def spacer(cls, line): return cls.RE_PARAMETER_SPACER.sub(r' &\1= ', line) def translate(self, line): - parsed = self.RE_LINE_PARSER.match(line) if parsed: @@ -106,7 +117,7 @@ def translate(self, line): return '\n# HTTP Request:\n' + self.stripslashes(value) elif stage == 'reply': return '\n\n# HTTP Response:\n' + self.stripslashes(value) - elif stage == 'header': + elif stage == 'header5': return value + '\n' else: return value @@ -114,7 +125,10 @@ def translate(self, line): return line -def consume(outbuffer=None): # Capture standard output +def consume(outbuffer=None): + """ + Capture standard output. + """ sys.stdout = HTTPTranslator(outbuffer or sys.stdout) return sys.stdout diff --git a/UniversalAnalytics/Tracker.py b/UniversalAnalytics/Tracker.py index eaa7223..1bbcceb 100644 --- a/UniversalAnalytics/Tracker.py +++ b/UniversalAnalytics/Tracker.py @@ -19,6 +19,7 @@ import uuid # Third party libraries +from six.moves import http_client from six.moves.urllib.error import HTTPError, URLError from six.moves.urllib.parse import urlencode from six.moves.urllib.request import build_opener, install_opener, urlopen @@ -102,9 +103,12 @@ def debug(): """ Activate debugging on urllib2. """ - handler = HTTPSHandler(debuglevel=1) - opener = build_opener(handler) - install_opener(opener) + if six.PY2: + handler = HTTPSHandler(debuglevel=1) + opener = build_opener(handler) + install_opener(opener) + else: + http_client.HTTPConnection.debuglevel = 1 @classmethod def fixUTF8(cls, data): # Ensure proper encoding for UA's servers... diff --git a/test/test_everything.py b/test/test_everything.py index f7ed1c6..455834a 100644 --- a/test/test_everything.py +++ b/test/test_everything.py @@ -41,7 +41,7 @@ def tearDown(self): @classmethod def url_quote(cls, value, safe_chars=''): - return urllib.quote(value, safe_chars) + return urllib.parse.quote(value, safe_chars) @property def buffer(self): @@ -66,7 +66,6 @@ def testPersistentCampaignSettings(self): def testSendPageview(self): # Send a pageview self.tracker.send('pageview', '/test') - print(self.buffer) self.assertIn('t=pageview', self.buffer) self.assertIn('dp={0}'.format(self.url_quote('/test')), self.buffer) self.reset() From 71f931e353ecda604a664613e9309dfa8a376bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Thu, 10 Mar 2016 17:01:58 -0500 Subject: [PATCH 3/4] Fix typo --- UniversalAnalytics/HTTPLog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UniversalAnalytics/HTTPLog.py b/UniversalAnalytics/HTTPLog.py index 37a5d4b..42ddf5e 100644 --- a/UniversalAnalytics/HTTPLog.py +++ b/UniversalAnalytics/HTTPLog.py @@ -117,7 +117,7 @@ def translate(self, line): return '\n# HTTP Request:\n' + self.stripslashes(value) elif stage == 'reply': return '\n\n# HTTP Response:\n' + self.stripslashes(value) - elif stage == 'header5': + elif stage == 'header': return value + '\n' else: return value From c797e9438de8e8fc3b38eb5ba91de5452d9116fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonzalo=20Pe=C3=B1a-Castellanos?= Date: Thu, 10 Mar 2016 19:13:14 -0500 Subject: [PATCH 4/4] Add six as dependency --- setup.py | 55 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/setup.py b/setup.py index 17c1134..0836bbb 100644 --- a/setup.py +++ b/setup.py @@ -1,35 +1,36 @@ -from setuptools import setup -import sys +# -*- coding: utf-8 -*- +import os +try: + from setuptools import setup + from setuptools.command.install import install +except ImportError: + from distutils.core import setup + from distutils.core.command.install import install + +here = os.path.abspath(os.path.dirname(__file__)) -VERSION='1.0.0' -#VERSION=open('commit-version').read().strip() -#print >>sys.stderr, "Preparing version {0}\n".format(VERSION or "NOTFOUND") +VERSION = '1.0.0' +# VERSION=open('commit-version').read().strip() +# print >>sys.stderr, "Preparing version {0}\n".format(VERSION or "NOTFOUND") try: - long_description=open('DESCRIPTION.rst', 'rt').read() + long_description = open('DESCRIPTION.rst', 'rt').read() except Exception: - long_description="Universal Analytics in Python; an implementation of Google's Universal Analytics Measurement Protocol" - - + long_description = ("Universal Analytics in Python; an implementation of " + "Google's Universal Analytics Measurement Protocol") setup( - name = "universal-analytics-python", - description = "Universal Analytics Python Module", - long_description = long_description, - - version = VERSION or 'NOTFOUND', - - author = 'Sam Briesemeister', - author_email = 'opensource@analyticspros.com', - - url = 'https://github.com/analytics-pros/universal-analytics-python', - download_url = "https://github.com/analytics-pros/universal-analytics-python/tarball/" + VERSION, - - license = 'BSD', - packages = ["UniversalAnalytics"], - - install_requires = [], - - zip_safe = True, + name="universal-analytics-python", + description="Universal Analytics Python Module", + long_description=long_description, + version=VERSION, + author='Sam Briesemeister', + author_email='opensource@analyticspros.com', + url='https://github.com/analytics-pros/universal-analytics-python', + download_url="https://github.com/analytics-pros/universal-analytics-python/tarball/" + VERSION, + license='BSD', + packages=["UniversalAnalytics"], + install_requires=['six'], + zip_safe=True, )