├── .coveragerc ├── .gitignore ├── Makefile ├── README.rst ├── docs ├── Makefile └── source │ ├── api.rst │ ├── conf.py │ └── index.rst ├── requirements.txt ├── runnerly ├── __init__.py └── dataservice │ ├── __init__.py │ ├── app.py │ ├── database.py │ ├── run.py │ ├── settings.ini │ ├── static │ └── api.yaml │ ├── tests │ ├── __init__.py │ ├── privkey.pem │ ├── pubkey.pem │ └── test_views.py │ └── views │ ├── __init__.py │ ├── home.py │ └── swagger.py ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = dataservice/tests/*, .tox/* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.un~ 2 | __pycache__ 3 | bin 4 | include 5 | lib 6 | *.egg-info 7 | .Python 8 | .cache 9 | .coverage 10 | .tox 11 | docs/build 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | HERE = $(shell pwd) 2 | VENV = . 3 | VIRTUALENV = virtualenv 4 | BIN = $(VENV)/bin 5 | PYTHON = $(BIN)/python 6 | 7 | INSTALL = $(BIN)/pip install --no-deps 8 | 9 | .PHONY: all test docs build_extras 10 | 11 | all: build 12 | 13 | $(PYTHON): 14 | $(VIRTUALENV) $(VTENV_OPTS) $(VENV) 15 | 16 | build: $(PYTHON) 17 | $(PYTHON) setup.py develop 18 | 19 | clean: 20 | rm -rf $(VENV) 21 | 22 | test_dependencies: 23 | $(BIN)/pip install flake8 tox 24 | 25 | test: build test_dependencies 26 | $(BIN)/tox 27 | 28 | run: 29 | FLASK_APP=dataservice/app.py bin/flask run 30 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | DataService 2 | =========== 3 | 4 | **DISCLAIMER** This repository is part of Runnerly, an application made for 5 | the Python Microservices Development. It was made for educational 6 | purpose and not suitable for production. It's still being updated. 7 | If you find any issue or want to talk with the author, feel free to 8 | open an issue in the issue tracker. 9 | 10 | Microservice that holds the Runs and Users 11 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = myservice 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | APIS 2 | ==== 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # myservice documentation build configuration file, created by 5 | # sphinx-quickstart on Mon Jan 16 16:26:46 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.autodoc'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = 'TokenDealer' 50 | copyright = '2017, Tarek Ziadé' 51 | author = 'Tarek Ziadé' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '1.0' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '1.0' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This patterns also effect to html_static_path and html_extra_path 72 | exclude_patterns = [] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = 'sphinx' 76 | 77 | # If true, `todo` and `todoList` produce output, else they produce nothing. 78 | todo_include_todos = False 79 | 80 | 81 | # -- Options for HTML output ---------------------------------------------- 82 | 83 | # The theme to use for HTML and HTML Help pages. See the documentation for 84 | # a list of builtin themes. 85 | # 86 | html_theme = 'alabaster' 87 | 88 | # Theme options are theme-specific and customize the look and feel of a theme 89 | # further. For a list of options available for each theme, see the 90 | # documentation. 91 | # 92 | # html_theme_options = {} 93 | 94 | # Add any paths that contain custom static files (such as style sheets) here, 95 | # relative to this directory. They are copied after the builtin static files, 96 | # so a file named "default.css" will overwrite the builtin "default.css". 97 | html_static_path = ['_static'] 98 | 99 | 100 | # -- Options for HTMLHelp output ------------------------------------------ 101 | 102 | # Output file base name for HTML help builder. 103 | htmlhelp_basename = 'myservicedoc' 104 | 105 | 106 | # -- Options for LaTeX output --------------------------------------------- 107 | 108 | latex_elements = { 109 | # The paper size ('letterpaper' or 'a4paper'). 110 | # 111 | # 'papersize': 'letterpaper', 112 | 113 | # The font size ('10pt', '11pt' or '12pt'). 114 | # 115 | # 'pointsize': '10pt', 116 | 117 | # Additional stuff for the LaTeX preamble. 118 | # 119 | # 'preamble': '', 120 | 121 | # Latex figure (float) alignment 122 | # 123 | # 'figure_align': 'htbp', 124 | } 125 | 126 | # Grouping the document tree into LaTeX files. List of tuples 127 | # (source start file, target name, title, 128 | # author, documentclass [howto, manual, or own class]). 129 | latex_documents = [ 130 | (master_doc, 'myservice.tex', 'myservice Documentation', 131 | 'joe', 'manual'), 132 | ] 133 | 134 | 135 | # -- Options for manual page output --------------------------------------- 136 | 137 | # One entry per manual page. List of tuples 138 | # (source start file, name, description, authors, manual section). 139 | man_pages = [ 140 | (master_doc, 'myservice', 'myservice Documentation', 141 | [author], 1) 142 | ] 143 | 144 | 145 | # -- Options for Texinfo output ------------------------------------------- 146 | 147 | # Grouping the document tree into Texinfo files. List of tuples 148 | # (source start file, target name, title, author, 149 | # dir menu entry, description, category) 150 | texinfo_documents = [ 151 | (master_doc, 'myservice', 'myservice Documentation', 152 | author, 'myservice', 'One line description of project.', 153 | 'Miscellaneous'), 154 | ] 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | DataService 3 | =========== 4 | 5 | The **DataService** microservice manages Runs and Users 6 | 7 | 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | api 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyjwt 2 | -e git+https://github.com/Runnerly/flakon.git#egg=flakon 3 | flask_webtest 4 | cryptography 5 | sqlalchemy 6 | flask_sqlalchemy 7 | chaussette 8 | -------------------------------------------------------------------------------- /runnerly/__init__.py: -------------------------------------------------------------------------------- 1 | from pkgutil import extend_path 2 | __path__ = extend_path(__path__, __name__) 3 | -------------------------------------------------------------------------------- /runnerly/dataservice/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '0.1' 3 | -------------------------------------------------------------------------------- /runnerly/dataservice/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from werkzeug.exceptions import HTTPException 3 | from flakon import create_app as _create_app 4 | from flakon.util import error_handling 5 | from flask import request, abort, g 6 | from flask_cors import CORS 7 | 8 | import jwt 9 | 10 | from .views import blueprints 11 | from .database import db 12 | 13 | 14 | _HERE = os.path.dirname(__file__) 15 | os.environ['TESTDIR'] = os.path.join(_HERE, 'tests') 16 | _SETTINGS = os.path.join(_HERE, 'settings.ini') 17 | 18 | 19 | def create_app(settings=None): 20 | if settings is None: 21 | settings = _SETTINGS 22 | 23 | app = _create_app(blueprints=blueprints, settings=settings) 24 | 25 | with open(app.config['pub_key']) as f: 26 | app.config['pub_key'] = f.read() 27 | 28 | CORS(app) 29 | 30 | @app.before_request 31 | def before_req(): 32 | if app.config.get('NEED_TOKEN', True): 33 | authenticate(app, request) 34 | 35 | return app 36 | 37 | 38 | def _400(desc): 39 | exc = HTTPException() 40 | exc.code = 400 41 | exc.description = desc 42 | return error_handling(exc) 43 | 44 | 45 | def authenticate(app, request): 46 | key = request.headers.get('Authorization') 47 | if key is None: 48 | return abort(401) 49 | 50 | key = key.split(' ') 51 | if len(key) != 2: 52 | return abort(401) 53 | 54 | if key[0].lower() != 'bearer': 55 | return abort(401) 56 | 57 | pub_key = app.config['pub_key'] 58 | try: 59 | token = key[1] 60 | token = jwt.decode(token, pub_key, audience='runnerly.io') 61 | except Exception as e: 62 | return abort(401) 63 | 64 | # we have the token ~ copied into the globals 65 | g.jwt_token = token 66 | -------------------------------------------------------------------------------- /runnerly/dataservice/database.py: -------------------------------------------------------------------------------- 1 | # encoding: utf8 2 | import os 3 | from datetime import datetime 4 | from decimal import Decimal 5 | from sqlalchemy.orm import relationship 6 | from flask_sqlalchemy import SQLAlchemy 7 | 8 | 9 | db = SQLAlchemy() 10 | 11 | 12 | class User(db.Model): 13 | __tablename__ = 'user' 14 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 15 | email = db.Column(db.Unicode(128), nullable=False) 16 | firstname = db.Column(db.Unicode(128)) 17 | lastname = db.Column(db.Unicode(128)) 18 | password = db.Column(db.Unicode(128)) 19 | strava_token = db.Column(db.String(128)) 20 | age = db.Column(db.Integer) 21 | weight = db.Column(db.Numeric(4, 1)) 22 | max_hr = db.Column(db.Integer) 23 | rest_hr = db.Column(db.Integer) 24 | vo2max = db.Column(db.Numeric(4, 2)) 25 | is_active = db.Column(db.Boolean, default=True) 26 | is_anonymous = False 27 | 28 | def to_json(self, secure=False): 29 | res = {} 30 | for attr in ('id', 'email', 'firstname', 'lastname', 'age', 'weight', 31 | 'max_hr', 'rest_hr', 'vo2max'): 32 | value = getattr(self, attr) 33 | if isinstance(value, Decimal): 34 | value = float(value) 35 | res[attr] = value 36 | if secure: 37 | res['strava_token'] = self.strava_token 38 | return res 39 | 40 | def get_id(self): 41 | return self.id 42 | 43 | 44 | class Run(db.Model): 45 | __tablename__ = 'run' 46 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 47 | title = db.Column(db.Unicode(128)) 48 | description = db.Column(db.Unicode(512)) 49 | strava_id = db.Column(db.Integer) 50 | distance = db.Column(db.Float) 51 | start_date = db.Column(db.DateTime) 52 | elapsed_time = db.Column(db.Integer) 53 | average_speed = db.Column(db.Float) 54 | average_heartrate = db.Column(db.Float) 55 | total_elevation_gain = db.Column(db.Float) 56 | runner_id = db.Column(db.Integer, db.ForeignKey('user.id')) 57 | runner = relationship('User', foreign_keys='Run.runner_id') 58 | 59 | def to_json(self): 60 | res = {} 61 | for attr in ('id', 'strava_id', 'distance', 'start_date', 62 | 'elapsed_time', 'average_speed', 'average_heartrate', 63 | 'total_elevation_gain', 'runner_id', 'title', 64 | 'description'): 65 | value = getattr(self, attr) 66 | if isinstance(value, datetime): 67 | value = value.timestamp() 68 | res[attr] = value 69 | return res 70 | 71 | 72 | def init_database(): 73 | exists = db.session.query(User).filter(User.email == 'tarek@ziade.org') 74 | if exists.all() != []: 75 | return 76 | 77 | tarek = User() 78 | tarek.email = 'tarek@ziade.org' 79 | tarek.firstname = 'Tarek' 80 | tarek.lastname = 'Ziadé' 81 | tarek.age = 40 82 | tarek.weight = 58 83 | tarek.max_hr = 192 84 | tarek.rest_hr = 47 85 | tarek.vo2max = 63 86 | tarek.strava_token = os.environ.get('STRAVA_TOKEN') 87 | db.session.add(tarek) 88 | db.session.commit() 89 | -------------------------------------------------------------------------------- /runnerly/dataservice/run.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | import signal 4 | 5 | from chaussette.server import make_server 6 | from werkzeug.serving import run_with_reloader 7 | 8 | from runnerly.dataservice.app import create_app 9 | from runnerly.dataservice.database import db, init_database 10 | 11 | 12 | def _quit(signal, frame): 13 | print("Bye!") 14 | # add any cleanup code here 15 | sys.exit(0) 16 | 17 | 18 | def main(args=sys.argv[1:]): 19 | parser = argparse.ArgumentParser(description='Runnerly Dataservice') 20 | 21 | parser.add_argument('--fd', type=int, default=None) 22 | parser.add_argument('--config-file', help='Config file', 23 | type=str, default=None) 24 | args = parser.parse_args(args=args) 25 | 26 | app = create_app(args.config_file) 27 | host = app.config.get('host', '0.0.0.0') 28 | port = app.config.get('port', 5000) 29 | debug = app.config.get('DEBUG', False) 30 | 31 | signal.signal(signal.SIGINT, _quit) 32 | signal.signal(signal.SIGTERM, _quit) 33 | 34 | db.init_app(app) 35 | db.app = app 36 | db.create_all(app=app) 37 | init_database() 38 | 39 | if args.fd is not None: 40 | # use chaussette 41 | httpd = make_server(app, host='fd://%d' % args.fd) 42 | httpd.serve_forever() 43 | else: 44 | app.run(debug=debug, host=host, port=port, use_reloader=debug) 45 | 46 | 47 | if __name__ == "__main__": 48 | main() 49 | -------------------------------------------------------------------------------- /runnerly/dataservice/settings.ini: -------------------------------------------------------------------------------- 1 | [flask] 2 | DEBUG = 1 3 | SQLALCHEMY_TRACK_MODIFICATIONS = False 4 | SQLALCHEMY_DATABASE_URI = sqlite:////tmp/runnerly.dataservice.db 5 | NEED_TOKEN = False 6 | pub_key = ${TESTDIR}/pubkey.pem 7 | host = 127.0.0.1 8 | port = 5002 9 | -------------------------------------------------------------------------------- /runnerly/dataservice/static/api.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: Runnerly Data Service 4 | description: returns info about Runnerly 5 | license: 6 | name: APLv2 7 | url: https://www.apache.org/licenses/LICENSE-2.0.html 8 | version: 0.1.0 9 | basePath: /api 10 | paths: 11 | /runs/{runner_id}/{year}/{month}: 12 | get: 13 | operationId: getRuns 14 | description: Get Runs 15 | produces: 16 | - application/json 17 | parameters: 18 | - name: runner_id 19 | in: path 20 | description: ID of Runner 21 | required: true 22 | type: integer 23 | - name: year 24 | in: path 25 | description: Year of runs to return 26 | required: true 27 | type: integer 28 | - name: month 29 | in: path 30 | description: Months of runs to return 31 | required: true 32 | type: integer 33 | responses: 34 | '200': 35 | description: List of runs 36 | /add_runs: 37 | post: 38 | operationId: addRuns 39 | description: Adds runs 40 | produces: 41 | - application/json 42 | responses: 43 | '200': 44 | description: List of runs ids 45 | schema: 46 | type: array 47 | items: 48 | type: integer 49 | /users: 50 | get: 51 | operationId: getUsers 52 | description: Returns a list of users 53 | produces: 54 | - application/json 55 | responses: 56 | '200': 57 | description: List of Ids 58 | schema: 59 | type: array 60 | items: 61 | type: integer 62 | -------------------------------------------------------------------------------- /runnerly/dataservice/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /runnerly/dataservice/tests/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /runnerly/dataservice/tests/pubkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApl1MD4WA3OGTpy977Xbe 3 | j1Gyf4DjfdYL9dgnj+lGWRS+P/yuzWWS9NvfAUa1oiAmWkdy5hCJ5tDMfn1NoJt9 4 | 1eLQ/qO3HRlkHBqasjcH2zt1AnQ4tRTD4saunQ1JHGnDRreh+qwV+TYBNd4MiFqs 5 | Lw6xoZAcnKq1xGqtPE88q9hzoaPaolfg4ywwtbuSkuS8shnp+3g/G4fmdO93yO6z 6 | CjqApSnph9Rd+AcaVpVpgofy7qnq9CEi20HKOOnT9lJz4uqzy2HBgv/IAzJtSDQa 7 | n0lcT0TnlS8WedfJoDhKgDohCMoq70Tnl53E2zC43gh0R8vCZroCUlzT5A9ckuaV 8 | j7zDt80RkFbt3RR/3dvJ09uMhG1OA9NgU2jhOkF5NBUkfdVxRwJAQbP2hi0vymhp 9 | yFgPtEE0a/dBqNUq1q/ZvSJCXSFjfHDbHHno1AZuJlcQ7+BzWJ6fJQb4ibpDSpH7 10 | z7L+KAb91MQ3mnrvl66z9BKD8D6ZAd1iB0DFzN6hL/CHTXHJoUxHslXNaHNuwVHd 11 | Aafiq4ibOU/7V7JAIupIcOPvWZLZVUMwjAn+dKcefHg0Ny2g2WRCcXk1eTy30XwM 12 | BLQIOa/GgwksioFbT/2VW+KkOMPKXdWK/q7UpBqZAeOLTAw9tp4hq5MrL9UrCt9T 13 | TcZkBAgdalem6Z8YfljenNsCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /runnerly/dataservice/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import jwt 4 | from dataservice.app import app 5 | from flask_webtest import TestApp as _TestApp 6 | 7 | 8 | _HERE = os.path.dirname(__file__) 9 | with open(os.path.join(_HERE, 'privkey.pem')) as f: 10 | _KEY = f.read() 11 | 12 | 13 | def create_token(data): 14 | return jwt.encode(data, _KEY, algorithm='RS512') 15 | 16 | 17 | _TOKEN = {'iss': 'runnerly', 18 | 'aud': 'runnerly.io'} 19 | 20 | 21 | class TestViews(unittest.TestCase): 22 | def setUp(self): 23 | self.app = _TestApp(app) 24 | self.token = create_token(_TOKEN).decode('ascii') 25 | self.headers = {'Authorization': 'Bearer ' + self.token} 26 | 27 | def test_one(self): 28 | resp = self.app.get('/', headers=self.headers) 29 | self.assertEqual(resp.status_code, 200) 30 | -------------------------------------------------------------------------------- /runnerly/dataservice/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .home import home 2 | from .swagger import api 3 | 4 | blueprints = [home, api] 5 | -------------------------------------------------------------------------------- /runnerly/dataservice/views/home.py: -------------------------------------------------------------------------------- 1 | from flakon import JsonBlueprint 2 | 3 | 4 | home = JsonBlueprint('api', __name__) 5 | 6 | 7 | @home.route('/') 8 | def some(): 9 | return {'here': 1} 10 | -------------------------------------------------------------------------------- /runnerly/dataservice/views/swagger.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | 4 | from flakon import SwaggerBlueprint 5 | from flask import request, jsonify 6 | from runnerly.dataservice.database import db, User, Run 7 | 8 | 9 | HERE = os.path.dirname(__file__) 10 | YML = os.path.join(HERE, '..', 'static', 'api.yaml') 11 | api = SwaggerBlueprint('API', __name__, swagger_spec=YML) 12 | 13 | 14 | @api.operation('addRuns') 15 | def add_runs(): 16 | # each run has the user id, and the actual run data 17 | # it's controlled & pushed in the DB 18 | # XXX controls 19 | # dupes 20 | added = 0 21 | for user, runs in request.json.items(): 22 | runner_id = int(user) 23 | for run in runs: 24 | db_run = Run() 25 | db_run.strava_id = run['strava_id'] 26 | db_run.distance = run['distance'] 27 | db_run.start_date = datetime.fromtimestamp(run['start_date']) 28 | db_run.elapsed_time = run['elapsed_time'] 29 | db_run.average_speed = run['average_speed'] 30 | db_run.average_heartrate = run['average_heartrate'] 31 | db_run.total_elevation_gain = run['total_elevation_gain'] 32 | db_run.runner_id = runner_id 33 | db_run.title = run['title'] 34 | db_run.description = run['description'] 35 | db.session.add(db_run) 36 | 37 | added += 1 38 | 39 | if added > 0: 40 | db.session.commit() 41 | 42 | return {'added': 1} 43 | 44 | 45 | @api.operation('getRuns') 46 | def get_runs(runner_id, year, month): 47 | runs = db.session.query(Run).filter(Run.runner_id == runner_id) 48 | return jsonify([run.to_json() for run in runs]) 49 | 50 | 51 | @api.operation('getUsers') 52 | def get_users(): 53 | users = db.session.query(User) 54 | page = 0 55 | page_size = None 56 | if page_size: 57 | users = users.limit(page_size) 58 | if page != 0: 59 | users = users.offset(page * page_size) 60 | return {'users': [user.to_json(secure=True) for user in users]} 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from runnerly.dataservice import __version__ 3 | 4 | 5 | setup(name='runnerly-data', 6 | version=__version__, 7 | packages=find_packages(), 8 | include_package_data=True, 9 | zip_safe=False, 10 | entry_points=""" 11 | [console_scripts] 12 | runnerly-dataservice = runnerly.dataservice.run:main 13 | """) 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35,flake8,docs 3 | 4 | [testenv] 5 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 6 | deps = pytest 7 | pytest-cov 8 | coveralls 9 | -rrequirements.txt 10 | 11 | commands = 12 | pytest --cov-config .coveragerc dataservice/tests --cov 13 | - coveralls 14 | 15 | 16 | [testenv:flake8] 17 | commands = flake8 dataservice 18 | deps = 19 | flake8 20 | 21 | [testenv:docs] 22 | deps = 23 | -rrequirements.txt 24 | sphinx 25 | commands= 26 | sphinx-build -W -b html docs/source docs/build 27 | --------------------------------------------------------------------------------