Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
perewall committed Sep 28, 2018
0 parents commit c1587c9
Show file tree
Hide file tree
Showing 22 changed files with 1,188 additions and 0 deletions.
105 changes: 105 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/
html/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
8 changes: 8 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
language: python
python:
- "3.6"
install:
- pip install -r requirements.txt
script:
- python -m unittest discover
cache: pip
2 changes: 2 additions & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web: gunicorn -b 0.0.0.0:$PORT 'app:create_app()'
worker: celery worker -A app.tasks -B --scheduler redbeat.RedBeatScheduler -l info
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## PushResume

[![Build Status](https://travis-ci.org/pushresume/backend.svg?branch=master)](https://travis-ci.org/pushresume/backend)
11 changes: 11 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "PushResume",
"repository": "https://github.com/pushresume/backend",
"scripts": {
"postdeploy": "from app import db, create_app; app=create_app(); app.app_context().push(); db.create_all()'"
},
"addons": [
"heroku-postgresql:hobby-dev",
"heroku-redis:hobby-dev"
]
}
75 changes: 75 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from logging import getLogger
from datetime import timedelta
from importlib import import_module

from redis import Redis
from celery import Celery
from flask import Flask, jsonify, abort
from flask_cors import CORS
from flask_caching import Cache
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager
from werkzeug.contrib.fixers import ProxyFix
from werkzeug.exceptions import HTTPException


__version__ = '0.1.0'

db = SQLAlchemy()
cache = Cache()


def create_app():
app = Flask(__name__)
app.config.from_object('config')

app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta( # must be timedelta,
minutes=int(app.config['JWT_ACCESS_TOKEN_EXPIRES'])) # must be here

external_logger = getLogger('gunicorn.error')
if len(external_logger.handlers) > 0:
app.logger.setLevel(external_logger.level)
app.logger.handlers = external_logger.handlers

db.init_app(app)
cache.init_app(app)
CORS(app, resources={r'/*': {'origins': app.config['FRONTEND_URL']}})
JWTManager(app)

app.wsgi_app = ProxyFix(app.wsgi_app)
app.redis = Redis.from_url(app.config['REDIS_URL'])
app.queue = Celery(
'pushresume',
backend=app.config['REDIS_URL'],
broker=app.config['REDIS_URL'])

@app.errorhandler(Exception)
def error_handler(e):
if not isinstance(e, HTTPException):
if app.debug:
raise e
app.logger.critical(e, exc_info=1)
return abort(500, type(e).__name__)

msg = {'status': e.code, 'message': e.description, 'error': e.name}
if e.code == 405:
msg.update({'allowed': e.valid_methods})
return jsonify(msg), e.code

app.providers = {}
for prov in app.config['PROVIDERS']:
try:
mod = import_module(f'app.providers.{prov}')
back_url = f'{app.config["FRONTEND_URL"]}/auth/{prov}'
app.providers[prov] = mod.Provider(
name=prov, redirect_uri=back_url, **app.config[prov.upper()])
app.logger.info(f'Provider [{prov}] loaded')
except Exception as e:
app.logger.warn(f'Provider [{prov}] load failed: {e}', exc_info=1)

from .views import module
app.register_blueprint(module)

app.logger.info(f'PushResume {__version__} startup')

return app
91 changes: 91 additions & 0 deletions app/controllers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from datetime import datetime, timedelta

from flask import current_app

from . import db
from .models import User, Resume


class UserController(object):
"""User Controller"""

def __init__(self, provider):
self._provider = provider

def auth(self, code, refresh=False):
ids = self._provider.tokenize(code, refresh=refresh)
identity = self._provider.identity(ids['access_token'])

user = User.query.filter_by(
uniq=identity, provider=self._provider.name).first()
if not user:
user = User(uniq=identity, provider=self._provider.name)

user.access = ids['access_token']
user.refresh = ids['refresh_token']
user.expires = datetime.utcnow() + timedelta(seconds=ids['expires_in'])

db.session.add(user)
db.session.commit()
return user


class ResumeController(object):
"""Resume Controller"""

@classmethod
def fetch(self, user_id):
user = User.query.get(user_id)
provider = current_app.providers[user.provider]
resumes = provider.fetch(user.access)

for i in resumes:
resume = Resume.query.filter_by(uniq=i['uniq'], owner=user).first()
if not resume:
resume = Resume(uniq=i['uniq'], enabled=False, owner=user)
current_app.logger.info(f'Resume created: {resume}')
db.session.add(resume)

i['enabled'] = resume.enabled

db.session.commit()
return resumes

@classmethod
def toggle(self, user_id, uniq):
user = User.query.get(user_id)
resume = Resume.query.filter_by(uniq=uniq, owner=user).first()
if resume:
resume.enabled = not resume.enabled
db.session.add(resume)
db.session.commit()

return resume


class StatsController(object):
"""Statistics Controller"""

@classmethod
def stats(self):
users = User.query.count()
resume = Resume.query.count()
redis = current_app.redis.info('memory')

result = {
'db': {
'rows': {'total': users + resume, 'max': 10000},
'memory': {'total': redis['used_memory'], 'max': 25000000}
},
'users': {'items': [], 'total': users},
'resume': {'items': [], 'total': resume}
}

ResumeUser = Resume.query.join(User)
for prov in current_app.providers.keys():
result['users']['items'].append(
{prov: User.query.filter_by(provider=prov).count()})
result['resume']['items'].append(
{prov: ResumeUser.filter(User.provider == prov).count()})

return result
36 changes: 36 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from datetime import datetime

from . import db


class User(db.Model):

__tablename__ = 'users'
__table_args__ = (db.Index('uniq', 'uniq', 'provider', unique=True),)

id = db.Column(db.Integer, primary_key=True)
uniq = db.Column(db.String(120), nullable=False)
provider = db.Column(db.String(120), nullable=False)
access = db.Column(db.String(200), nullable=False)
refresh = db.Column(db.String(200), nullable=False)
expires = db.Column(db.DateTime, nullable=False)
updated = db.Column(db.DateTime, default=datetime.utcnow)

resume = db.relationship(
'Resume', foreign_keys='Resume.user_id', backref='owner')

def __str__(self):
return f'{self.uniq}, provider={self.provider}'


class Resume(db.Model):

__tablename__ = 'resume'

id = db.Column(db.Integer, primary_key=True)
uniq = db.Column(db.String(120), unique=True, nullable=False)
enabled = db.Column(db.Boolean, default=False, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)

def __str__(self):
return f'{self.uniq}, enabled={self.enabled}, user={self.owner}'
50 changes: 50 additions & 0 deletions app/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from rauth import OAuth2Service


class ProviderError(Exception):
"""Provider Error"""


class IdentityError(ProviderError):
"""Identity Error"""


class ResumeError(ProviderError):
"""Resume Error"""


class PushError(ProviderError):
"""Push Error"""


class TokenError(ProviderError):
"""Token Error"""


class BaseProvider(object):
"""Base Provider"""

_headers = {'User-Agent': 'OpenResume'}

def __init__(self, name, redirect_uri, **kwargs):
self.name = name
self._redirect_uri = redirect_uri
self._prov = OAuth2Service(name=name, **kwargs)

def redirect(self, back_url=None):
raise NotImplemented

def identity(self, token):
raise NotImplemented

def fetch(self, token):
raise NotImplemented

def push(self, token, resume):
raise NotImplemented

def tokenize(self, code, refresh=False):
raise NotImplemented

def __str__(self):
return f'{self.name}'
Loading

0 comments on commit c1587c9

Please sign in to comment.