Skip to content

Commit

Permalink
Initial commit of BigQuery admin integration
Browse files Browse the repository at this point in the history
  • Loading branch information
onejgordon committed Mar 28, 2017
1 parent 5a31d72 commit ad0468d
Show file tree
Hide file tree
Showing 20 changed files with 283 additions and 36 deletions.
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ lib/*
dist/*
secrets.py
src/js/constants/client_secrets.js
client_secret.json
client_secret.json
settings/*.json
.Python
env/*
bin/*
include/*
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ Create secrets.py, client_secrets.js from the templates.

### Run the dev server locally

To avoid conflicts sometimes seen with gcloud and google.cloud python libs it is often helpful to run the dev server in a virtualenv.

Make sure dev_appserver.py is in your path, and run `./server.sh` to start the dev server locally, and gulp to build JS etc.

### Deploy
Expand Down
16 changes: 10 additions & 6 deletions api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
try:
imp.find_module('secrets')
except ImportError:
import secrets_template as secrets
from settings import secrets_template as secrets
else:
import secrets
from settings import secrets


class ProjectAPI(handlers.JsonRequestHandler):
Expand Down Expand Up @@ -136,7 +136,7 @@ def recent(self, d):
habitdays = HabitDay.Range(self.user, habits, start_date)
self.set_response({
'habits': [habit.json() for habit in habits],
'habitdays': tools.lookupDict([hd for hd in habitdays if hd],
'habitdays': tools.lookupDict(habitdays,
keyprop="key_id",
valueTransform=lambda hd: hd.json())
})
Expand All @@ -152,7 +152,7 @@ def range(self, d):
habitdays = HabitDay.Range(self.user, habits, tools.fromISODate(start), until_date=tools.fromISODate(end))
self.set_response({
'habits': [habit.json() for habit in habits],
'habitdays': tools.lookupDict([hd for hd in habitdays if hd],
'habitdays': tools.lookupDict(habitdays,
keyprop="key_id",
valueTransform=lambda hd: hd.json())
}, success=True)
Expand Down Expand Up @@ -639,6 +639,7 @@ def google_login(self):
from constants import ADMIN_EMAIL
token = self.request.get('token')
ok, _email, name = self.validate_google_id_token(token)
u = None
if ok:
u = User.GetByEmail(_email)
if not u:
Expand Down Expand Up @@ -704,7 +705,7 @@ def google_oauth2_callback(self, d):
scope = self.request.get('scope')
# state_scopes = self.request.get('state')
if code:
from secrets import GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
from settings.secrets import GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
from constants import SECURE_BASE
base = 'http://localhost:8080' if tools.on_dev_server() else SECURE_BASE
credentials = client.credentials_from_code(
Expand All @@ -724,6 +725,7 @@ def google_oauth2_callback(self, d):
self.redirect("/app/integrations")

def validate_google_id_token(self, token):
from settings import secrets
success = False
email = name = None
g_response = urlfetch.fetch("https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=%s" % token)
Expand All @@ -735,6 +737,8 @@ def validate_google_id_token(self, token):
success = True
email = json_response.get("email", None)
name = json_response.get("name", None)
else:
logging.error("Client ID mismatch")
return (success, email, name)

def fbook_auth(self):
Expand Down Expand Up @@ -808,7 +812,7 @@ def get(self, d):
'goals': [g.json() for g in goals],
'tasks': [t.json() for t in tasks],
'tracking_days': [p.json() for p in tracking_days],
'habitdays': tools.lookupDict([hd for hd in habitdays if hd],
'habitdays': tools.lookupDict(habitdays,
keyprop="key_id",
valueTransform=lambda hd: hd.json())

Expand Down
3 changes: 3 additions & 0 deletions app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ libraries:
version: "2.3.5"
- name: pycrypto
version: "2.6"

env_variables:
GOOGLE_APPLICATION_CREDENTIALS: 'settings/genzai-app-f02b0e933bb4.json'
4 changes: 2 additions & 2 deletions flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
try:
imp.find_module('secrets')
except ImportError:
import secrets_template as secrets
from settings import secrets_template as secrets
else:
import secrets
from settings import secrets

SECS_PER_WEEK = 60 * 60 * 24 * 7
# Enable ctypes -> Jinja2 tracebacks
Expand Down
16 changes: 11 additions & 5 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
try:
imp.find_module('secrets')
except ImportError:
import secrets_template as secrets
from settings import secrets_template as secrets
else:
import secrets
from settings import secrets


class UserAccessible(ndb.Model):
Expand Down Expand Up @@ -72,6 +72,7 @@ def json(self, is_self=False):
'id': self.key.id(),
'name': self.name,
'email': self.email,
'level': self.level,
'integrations': tools.getJson(self.integrations),
'settings': tools.getJson(self.settings, {}),
'timezone': self.timezone,
Expand Down Expand Up @@ -383,13 +384,16 @@ def json(self):
'icon': self.icon
}

def slug_name(self):
return tools.strip_symbols(self.name).lower().strip()

@staticmethod
def All(user):
return Habit.query(ancestor=user.key).fetch(limit=20)

@staticmethod
def Active(user):
return Habit.query(ancestor=user.key).filter(Habit.archived == False).fetch(limit=5)
return Habit.query(ancestor=user.key).filter(Habit.archived == False).fetch(limit=8)

@staticmethod
def Create(user):
Expand Down Expand Up @@ -452,7 +456,7 @@ def Range(user, habits, since_date, until_date=None):
ids.append(ndb.Key('HabitDay', HabitDay.ID(h, cursor), parent=user.key))
cursor += timedelta(days=1)
if ids:
return ndb.get_multi(ids)
return [hd for hd in ndb.get_multi(ids) if hd]
return []

@staticmethod
Expand Down Expand Up @@ -863,7 +867,7 @@ def json(self):

@staticmethod
def Fetch(user, favorites=False, with_notes=False, unread=False, read=False,
limit=30, since=None, offset=0, keys_only=False):
limit=30, since=None, until=None, offset=0, keys_only=False):
q = Readable.query(ancestor=user.key)
ordering_prop = Readable.dt_added if not read else Readable.dt_read
if with_notes:
Expand All @@ -877,6 +881,8 @@ def Fetch(user, favorites=False, with_notes=False, unread=False, read=False,
q = q.order(-ordering_prop)
if since:
q = q.filter(ordering_prop >= tools.fromISODate(since))
if until:
q = q.filter(ordering_prop <= tools.fromISODate(until))
return q.fetch(limit=limit, offset=offset, keys_only=keys_only)

@staticmethod
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ beautifulsoup4
GoogleAppEngineCloudStorageClient==1.9.22.1
evernote==1.25.2
oauth2client
google-cloud-bigquery==0.23.0
google_api_python_client==1.5.1
4 changes: 2 additions & 2 deletions services/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
try:
imp.find_module('secrets')
except ImportError:
import secrets_template as secrets
from settings import secrets_template as secrets
else:
import secrets
from settings import secrets

AGENT_GOOGLE_ASST = 1
AGENT_FBOOK_MESSENGER = 2
Expand Down
154 changes: 154 additions & 0 deletions services/flow_bigquery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# API calls to interact with Google Big Query

# TODO:
# - User authentication for own bigquery dataset access (otherwise datasets created in flow account?)

from __future__ import absolute_import
from datetime import datetime, timedelta, time
from oauth2client.client import GoogleCredentials
from google.cloud import bigquery
from google.cloud.bigquery.schema import SchemaField
from models import Habit, HabitDay, Task, Readable
import logging
import tools


class BigQueryClient(object):

def __init__(self, user):
self.user = user
# self.credentials = GoogleCredentials.get_application_default()
self.client = bigquery.Client()
self.dataset_name = self.user.get_integration_prop('bigquery_dataset_name')
self.table_name = self.user.get_integration_prop('bigquery_table_name')
self.dataset = self.client.dataset(self.dataset_name)
self.table = None
self.habits = None
self.journal_questions = None

def _maybe_get_habits(self):
if self.habits is None:
self.habits = Habit.Active(self.user)

def _maybe_get_journal_questions(self):
if self.journal_questions is None:
self.journal_questions = tools.getJson(self.user.settings, {}).get('journals', {}).get('questions', [])

def _bq_schema(self):
# Genreate bigquery schema from user
common_fields = [
SchemaField("date", "DATE", mode="REQUIRED"),
SchemaField("tasks_done", "INT64", mode="REQUIRED"),
SchemaField("tasks_undone", "INT64", mode="REQUIRED"),
SchemaField("habits_done", "INT64", mode="REQUIRED"),
SchemaField("habits_cmt", "INT64", mode="REQUIRED"),
SchemaField("habits_cmt_undone", "INT64", mode="REQUIRED",
description="Habits committed but not completed"),
SchemaField("items_read", "INT64", mode="REQUIRED"),
]
self._maybe_get_habits()
self._maybe_get_journal_questions()
user_fields = []
for h in self.habits:
user_fields.append(SchemaField("habit_%s" % h.slug_name(),
"BOOL",
mode="REQUIRED",
description="Habit done - %s" % h.name))
for q in self.journal_questions:
name = q.get('name')
label = q.get('label')
response_type = q.get('response_type')
if response_type in ['slider', 'number']:
user_fields.append(
SchemaField(name, "INT64",
mode="NULLABLE",
description="Journal response: %s" % label))
schema = common_fields + user_fields
# TODO: How do schema changes work?
return schema

def table_created(self):
table = self.dataset.table(self.table_name)
return table.exists(client=self.client)

def create_table(self):
logging.debug("Creating table...")
self.table = self.dataset.table(self.table_name, self._bq_schema())
self.table.create(client=self.client)

def fetch_daily_panel_data(self, since=None, until=None):
self._maybe_get_habits()
self._maybe_get_journal_questions()
if not since:
since = datetime.combine((datetime.now() - timedelta(days=8)).date(), time(0, 0))
if not until:
until = datetime.combine((datetime.now() - timedelta(days=1)).date(), time(0, 0))
rows = []
row_ids = []
habitdays_by_day = tools.partition(
HabitDay.Range(self.user, self.habits, since, until_date=until),
lambda hd: tools.iso_date(hd.date)
)
tasks_by_day = tools.partition(
Task.DueInRange(self.user, since, until, limit=500),
lambda t: tools.iso_date(t.dt_due)
)
readables_by_day = tools.partition(
Readable.Fetch(self.user, read=True,
since=tools.iso_date(since),
until=tools.iso_date(until)),
lambda r: tools.iso_date(r.dt_read)
)
cursor = since
while cursor <= until:
iso_date = tools.iso_date(cursor)
tasks = tasks_by_day.get(iso_date, [])
habits = habitdays_by_day.get(iso_date, [])
readables = readables_by_day.get(iso_date, [])
tasks_done = tasks_undone = habits_done = habits_cmt = habits_cmt_undone = items_read = 0
for t in tasks:
if t.is_done():
tasks_done += 1
else:
tasks_undone += 1
for hd in habits:
if hd.done:
habits_done += 1
if hd.committed:
habits_cmt += 1
if not hd.done:
habits_cmt_undone += 1
items_read = len(readables)
row = [
iso_date,
tasks_done,
tasks_undone,
habits_done,
habits_cmt,
habits_cmt_undone,
items_read
]
for h in self.habits:
this_habit_done = 0
row.append('true' if this_habit_done else 'false')
rows.append(tuple(row))
row_ids.append(iso_date)
cursor += timedelta(days=1)
return (rows, row_ids)

def push_data(self, rows, row_ids):
logging.debug("Inserting into table '%s' with %d rows" % (self.table.friendly_name, self.table.num_rows))
self.table.insert_data(rows, row_ids=row_ids,
skip_invalid_rows=True,
client=self.client)

def run(self):
if not self.table_created():
self.create_table()
rows, row_ids = self.fetch_daily_panel_data()
self.push_data(rows, row_ids)


8 changes: 4 additions & 4 deletions services/flow_evernote.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@


def user_access_token(user):
from secrets import EVERNOTE_DEV_TOKEN
from settings.secrets import EVERNOTE_DEV_TOKEN
if USE_DEV_TOKEN:
access_token = EVERNOTE_DEV_TOKEN
else:
Expand All @@ -28,7 +28,7 @@ def get_request_token(user, callback):
'''
Get request token
'''
from secrets import EVERNOTE_CONSUMER_KEY, EVERNOTE_CONSUMER_SECRET
from settings.secrets import EVERNOTE_CONSUMER_KEY, EVERNOTE_CONSUMER_SECRET
client = EvernoteClient(
consumer_key=EVERNOTE_CONSUMER_KEY,
consumer_secret=EVERNOTE_CONSUMER_SECRET,
Expand All @@ -45,7 +45,7 @@ def get_access_token(user, oauth_token, oauth_token_secret, oauth_verifier):
'''
Get request token
'''
from secrets import EVERNOTE_CONSUMER_KEY, EVERNOTE_CONSUMER_SECRET
from settings.secrets import EVERNOTE_CONSUMER_KEY, EVERNOTE_CONSUMER_SECRET
client = EvernoteClient(
consumer_key=EVERNOTE_CONSUMER_KEY,
consumer_secret=EVERNOTE_CONSUMER_SECRET,
Expand Down Expand Up @@ -91,7 +91,7 @@ def get_note(user, note_id):
return (title, content)

if __name__ == "__main__":
from secrets import EVERNOTE_DEV_TOKEN
from settings.secrets import EVERNOTE_DEV_TOKEN
client = EvernoteClient(token=EVERNOTE_DEV_TOKEN)
noteStore = client.get_note_store()
print noteStore.getNote("x")
2 changes: 1 addition & 1 deletion services/goodreads.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from secrets import GR_API_KEY
from settings.secrets import GR_API_KEY
from google.appengine.api import urlfetch
from google.appengine.ext import ndb
from lxml import etree
Expand Down

0 comments on commit ad0468d

Please sign in to comment.