text: """
  1. Goto https://code.google.com/apis/console
  2. Create new project
  3. Goto APIs and register for OAuth2.0 for installed applications
  4. Download JSON secret file and move into same directory as this file
  """
  from datetime import datetime
  import re
  from pandas import compat
  import numpy as np
  from pandas import DataFrame
  import pandas as pd
  import pandas.io.parsers as psr
  import pandas.lib as lib
  from pandas.io.date_converters import generic_parser
  import pandas.io.auth as auth
  from pandas.util.decorators import Appender, Substitution
  
  from apiclient.errors import HttpError
  from oauth2client.client import AccessTokenRefreshError
  from pandas.compat import zip, u
  
  TYPE_MAP = {u('INTEGER'): int, u('FLOAT'): float, u('TIME'): int}
  
  NO_CALLBACK = auth.OOB_CALLBACK_URN
  DOC_URL = auth.DOC_URL
  
  _QUERY_PARAMS = """metrics : list of str
      Un-prefixed metric names (e.g., 'visitors' and not 'ga:visitors')
  dimensions : list of str
      Un-prefixed dimension variable names
  start_date : str/date/datetime
  end_date : str/date/datetime, optional
      Defaults to today
  segment : list of str, optional
  filters : list of str, optional
  start_index : int, default 1
  max_results : int, default 10000
      If >10000, must specify chunksize or ValueError will be raised"""
  
  _QUERY_DOC = """
  Construct a google analytics query using given parameters
  Metrics and dimensions do not need the 'ga:' prefix
  
  Parameters
  ----------
  profile_id : str
  %s
  """ % _QUERY_PARAMS
  
  _GA_READER_DOC = """Given query parameters, return a DataFrame with all the
  data or an iterator that returns DataFrames containing chunks of the data
  
  Parameters
  ----------
  %s
  sort : bool/list, default True
      Sort output by index or list of columns
  chunksize : int, optional
      If max_results >10000, specifies the number of rows per iteration
  index_col : str/list of str/dict, optional
      If unspecified then dimension variables are set as index
  parse_dates : bool/list/dict, default True
  keep_date_col : boolean, default False
  date_parser : optional
  na_values : optional
  converters : optional
  dayfirst : bool, default False
      Informs date parsing
  account_name : str, optional
  account_id : str, optional
  property_name : str, optional
  property_id : str, optional
  profile_name : str, optional
  profile_id : str, optional
  %%(extras)s
  Returns
  -------
  data : DataFrame or DataFrame yielding iterator
  """ % _QUERY_PARAMS
  
  _AUTH_PARAMS = """secrets : str, optional
      File path to the secrets file
  scope : str, optional
      Authentication scope
  token_file_name : str, optional
      Path to token storage
  redirect : str, optional
      Local host redirect if unspecified
  """
  
  
  def reset_token_store():
      """
      Deletes the default token store
      """
      auth.reset_default_token_store()
  
  
  @Substitution(extras=_AUTH_PARAMS)
  @Appender(_GA_READER_DOC)
  def read_ga(metrics, dimensions, start_date, **kwargs):
      lst = ['secrets', 'scope', 'token_file_name', 'redirect']
      reader_kwds = dict((p, kwargs.pop(p)) for p in lst if p in kwargs)
      reader = GAnalytics(**reader_kwds)
      return reader.get_data(metrics=metrics, start_date=start_date,
                             dimensions=dimensions, **kwargs)
  
  
  class OAuthDataReader(object):
      """
      Abstract class for handling OAuth2 authentication using the Google
      oauth2client library
      """
      def __init__(self, scope, token_file_name, redirect):
          """
          Parameters
          ----------
          scope : str
              Designates the authentication scope
          token_file_name : str
              Location of cache for authenticated tokens
          redirect : str
              Redirect URL
          """
          self.scope = scope
          self.token_store = auth.make_token_store(token_file_name)
          self.redirect_url = redirect
  
      def authenticate(self, secrets):
          """
          Run the authentication process and return an authorized
          http object
  
          Parameters
          ----------
          secrets : str
              File name for client secrets
  
          Notes
          -----
          See google documention for format of secrets file
          %s
          """ % DOC_URL
          flow = self._create_flow(secrets)
          return auth.authenticate(flow, self.token_store)
  
      def _create_flow(self, secrets):
          """
          Create an authentication flow based on the secrets file
  
          Parameters
          ----------
          secrets : str
              File name for client secrets
  
          Notes
          -----
          See google documentation for format of secrets file
          %s
          """ % DOC_URL
          return auth.get_flow(secrets, self.scope, self.redirect_url)
  
  
  class GDataReader(OAuthDataReader):
      """
      Abstract class for reading data from google APIs using OAuth2
      Subclasses must implement create_query method
      """
      def __init__(self, scope=auth.DEFAULT_SCOPE,
                   token_file_name=auth.DEFAULT_TOKEN_FILE,
                   redirect=NO_CALLBACK, secrets=auth.DEFAULT_SECRETS):
          super(GDataReader, self).__init__(scope, token_file_name, redirect)
          self._service = self._init_service(secrets)
  
      @property
      def service(self):
          """The authenticated request service object"""
          return self._service
  
      def _init_service(self, secrets):
          """
          Build an authenticated google api request service using the given
          secrets file
          """
          http = self.authenticate(secrets)
          return auth.init_service(http)
  
      def get_account(self, name=None, id=None, **kwargs):
          """ Retrieve an account that matches the name, id, or some account
          attribute specified in **kwargs
  
          Parameters
          ----------
          name : str, optional
          id : str, optional
          """
          accounts = self.service.management().accounts().list().execute()
          return _get_match(accounts, name, id, **kwargs)
  
      def get_web_property(self, account_id=None, name=None, id=None, **kwargs):
          """
          Retrieve a web property given and account and property name, id, or
          custom attribute
  
          Parameters
          ----------
          account_id : str, optional
          name : str, optional
          id : str, optional
          """
          prop_store = self.service.management().webproperties()
          kwds = {}
          if account_id is not None:
              kwds['accountId'] = account_id
          prop_for_acct = prop_store.list(**kwds).execute()
          return _get_match(prop_for_acct, name, id, **kwargs)
  
      def get_profile(self, account_id=None, web_property_id=None, name=None,
                      id=None, **kwargs):
  
          """
          Retrieve the right profile for the given account, web property, and
          profile attribute (name, id, or arbitrary parameter in kwargs)
  
          Parameters
          ----------
          account_id : str, optional
          web_property_id : str, optional
          name : str, optional
          id : str, optional
          """
          profile_store = self.service.management().profiles()
          kwds = {}
          if account_id is not None:
              kwds['accountId'] = account_id
          if web_property_id is not None:
              kwds['webPropertyId'] = web_property_id
          profiles = profile_store.list(**kwds).execute()
          return _get_match(profiles, name, id, **kwargs)
  
      def create_query(self, *args, **kwargs):
          raise NotImplementedError()
  
      @Substitution(extras='')
      @Appender(_GA_READER_DOC)
      def get_data(self, metrics, start_date, end_date=None,
                   dimensions=None, segment=None, filters=None, start_index=1,
                   max_results=10000, index_col=None, parse_dates=True,
                   keep_date_col=False, date_parser=None, na_values=None,
                   converters=None, sort=True, dayfirst=False,
                   account_name=None, account_id=None, property_name=None,
                   property_id=None, profile_name=None, profile_id=None,
                   chunksize=None):
          if chunksize is None and max_results > 10000:
              raise ValueError('Google API returns maximum of 10,000 rows, '
                               'please set chunksize')
  
          account = self.get_account(account_name, account_id)
          web_property = self.get_web_property(account.get('id'), property_name,
                                               property_id)
          profile = self.get_profile(account.get('id'), web_property.get('id'),
                                     profile_name, profile_id)
  
          profile_id = profile.get('id')
  
          if index_col is None and dimensions is not None:
              if isinstance(dimensions, compat.string_types):
                  dimensions = [dimensions]
              index_col = _clean_index(list(dimensions), parse_dates)
  
          def _read(start, result_size):
              query = self.create_query(profile_id, metrics, start_date,
                                        end_date=end_date, dimensions=dimensions,
                                        segment=segment, filters=filters,
                                        start_index=start,
                                        max_results=result_size)
  
              try:
                  rs = query.execute()
                  rows = rs.get('rows', [])
                  col_info = rs.get('columnHeaders', [])
                  return self._parse_data(rows, col_info, index_col,
                                          parse_dates=parse_dates,
                                          keep_date_col=keep_date_col,
                                          date_parser=date_parser,
                                          dayfirst=dayfirst,
                                          na_values=na_values,
                                          converters=converters, sort=sort)
              except HttpError as inst:
                  raise ValueError('Google API error %s: %s' % (inst.resp.status,
                                   inst._get_reason()))
  
          if chunksize is None:
              return _read(start_index, max_results)
  
          def iterator():
              curr_start = start_index
  
              while curr_start < max_results:
                  yield _read(curr_start, chunksize)
                  curr_start += chunksize
          return iterator()
  
      def _parse_data(self, rows, col_info, index_col, parse_dates=True,
                      keep_date_col=False, date_parser=None, dayfirst=False,
                      na_values=None, converters=None, sort=True):
          # TODO use returned column types
          col_names = _get_col_names(col_info)
          df = psr._read(rows, dict(index_col=index_col, parse_dates=parse_dates,
                                    date_parser=date_parser, dayfirst=dayfirst,
                                    na_values=na_values,
                                    keep_date_col=keep_date_col,
                                    converters=converters,
                                    header=None, names=col_names))
  
          if isinstance(sort, bool) and sort:
              return df.sort_index()
          elif isinstance(sort, (compat.string_types, list, tuple, np.ndarray)):
              return df.sort_index(by=sort)
  
          return df
  
  
  class GAnalytics(GDataReader):
  
      @Appender(_QUERY_DOC)
      def create_query(self, profile_id, metrics, start_date, end_date=None,
                       dimensions=None, segment=None, filters=None,
                       start_index=None, max_results=10000, **kwargs):
          qry = format_query(profile_id, metrics, start_date, end_date=end_date,
                             dimensions=dimensions, segment=segment,
                             filters=filters, start_index=start_index,
                             max_results=max_results, **kwargs)
          try:
              return self.service.data().ga().get(**qry)
          except TypeError as error:
              raise ValueError('Error making query: %s' % error)
  
  
  def format_query(ids, metrics, start_date, end_date=None, dimensions=None,
                   segment=None, filters=None, sort=None, start_index=None,
                   max_results=10000, **kwargs):
      if isinstance(metrics, compat.string_types):
          metrics = [metrics]
      met = ','.join(['ga:%s' % x for x in metrics])
  
      start_date = pd.to_datetime(start_date).strftime('%Y-%m-%d')
      if end_date is None:
          end_date = datetime.today()
      end_date = pd.to_datetime(end_date).strftime('%Y-%m-%d')
  
      qry = dict(ids='ga:%s' % str(ids),
                 metrics=met,
                 start_date=start_date,
                 end_date=end_date)
      qry.update(kwargs)
  
      names = ['dimensions', 'filters', 'sort']
      lst = [dimensions, filters, sort]
      [_maybe_add_arg(qry, n, d) for n, d in zip(names, lst)]
  
      if isinstance(segment, compat.string_types):
          if re.match("^[a-zA-Z0-9\-\_]+$", segment):
              _maybe_add_arg(qry, 'segment', segment, 'gaid:')
          else:
              _maybe_add_arg(qry, 'segment', segment, 'dynamic::ga')
      elif isinstance(segment, int):
          _maybe_add_arg(qry, 'segment', segment, 'gaid:')
      elif segment:
          raise ValueError("segment must be string for dynamic and int ID")
  
      if start_index is not None:
          qry['start_index'] = str(start_index)
  
      if max_results is not None:
          qry['max_results'] = str(max_results)
  
      return qry
  
  
  def _maybe_add_arg(query, field, data, prefix='ga'):
      if data is not None:
          if isinstance(data, (compat.string_types, int)):
              data = [data]
          data = ','.join(['%s:%s' % (prefix, x) for x in data])
          query[field] = data
  
  
  def _get_match(obj_store, name, id, **kwargs):
      key, val = None, None
      if len(kwargs) > 0:
          key = list(kwargs.keys())[0]
          val = list(kwargs.values())[0]
  
      if name is None and id is None and key is None:
          return obj_store.get('items')[0]
  
      name_ok = lambda item: name is not None and item.get('name') == name
      id_ok = lambda item: id is not None and item.get('id') == id
      key_ok = lambda item: key is not None and item.get(key) == val
  
      match = None
      if obj_store.get('items'):
          # TODO look up gapi for faster lookup
          for item in obj_store.get('items'):
              if name_ok(item) or id_ok(item) or key_ok(item):
                  return item
  
  
  def _clean_index(index_dims, parse_dates):
      _should_add = lambda lst: pd.Index(lst).isin(index_dims).all()
      to_remove = []
      to_add = []
  
      if isinstance(parse_dates, (list, tuple, np.ndarray)):
          for lst in parse_dates:
              if isinstance(lst, (list, tuple, np.ndarray)):
                  if _should_add(lst):
                      to_add.append('_'.join(lst))
                  to_remove.extend(lst)
      elif isinstance(parse_dates, dict):
          for name, lst in compat.iteritems(parse_dates):
              if isinstance(lst, (list, tuple, np.ndarray)):
                  if _should_add(lst):
                      to_add.append(name)
                  to_remove.extend(lst)
  
      index_dims = pd.Index(index_dims)
      to_remove = pd.Index(set(to_remove))
      to_add = pd.Index(set(to_add))
  
      return index_dims.difference(to_remove).union(to_add)
  
  
  def _get_col_names(header_info):
      return [x['name'][3:] for x in header_info]
  
  
  def _get_column_types(header_info):
      return [(x['name'][3:], x['columnType']) for x in header_info]
  
  
  def _get_dim_names(header_info):
      return [x['name'][3:] for x in header_info
              if x['columnType'] == u('DIMENSION')]
  
  
  def _get_met_names(header_info):
      return [x['name'][3:] for x in header_info
              if x['columnType'] == u('METRIC')]
  
  
  def _get_data_types(header_info):
      return [(x['name'][3:], TYPE_MAP.get(x['dataType'], object))
              for x in header_info]
  