-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit c1587c9
Showing
22 changed files
with
1,188 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}' |
Oops, something went wrong.