mirror of https://github.com/rhaamo/omnomnomnom
29 changed files with 1495 additions and 0 deletions
@ -0,0 +1,37 @@ |
|||
*.pyc |
|||
/venv |
|||
/config.py |
|||
/.idea |
|||
*.db |
|||
/uploads/*/* |
|||
/tmp/cty.xml |
|||
/.pytest_cache/ |
|||
/api/*.egg-info |
|||
/api/.eggs |
|||
/.eggs |
|||
/configtest.py |
|||
/api/*.log |
|||
.coverage |
|||
.ropeproject |
|||
/.mypy_cache |
|||
/api/test-reports |
|||
test/unit/coverage/ |
|||
test/e2e/reports |
|||
.DS_Store |
|||
/docs-build |
|||
/docs/_build/ |
|||
|
|||
api/config/*_secret.py |
|||
|
|||
# Translations |
|||
*.mo |
|||
|
|||
*~ |
|||
|
|||
front/src/translations.json |
|||
front/src/translations/*.json |
|||
front/locales/*/LC_MESSAGES/app.po |
|||
|
|||
.env |
|||
/data |
|||
/api/celerybeat-schedule |
@ -0,0 +1,22 @@ |
|||
# .tmuxinator.yml |
|||
|
|||
name: dev |
|||
root: . |
|||
|
|||
windows: |
|||
- backend: |
|||
- export FLASK_ENV=development |
|||
- export APP_SETTINGS='config.development_secret.Config' |
|||
- source venv/bin/activate |
|||
- cd api |
|||
- flask run |
|||
# - workers: |
|||
# - export FLASK_ENV=development |
|||
# - export APP_SETTINGS='config.development_secret.Config' |
|||
# - source venv/bin/activate |
|||
# - cd api |
|||
# - celery worker -A tasks.celery --loglevel=error |
|||
- frontend: |
|||
- cd front |
|||
- yarn dev |
|||
|
@ -0,0 +1,42 @@ |
|||
{ |
|||
"python.linting.pylintEnabled": false, |
|||
"python.linting.flake8Enabled": true, |
|||
"python.linting.enabled": true, |
|||
"files.exclude": { |
|||
"**/.git": true, |
|||
"**/.svn": true, |
|||
"**/.hg": true, |
|||
"**/CVS": true, |
|||
"**/.DS_Store": true, |
|||
"**/node_modueles": true, |
|||
"**/__pycache__": true, |
|||
"**/*.egg-info": true |
|||
}, |
|||
"editor.codeActionsOnSave": { |
|||
"source.fixAll": true |
|||
}, |
|||
"eslint.autoFixOnSave": true, |
|||
"eslint.validate": [ |
|||
{ |
|||
"language": "vue", |
|||
"autoFix": true |
|||
}, |
|||
{ |
|||
"language": "html", |
|||
"autoFix": true |
|||
}, |
|||
{ |
|||
"language": "javascript", |
|||
"autoFix": true |
|||
}, |
|||
{ |
|||
"language": "typescript", |
|||
"autoFix": true |
|||
} |
|||
], |
|||
"vetur.format.defaultFormatter.js": "vscode-typescript", |
|||
"vetur.format.defaultFormatter.html": "js-beautify-html", |
|||
"javascript.format.insertSpaceBeforeFunctionParenthesis": true, |
|||
"python.pythonPath": "/home/dashie/dev/omnomnomnom/venv/bin/python", |
|||
"restructuredtext.confPath": "${workspaceFolder}/docs" |
|||
} |
@ -0,0 +1,69 @@ |
|||
### OSX ### |
|||
.DS_Store |
|||
.AppleDouble |
|||
.LSOverride |
|||
|
|||
### SublimeText ### |
|||
# cache files for sublime text |
|||
*.tmlanguage.cache |
|||
*.tmPreferences.cache |
|||
*.stTheme.cache |
|||
|
|||
# workspace files are user-specific |
|||
*.sublime-workspace |
|||
|
|||
# project files should be checked into the repository, unless a significant |
|||
# proportion of contributors will probably not be using SublimeText |
|||
# *.sublime-project |
|||
|
|||
# sftp configuration file |
|||
sftp-config.json |
|||
|
|||
# Basics |
|||
*.py[cod] |
|||
__pycache__ |
|||
|
|||
# Logs |
|||
*.log |
|||
pip-log.txt |
|||
|
|||
# Unit test / coverage reports |
|||
.coverage |
|||
.tox |
|||
nosetests.xml |
|||
htmlcov |
|||
|
|||
# Translations |
|||
*.mo |
|||
*.pot |
|||
|
|||
# Pycharm |
|||
.idea |
|||
|
|||
# Vim |
|||
|
|||
*~ |
|||
*.swp |
|||
*.swo |
|||
|
|||
# npm |
|||
front/node_modules/ |
|||
|
|||
# Compass |
|||
.sass-cache |
|||
|
|||
# virtual environments |
|||
.env |
|||
|
|||
# User-uploaded media |
|||
uploads/ |
|||
|
|||
# Hitch directory |
|||
tests/.hitch |
|||
|
|||
# MailHog binary |
|||
mailhog |
|||
|
|||
*.sqlite3 |
|||
music |
|||
media |
@ -0,0 +1,260 @@ |
|||
# encoding: utf-8 |
|||
import logging |
|||
import os |
|||
import subprocess |
|||
from logging.handlers import RotatingFileHandler |
|||
from flask_babelex import gettext, Babel |
|||
from flask import Flask, g, send_from_directory, jsonify, safe_join, request, flash, Response |
|||
from flask_mail import Mail |
|||
from flask_migrate import Migrate |
|||
from flask_security import Security |
|||
from flask_security import signals as FlaskSecuritySignals |
|||
from flask_cors import CORS, cross_origin |
|||
from flasgger import Swagger |
|||
from werkzeug.middleware.proxy_fix import ProxyFix |
|||
from werkzeug.utils import import_string |
|||
import commands |
|||
from utils.flake_id import FlakeId |
|||
from sqlalchemy import event |
|||
from sqlalchemy.engine import Engine |
|||
import time |
|||
|
|||
from models import db, user_datastore |
|||
from utils.various import InvalidUsage, is_admin |
|||
|
|||
from celery import Celery |
|||
|
|||
from version import VERSION |
|||
|
|||
__VERSION__ = VERSION |
|||
|
|||
AVAILABLE_LOCALES = ["fr", "fr_FR", "en", "en_US", "pl"] |
|||
|
|||
try: |
|||
import sentry_sdk |
|||
from sentry_sdk.integrations.flask import FlaskIntegration as SentryFlaskIntegration |
|||
from sentry_sdk.integrations.celery import CeleryIntegration as SentryCeleryIntegration |
|||
|
|||
print(" * Sentry Flask/Celery support have been loaded") |
|||
HAS_SENTRY = True |
|||
except ImportError: |
|||
print(" * No Sentry Flask/Celery support available") |
|||
HAS_SENTRY = False |
|||
|
|||
GIT_VERSION = "" |
|||
gitpath = os.path.join(os.getcwd(), "../.git") |
|||
if os.path.isdir(gitpath): |
|||
GIT_VERSION = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) |
|||
if GIT_VERSION: |
|||
GIT_VERSION = GIT_VERSION.strip().decode("UTF-8") |
|||
|
|||
|
|||
def make_celery(remoulade): |
|||
celery = Celery(remoulade.import_name, broker=remoulade.config["CELERY_BROKER_URL"]) |
|||
celery.conf.update(remoulade.config) |
|||
TaskBase = celery.Task |
|||
|
|||
class ContextTask(TaskBase): |
|||
abstract = True |
|||
|
|||
def __call__(self, *args, **kwargs): |
|||
with remoulade.app_context(): |
|||
return TaskBase.__call__(self, *args, **kwargs) |
|||
|
|||
celery.Task = ContextTask |
|||
return celery # omnomnom |
|||
|
|||
|
|||
def create_app(config_filename="config.development.Config", app_name=None, register_blueprints=True): |
|||
# App configuration |
|||
app = Flask(app_name or __name__) |
|||
app_settings = os.getenv("APP_SETTINGS", config_filename) |
|||
print(f" * Loading config: '{app_settings}'") |
|||
try: |
|||
cfg = import_string(app_settings)() |
|||
except ImportError: |
|||
print(" *** Cannot import config ***") |
|||
cfg = import_string("config.config.BaseConfig") |
|||
print(" *** Default config loaded, expect problems ***") |
|||
app.config.from_object(cfg) |
|||
|
|||
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) |
|||
|
|||
app.jinja_env.add_extension("jinja2.ext.with_") |
|||
app.jinja_env.add_extension("jinja2.ext.do") |
|||
app.jinja_env.globals.update(is_admin=is_admin) |
|||
|
|||
if HAS_SENTRY: |
|||
sentry_sdk.init( |
|||
app.config["SENTRY_DSN"], |
|||
integrations=[SentryFlaskIntegration(), SentryCeleryIntegration()], |
|||
release=f"{VERSION} ({GIT_VERSION})", |
|||
) |
|||
print(" * Sentry Flask/Celery support activated") |
|||
print(" * Sentry DSN: %s" % app.config["SENTRY_DSN"]) |
|||
|
|||
if app.debug: |
|||
app.jinja_env.auto_reload = True |
|||
logging.basicConfig(level=logging.DEBUG) |
|||
|
|||
# Logging |
|||
if not app.debug: |
|||
formatter = logging.Formatter("%(asctime)s %(levelname)s: %(message)s " "[in %(pathname)s:%(lineno)d]") |
|||
file_handler = RotatingFileHandler("%s/errors_app.log" % os.getcwd(), "a", 1000000, 1) |
|||
file_handler.setLevel(logging.INFO) |
|||
file_handler.setFormatter(formatter) |
|||
app.logger.addHandler(file_handler) |
|||
|
|||
dbLogger = logging.getLogger("omnomnomnom.sqltime") |
|||
dbLogger.setLevel(logging.DEBUG) |
|||
|
|||
CORS(app, origins=["*"]) |
|||
|
|||
if app.debug: |
|||
logging.getLogger("flask_cors.extension").level = logging.DEBUG |
|||
|
|||
mail = Mail(app) # noqa: F841 |
|||
migrate = Migrate(app, db) # noqa: F841 lgtm [py/unused-local-variable] |
|||
babel = Babel(app) # noqa: F841 |
|||
app.babel = babel |
|||
|
|||
template = { |
|||
"swagger": "2.0", |
|||
"info": {"title": "omnomnomnom API", "description": "API", "version": VERSION}, |
|||
"host": app.config["APP_HOST"], |
|||
"basePath": "/", |
|||
"schemes": ["https"], |
|||
"securityDefinitions": { |
|||
}, |
|||
"consumes": ["application/json"], |
|||
"produces": ["application/json"], |
|||
} |
|||
|
|||
db.init_app(app) |
|||
|
|||
# Setup Flask-Security |
|||
security = Security(app, user_datastore) # noqa: F841 lgtm [py/unused-local-variable] |
|||
|
|||
@FlaskSecuritySignals.password_reset.connect_via(app) |
|||
@FlaskSecuritySignals.password_changed.connect_via(app) |
|||
def log_password_reset(sender, user): |
|||
if not user: |
|||
return |
|||
add_user_log(user.id, user.id, "user", "info", "Your password has been changed !") |
|||
|
|||
@FlaskSecuritySignals.reset_password_instructions_sent.connect_via(app) |
|||
def log_reset_password_instr(sender, user, token): |
|||
if not user: |
|||
return |
|||
add_user_log(user.id, user.id, "user", "info", "Password reset instructions sent.") |
|||
|
|||
@security.mail_context_processor |
|||
def mail_ctx_proc(): |
|||
return dict() |
|||
|
|||
@babel.localeselector |
|||
def get_locale(): |
|||
# if a user is logged in, use the locale from the user settings |
|||
identity = getattr(g, "identity", None) |
|||
if identity is not None and identity.id: |
|||
return identity.user.locale |
|||
# otherwise try to guess the language from the user accept |
|||
# header the browser transmits. We support fr/en in this |
|||
# example. The best match wins. |
|||
return request.accept_languages.best_match(AVAILABLE_LOCALES) |
|||
|
|||
@babel.timezoneselector |
|||
def get_timezone(): |
|||
identity = getattr(g, "identity", None) |
|||
if identity is not None and identity.id: |
|||
return identity.user.timezone |
|||
|
|||
|
|||
@app.before_request |
|||
def before_request(): |
|||
|
|||
cfg = { |
|||
"OMNOMNOMNOM_VERSION_VER": VERSION, |
|||
"OMNOMNOMNOM_VERSION_GIT": GIT_VERSION, |
|||
} |
|||
if GIT_VERSION: |
|||
cfg["OMNOMNOMNOM_VERSION"] = "{0}-{1}".format(VERSION, GIT_VERSION) |
|||
else: |
|||
cfg["OMNOMNOMNOM_VERSION"] = VERSION |
|||
|
|||
g.cfg = cfg |
|||
|
|||
@app.errorhandler(InvalidUsage) |
|||
def handle_invalid_usage(error): |
|||
response = jsonify(error.to_dict()) |
|||
response.status_code = error.status_code |
|||
return response |
|||
|
|||
@event.listens_for(Engine, "before_cursor_execute") |
|||
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany): |
|||
if not False: |
|||
return |
|||
conn.info.setdefault("query_start_time", []).append(time.time()) |
|||
dbLogger.debug("Start Query: %s", statement) |
|||
|
|||
@event.listens_for(Engine, "after_cursor_execute") |
|||
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany): |
|||
if not False: |
|||
return |
|||
total = time.time() - conn.info["query_start_time"].pop(-1) |
|||
dbLogger.debug("Query Complete!") |
|||
dbLogger.debug("Total Time: %f", total) |
|||
|
|||
app.flake_id = FlakeId() |
|||
|
|||
if register_blueprints: |
|||
# from controllers.api.v1.Xm import bp_X |
|||
# app.register_blueprint(bp_X) |
|||
|
|||
swagger = Swagger(app, template=template) # noqa: F841 lgtm [py/unused-local-variable] |
|||
|
|||
@app.route("/uploads/<string:thing>/<path:stuff>", methods=["GET"]) |
|||
@cross_origin(origins="*", methods=["GET", "HEAD", "OPTIONS"], expose_headers="content-length", send_wildcard=True) |
|||
def get_uploads_stuff(thing, stuff): |
|||
if app.testing or app.debug: |
|||
directory = safe_join(app.config["UPLOADS_DEFAULT_DEST"], thing) |
|||
app.logger.debug(f"serving {stuff} from {directory}") |
|||
return send_from_directory(directory, stuff, as_attachment=True) |
|||
else: |
|||
app.logger.debug(f"X-Accel-Redirect serving {stuff}") |
|||
resp = Response("") |
|||
resp.headers["Content-Disposition"] = f"attachment; filename={stuff}" |
|||
resp.headers["X-Accel-Redirect"] = f"/_protected/media/{thing}/{stuff}" |
|||
resp.headers["Content-Type"] = "" # empty it so Nginx will guess it correctly |
|||
return resp |
|||
|
|||
@app.errorhandler(404) |
|||
def page_not_found(msg): |
|||
return jsonify({"error": "page not found"}), 404 |
|||
|
|||
@app.errorhandler(403) |
|||
def err_forbidden(msg): |
|||
return jsonify({"error": "access forbidden"}), 403 |
|||
|
|||
@app.errorhandler(410) |
|||
def err_gone(msg): |
|||
return jsonify({"error": "gone"}), 410 |
|||
|
|||
if not app.debug: |
|||
|
|||
@app.errorhandler(500) |
|||
def err_failed(msg): |
|||
return jsonify({"error": "server error"}), 500 |
|||
|
|||
@app.after_request |
|||
def set_x_powered_by(response): |
|||
response.headers["X-Powered-By"] = "omnomnomnom" |
|||
return response |
|||
|
|||
# Register CLI commands |
|||
app.cli.add_command(commands.db_datas) |
|||
app.cli.add_command(commands.users) |
|||
app.cli.add_command(commands.roles) |
|||
app.cli.add_command(commands.system) |
|||
|
|||
return app |
@ -0,0 +1,5 @@ |
|||
# flake8: noqa: F401 |
|||
from .db_datas import db_datas |
|||
from .users import users |
|||
from .roles import roles |
|||
from .system import system |
@ -0,0 +1,39 @@ |
|||
import click |
|||
from models import db, Role |
|||
from flask.cli import with_appcontext |
|||
|
|||
|
|||
@click.group() |
|||
def db_datas(): |
|||
""" |
|||
Datas migrations sometimes needed. |
|||
|
|||
Run them only one time unless specified BREAKING. |
|||
""" |
|||
pass |
|||
|
|||
|
|||
def make_db_seed(db): |
|||
# roles |
|||
roles = db.session.query(Role.name).all() |
|||
roles = [r[0] for r in roles] |
|||
if "user" not in roles: |
|||
role_usr = Role(name="user", description="Simple user") |
|||
db.session.add(role_usr) |
|||
if "admin" not in roles: |
|||
role_adm = Role(name="admin", description="Admin user") |
|||
db.session.add(role_adm) |
|||
|
|||
# Final commit |
|||
db.session.commit() |
|||
|
|||
|
|||
@db_datas.command(name="000-seeds") |
|||
@with_appcontext |
|||
def seeds(): |
|||
""" |
|||
Seed database with default config and roles values |
|||
|
|||
non breaking. |
|||
""" |
|||
make_db_seed(db) |
@ -0,0 +1,32 @@ |
|||
import click |
|||
import texttable |
|||
from models import Role |
|||
from flask.cli import with_appcontext |
|||
|
|||
|
|||
@click.group() |
|||
def roles(): |
|||
""" |
|||
Role commands. |
|||
""" |
|||
pass |
|||
|
|||
|
|||
@roles.command(name="list") |
|||
@with_appcontext |
|||
def list(): |
|||
""" |
|||
List roles. |
|||
""" |
|||
roles = Role.query.order_by(Role.id.asc()) |
|||
|
|||
table = texttable.Texttable() |
|||
table.set_deco(texttable.Texttable().HEADER) |
|||
table.set_cols_dtype(["t", "t"]) |
|||
table.set_cols_align(["l", "l"]) |
|||
table.add_rows([["Name", "Description"]]) |
|||
|
|||
for role in roles.all(): |
|||
table.add_row([role.name, role.description]) |
|||
|
|||
print(table.draw()) |
@ -0,0 +1,56 @@ |
|||
import click |
|||
import sys |
|||
from flask.cli import with_appcontext |
|||
from flask_mail import Message |
|||
from flask import render_template, current_app |
|||
import texttable |
|||
|
|||
|
|||
@click.group() |
|||
def system(): |
|||
""" |
|||
System commands. |
|||
""" |
|||
pass |
|||
|
|||
|
|||
@system.command(name="test-email") |
|||
@click.option("--email", prompt=True, help="Email to send the test to") |
|||
@with_appcontext |
|||
def test_email(email): |
|||
""" |
|||
Test email sending. |
|||
""" |
|||
mail = current_app.extensions.get("mail") |
|||
if not mail: |
|||
print("ERROR: mail extensions is None !!!") |
|||
exit(-1) |
|||
|
|||
msg = Message(subject="omnomnomnom test email", recipients=[email], sender=current_app.config["MAIL_DEFAULT_SENDER"]) |
|||
msg.body = render_template("email/test_email.txt") |
|||
msg.html = render_template("email/test_email.html") |
|||
try: |
|||
mail.send(msg) |
|||
except: # noqa: E722 |
|||
print(f"Error sending mail: {sys.exc_info()[0]}") |
|||
|
|||
|
|||
@system.command(name="routes") |
|||
@with_appcontext |
|||
def routes(): |
|||
""" |
|||
Dump all routes of defined app |
|||
""" |
|||
table = texttable.Texttable() |
|||
table.set_deco(texttable.Texttable().HEADER) |
|||
table.set_cols_dtype(["t", "t", "t"]) |
|||
table.set_cols_align(["l", "l", "l"]) |
|||
table.set_cols_width([50, 30, 80]) |
|||
|
|||
table.add_rows([["Prefix", "Verb", "URI Pattern"]]) |
|||
|
|||
for rule in sorted(current_app.url_map.iter_rules(), key=lambda x: str(x)): |
|||
methods = ",".join(rule.methods) |
|||
table.add_row([rule.endpoint, methods, rule]) |
|||
|
|||
print(table.draw()) |
@ -0,0 +1,87 @@ |
|||
import click |
|||
from models import db, user_datastore, Role, User |
|||
from flask.cli import with_appcontext |
|||
from flask import current_app |
|||
from flask_security.utils import hash_password |
|||
from flask_security import confirmable as FSConfirmable |
|||
import texttable |
|||
import datetime |
|||
|
|||
|
|||
@click.group() |
|||
def users(): |
|||
""" |
|||
User commands. |
|||
""" |
|||
pass |
|||
|
|||
|
|||
@users.command(name="list") |
|||
@with_appcontext |
|||
def list(): |
|||
""" |
|||
List local users. |
|||
""" |
|||
users = User.query.filter() |
|||
|
|||
table = texttable.Texttable(max_width=120) |
|||
table.set_deco(texttable.Texttable().HEADER) |
|||
table.set_cols_dtype(["i", "t"]) |
|||
table.set_cols_align(["l", "l"]) |
|||
table.add_rows([["ID", "username"]]) |
|||
|
|||
for user in users.all(): |
|||
table.add_row( |
|||
[ |
|||
user.id, |
|||
user.name, |
|||
] |
|||
) |
|||
|
|||
print(table.draw()) |
|||
|
|||
|
|||
@users.command(name="create") |
|||
@with_appcontext |
|||
def create(): |
|||
""" |
|||
Create a user. |
|||
""" |
|||
username = click.prompt("Username", type=str) |
|||
email = click.prompt("Email", type=str) |
|||
password = click.prompt("Password", type=str, hide_input=True, confirmation_prompt=True) |
|||
while True: |
|||
role = click.prompt("Role [admin/user]", type=str) |
|||
if role == "admin" or role == "user": |
|||
break |
|||
|
|||
if click.confirm("Do you want to continue ?"): |
|||
role = Role.query.filter(Role.name == role).first() |
|||
if not role: |
|||
raise click.UsageError("Roles not present in database") |
|||
u = user_datastore.create_user(name=username, email=email, password=hash_password(password), roles=[role]) |
|||
|
|||
db.session.commit() |
|||
|
|||
if FSConfirmable.requires_confirmation(u): |
|||
with current_app.app_context(): |
|||
FSConfirmable.send_confirmation_instructions(u) |
|||
print("Look at your emails for validation instructions.") |
|||
|
|||
|
|||
@users.command(name="confirm") |
|||
@click.option("--username", prompt=True, help="Username") |
|||
@with_appcontext |
|||
def confirm(username): |
|||
""" |
|||
Force activation of an user. |
|||
""" |
|||
u = User.query.filter(User.name == username).first() |
|||
if not u: |
|||
print(f"Cannot find user with username '{username}'") |
|||
exit(1) |
|||
|
|||
u.confirmed_at = datetime.datetime.now() |
|||
db.session.commit() |
|||
|
|||
print("User confirmed at: ", u.confirmed_at) |
@ -0,0 +1,141 @@ |
|||
import os |
|||
|
|||
# .i;;;;i. |
|||
# iYcviii;vXY: |
|||
# .YXi .i1c. |
|||
# .YC. . in7. |
|||
# .vc. ...... ;1c. |
|||
# i7, .. .;1; |
|||
# i7, .. ... .Y1i |
|||
# ,7v .6MMM@; .YX, |
|||
# .7;. ..IMMMMMM1 :t7. |
|||
# .;Y. ;$MMMMMM9. :tc. |
|||
# vY. .. .nMMM@MMU. ;1v. |
|||
# i7i ... .#MM@M@C. .....:71i |
|||
# it: .... $MMM@9;.,i;;;i,;tti |
|||
# :t7. ..... 0MMMWv.,iii:::,,;St. |
|||
# .nC. ..... IMMMQ..,::::::,.,czX. |
|||
# .ct: ....... .ZMMMI..,:::::::,,:76Y. |
|||
# c2: ......,i..Y$M@t..:::::::,,..inZY |
|||
# vov ......:ii..c$MBc..,,,,,,,,,,..iI9i |
|||
# i9Y ......iii:..7@MA,..,,,,,,,,,....;AA: |
|||
# iIS. ......:ii::..;@MI....,............;Ez. |
|||
# .I9. ......:i::::...8M1..................C0z. |
|||
# .z9; ......:i::::,.. .i:...................zWX. |
|||
# vbv ......,i::::,,. ................. :AQY |
|||
# c6Y. .,...,::::,,..:t0@@QY. ................ :8bi |
|||
# :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ, |
|||
# :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2. |
|||
# .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn |
|||
# 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi |
|||
# 7C...::::::::::::,,,,.. .................... vSi. |
|||
# ;1;...,,::::::,......... .................. Yz: |
|||
# v97,......... .voC. |
|||
# izAotX7777777777777777777777777777777777777777Y7n92: |
|||
# .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov. |
|||
# |
|||
# !!! ATTENTION !!! |
|||
# DO NOT EDIT THIS FILE! THIS FILE CONTAINS THE DEFAULT VALUES FOR THE CON- |
|||
# FIGURATION! EDIT YOUR OWN CONFIG FILE (based on production.py or development.py or testing.py). |
|||
# AND DICTATE THE APP TO USE YOUR FILE WITH ENV VARIABLE: |
|||
# APP_SETTINGS='config.yourEnv.Config' |
|||
# |
|||
# Editing this file is done at your own risks, don't cry if doing that transforms your cat in an opossum. |
|||
|
|||
|
|||
def bool_env(var_name, default=False): |
|||
test_val = os.getenv(var_name, default) |
|||
if test_val in ("False", "false", "0", "no"): |
|||
return False |
|||
return bool(test_val) |
|||
|
|||
|
|||
class BaseConfig(object): |
|||
""" Base configuration, pls dont edit me """ |
|||
|
|||
# Debug and testing specific |
|||
TESTING = bool_env("TESTING", False) |
|||
DEBUG = bool_env("DEBUG", False) |
|||
|
|||
@property |
|||
def TEMPLATES_AUTO_RELOAD(self): |
|||
return self.DEBUG |
|||
|
|||
# Can users register |
|||
REGISTRATION_ENABLED = bool_env("REGISTRATION_ENABLED", False) |
|||
|
|||
# Registration, same as upper |
|||
@property |
|||
def SECURITY_REGISTERABLE(self): |
|||
return self.REGISTRATION_ENABLED |
|||
|
|||
# Secret key, you are supposed to generate one |
|||
# Ex: `openssl rand -hex 42` |
|||
SECRET_KEY = os.getenv("SECRET_KEY", None) |
|||
# Ex: `openssl rand -hex 5` |
|||
SECURITY_PASSWORD_SALT = os.getenv("SECURITY_PASSWORD_SALT", None) |
|||
|
|||
# Database stuff |
|||
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI", "postgresql+psycopg2://postgres@localhost/omnomnomnom") |
|||
SQLALCHEMY_ECHO = bool_env("SQLALCHEMY_ECHO", False) |
|||
# Thoses two shouldn't be touched |
|||
SQLALCHEMY_TRACK_MODIFICATIONS = False |
|||
SQLALCHEMY_RECORD_QUERIES = True |
|||
|
|||
# Flask-Security stuff |
|||
# Should users confirm theire email address ? |
|||
SECURITY_CONFIRMABLE = bool_env("SECURITY_CONFIRMABLE", True) |
|||
|
|||
# We have an alternative way |
|||
SECURITY_RECOVERABLE = False |
|||
# Don't change or you will break things |
|||
SECURITY_CHANGEABLE = True |
|||
# Same or I bite you |
|||
SECURITY_PASSWORD_HASH = "bcrypt" |
|||
SECURITY_SEND_REGISTER_EMAIL = True |
|||
SECURITY_SEND_PASSWORD_CHANGE_EMAIL = True |
|||
SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL = True |
|||
|
|||
# Backend default language |
|||
BABEL_DEFAULT_LOCALE = os.getenv("BABEL_DEFAULT_LOCALE", "en") |
|||
# Not sure this one has any effect... |
|||
BABEL_DEFAULT_TIMEZONE = os.getenv("BABEL_DEFAULT_TIMEZONE", "UTC") |
|||
|
|||
# If using sentry |
|||
SENTRY_DSN = os.getenv("SENTRY_DSN", None) |
|||
|
|||
# Broker setup for Celery, same redis base for both |
|||
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://127.0.0.1:6379/0") |
|||
CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", "redis://127.0.0.1:6379/0") |
|||
|
|||
# ActivityPub stuff |
|||
AP_ENABLED = bool_env("AP_ENABLED", False) |
|||
|
|||
# Sources of that instance, should be your repos if forked |
|||
SOURCES_REPOSITORY_URL = os.getenv("SOURCES_REPOSITORY_URL", "https://github.com/dashie/omnopmnomnom") |
|||
|
|||
# Mail setup |
|||
MAIL_SERVER = os.getenv("MAIL_SERVER", "localhost") |
|||
MAIL_PORT = os.getenv("MAIL_PORT", 25) |
|||
MAIL_USE_TLS = bool_env("MAIL_USE_TLS", False) |
|||
MAIL_USE_SSL = bool_env("MAIL_USE_SSL", False) |
|||
MAIL_USERNAME = os.getenv("MAIL_USERNAME", None) |
|||
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD", None) |
|||
|
|||
APP_HOST = "localhost" |
|||
|
|||
@property |
|||
def MAIL_DEFAULT_SENDER(self): |
|||
return os.getenv("MAIL_DEFAULT_SENDER", f"postmaster@{self.APP_HOST}") |
|||
|
|||
@property |
|||
def SECURITY_EMAIL_SENDER(self): |
|||
return os.getenv("MAIL_DEFAULT_SENDER", f"postmaster@{self.APP_HOST}") |
|||
|
|||
SECURITY_EMAIL_SUBJECT_REGISTER = os.getenv("SECURITY_EMAIL_SUBJECT_REGISTER", "Welcome to omnomnomnom") |
|||
|
|||
# Don't touch |
|||
SWAGGER_UI_DOC_EXPANSION = "list" |
|||
|
|||
SECURITY_CLI_USERS_NAME = False |
|||
SECURITY_CLI_ROLES_NAME = False |
@ -0,0 +1,5 @@ |
|||
from .config import BaseConfig |
|||
|
|||
|
|||
class Config(BaseConfig): |
|||
DEBUG = True |
@ -0,0 +1,33 @@ |
|||
from .development import Config as BaseConfig |
|||
|
|||
|
|||
class Config(BaseConfig): |
|||
# See the Configuration documentation at: |
|||
# FIXME AHAH |
|||
# For all the config keys you can use |
|||
|
|||
# Please generate me with: openssl rand -hex 42 |
|||
SECRET_KEY = "fdsfsdfsdfsdfdsfsdfsdfsdfdsfsdfsd" |
|||
# Please generate me with: openssl rand -hex 5 |
|||
SECURITY_PASSWORD_SALT = "omgponies" |
|||
# Set your DB URI |
|||
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://dashie@localhost/omnomnomnom" |
|||
|
|||
APP_HOST = "miam.host.tld" |
|||
|
|||
# If you are using Sentry, otherwise, set to None |
|||
SENTRY_DSN = None |
|||
|
|||
# If you are using a modified instance, please set your own repository URL |
|||
SOURCES_REPOSITORY_URL = "https://github.com/dashie/omnomnomnom" |
|||
|
|||
# Email settings |
|||
MAIL_SERVER = "localhost" |
|||
# MAIL_PORT = 25 |
|||
# MAIL_USE_TLS = False |
|||
# MAIL_USE_SSL = False |
|||
# MAIL_USERNAME = None |
|||
# MAIL_PASSWORD = None |
|||
|
|||
# CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0' |
|||
# CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0' |
@ -0,0 +1,25 @@ |
|||
from .config import BaseConfig |
|||
|
|||
|
|||
class Config(BaseConfig): |
|||
DEBUG = True |
|||
TESTING = True |
|||
MAIL_DEBUG = True |
|||
REGISTRATION_ENABLED = True |
|||
SECRET_KEY = "udf298euf02uf2f02f2uf0" |
|||
SECURITY_CONFIRMABLE = False |
|||
SECURITY_REGISTERABLE = True |
|||
SECURITY_RECOVERABLE = True |
|||
SECURITY_TRACKABLE = False |
|||
SECURITY_CHANGEABLE = True |
|||
SECURITY_PASSWORD_HASH = "bcrypt" |
|||
SECURITY_PASSWORD_SALT = "dsadsaasd" |
|||
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://postgres@localhost:5432/omnomnomnom_test" |
|||
SECURITY_SEND_REGISTER_EMAIL = False |
|||
SECURITY_SEND_PASSWORD_CHANGE_EMAIL = False |
|||
SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL = False |
|||
SECURITY_SEND_PASSWORD_RESET_EMAIL = False |
|||
# Bcrypt algorithm hashing rounds (reduced for testing purposes only!) |
|||
BCRYPT_LOG_ROUNDS = 4 |
|||
CELERY_BROKER_URL = "redis://redis:6379/0" |
|||
CELERY_RESULT_BACKEND = "redis://redis:6379/0" |
@ -0,0 +1 @@ |
|||
Generic single-database configuration. |
@ -0,0 +1,45 @@ |
|||
# A generic, single database configuration. |
|||
|
|||
[alembic] |
|||
# template used to generate migration files |
|||
# file_template = %%(rev)s_%%(slug)s |
|||
|
|||
# set to 'true' to run the environment during |
|||
# the 'revision' command, regardless of autogenerate |
|||
# revision_environment = false |
|||
|
|||
|
|||
# Logging configuration |
|||
[loggers] |
|||
keys = root,sqlalchemy,alembic |
|||
|
|||
[handlers] |
|||
keys = console |
|||
|
|||
[formatters] |
|||
keys = generic |
|||
|
|||
[logger_root] |
|||
level = WARN |
|||
handlers = console |
|||
qualname = |
|||
|
|||
[logger_sqlalchemy] |
|||
level = WARN |
|||
handlers = |
|||
qualname = sqlalchemy.engine |
|||
|
|||
[logger_alembic] |
|||
level = INFO |
|||
handlers = |
|||
qualname = alembic |
|||
|
|||
[handler_console] |
|||
class = StreamHandler |
|||
args = (sys.stderr,) |
|||
level = NOTSET |
|||
formatter = generic |
|||
|
|||
[formatter_generic] |
|||
format = %(levelname)-5.5s [%(name)s] %(message)s |
|||
datefmt = %H:%M:%S |
@ -0,0 +1,96 @@ |
|||
from __future__ import with_statement |
|||
|
|||
import logging |
|||
from logging.config import fileConfig |
|||
|
|||
from sqlalchemy import engine_from_config |
|||
from sqlalchemy import pool |
|||
|
|||
from alembic import context |
|||
|
|||
# this is the Alembic Config object, which provides |
|||
# access to the values within the .ini file in use. |
|||
config = context.config |
|||
|
|||
# Interpret the config file for Python logging. |
|||
# This line sets up loggers basically. |
|||
fileConfig(config.config_file_name) |
|||
logger = logging.getLogger('alembic.env') |
|||
|
|||
# add your model's MetaData object here |
|||
# for 'autogenerate' support |
|||
# from myapp import mymodel |
|||
# target_metadata = mymodel.Base.metadata |
|||
from flask import current_app |
|||
config.set_main_option( |
|||
'sqlalchemy.url', current_app.config.get( |
|||
'SQLALCHEMY_DATABASE_URI').replace('%', '%%')) |
|||
target_metadata = current_app.extensions['migrate'].db.metadata |
|||
|
|||
# other values from the config, defined by the needs of env.py, |
|||
# can be acquired: |
|||
# my_important_option = config.get_main_option("my_important_option") |
|||
# ... etc. |
|||
|
|||
|
|||
def run_migrations_offline(): |
|||
"""Run migrations in 'offline' mode. |
|||
|
|||
This configures the context with just a URL |
|||
and not an Engine, though an Engine is acceptable |
|||
here as well. By skipping the Engine creation |
|||
we don't even need a DBAPI to be available. |
|||
|
|||
Calls to context.execute() here emit the given string to the |
|||
script output. |
|||
|
|||
""" |
|||
url = config.get_main_option("sqlalchemy.url") |
|||
context.configure( |
|||
url=url, target_metadata=target_metadata, literal_binds=True |
|||
) |
|||
|
|||
with context.begin_transaction(): |
|||
context.run_migrations() |
|||
|
|||
|
|||
def run_migrations_online(): |
|||
"""Run migrations in 'online' mode. |
|||
|
|||
In this scenario we need to create an Engine |
|||
and associate a connection with the context. |
|||
|
|||
""" |
|||
|
|||
# this callback is used to prevent an auto-migration from being generated |
|||
# when there are no changes to the schema |
|||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html |
|||
def process_revision_directives(context, revision, directives): |
|||
if getattr(config.cmd_opts, 'autogenerate', False): |
|||
script = directives[0] |
|||
if script.upgrade_ops.is_empty(): |
|||
directives[:] = [] |
|||
logger.info('No changes in schema detected.') |
|||
|
|||
connectable = engine_from_config( |
|||
config.get_section(config.config_ini_section), |
|||
prefix='sqlalchemy.', |
|||
poolclass=pool.NullPool, |
|||
) |
|||
|
|||
with connectable.connect() as connection: |
|||
context.configure( |
|||
connection=connection, |
|||
target_metadata=target_metadata, |
|||
process_revision_directives=process_revision_directives, |
|||
**current_app.extensions['migrate'].configure_args |
|||
) |
|||
|
|||
with context.begin_transaction(): |
|||
context.run_migrations() |
|||
|
|||
|
|||
if context.is_offline_mode(): |
|||
run_migrations_offline() |
|||
else: |
|||
run_migrations_online() |
@ -0,0 +1,24 @@ |
|||
"""${message} |
|||
|
|||
Revision ID: ${up_revision} |
|||
Revises: ${down_revision | comma,n} |
|||
Create Date: ${create_date} |
|||
|
|||
""" |
|||
from alembic import op # noqa: E402 |
|||
import sqlalchemy as sa # noqa: E402 |
|||
${imports if imports else ""} |
|||
|
|||
# revision identifiers, used by Alembic. |
|||
revision = ${repr(up_revision)} |
|||
down_revision = ${repr(down_revision)} |
|||
branch_labels = ${repr(branch_labels)} |
|||
depends_on = ${repr(depends_on)} |
|||
|
|||
|
|||
def upgrade(): |
|||
${upgrades if upgrades else "pass"} |
|||
|
|||
|
|||
def downgrade(): |
|||
${downgrades if downgrades else "pass"} |
@ -0,0 +1,69 @@ |
|||
"""Initial migration |
|||
|
|||
Revision ID: 77dc08b8493f |
|||
Revises: |
|||
Create Date: 2020-08-09 11:20:39.008541 |
|||
|
|||
""" |
|||
from alembic import op # noqa: E402 |
|||
import sqlalchemy as sa # noqa: E402 |
|||
from sqlalchemy.dialects import postgresql |
|||
|
|||
# revision identifiers, used by Alembic. |
|||
revision = '77dc08b8493f' |
|||
down_revision = None |
|||
branch_labels = None |
|||
depends_on = None |
|||
|
|||
|
|||
def upgrade(): |
|||
# ### commands auto generated by Alembic - please adjust! ### |
|||
op.create_table('role', |
|||
sa.Column('id', sa.Integer(), nullable=False), |
|||
sa.Column('name', sa.String(length=80), nullable=False), |
|||
sa.Column('description', sa.String(length=255), nullable=True), |
|||
sa.PrimaryKeyConstraint('id'), |
|||
sa.UniqueConstraint('name') |
|||
) |
|||
op.create_table('user', |
|||
sa.Column('id', sa.Integer(), nullable=False), |
|||
sa.Column('email', sa.String(length=255), nullable=True), |
|||
sa.Column('name', sa.String(length=255), nullable=False), |
|||
sa.Column('password', sa.String(length=255), nullable=True), |
|||
sa.Column('active', sa.Boolean(), nullable=True), |
|||
sa.Column('confirmed_at', sa.DateTime(), nullable=True), |
|||
sa.Column('fs_uniquifier', sa.String(length=255), nullable=True), |
|||
sa.Column('created_at', sa.DateTime(), nullable=True), |
|||
sa.Column('updated_at', sa.DateTime(), nullable=True), |
|||
sa.Column('locale', sa.String(length=5), nullable=True), |
|||
sa.Column('flake_id', postgresql.UUID(as_uuid=True), nullable=True), |
|||
sa.Column('slug', sa.String(length=255), nullable=True), |
|||
sa.PrimaryKeyConstraint('id'), |
|||
sa.UniqueConstraint('email') |
|||
) |
|||
op.create_table('logging', |
|||
sa.Column('id', sa.Integer(), nullable=False), |
|||
sa.Column('category', sa.String(length=255), nullable=False), |
|||
sa.Column('level', sa.String(length=255), nullable=False), |
|||
sa.Column('message', sa.Text(), nullable=False), |
|||
sa.Column('timestamp', sa.DateTime(), nullable=True), |
|||
sa.Column('user_id', sa.Integer(), nullable=True), |
|||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), |
|||
sa.PrimaryKeyConstraint('id') |
|||
) |
|||
op.create_table('roles_users', |
|||
sa.Column('user_id', sa.Integer(), nullable=True), |
|||
sa.Column('role_id', sa.Integer(), nullable=True), |
|||
sa.ForeignKeyConstraint(['role_id'], ['role.id'], ), |
|||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ) |
|||
) |
|||
# ### end Alembic commands ### |
|||
|
|||
|
|||
def downgrade(): |
|||
# ### commands auto generated by Alembic - please adjust! ### |
|||
op.drop_table('roles_users') |
|||
op.drop_table('logging') |
|||
op.drop_table('user') |
|||
op.drop_table('role') |
|||
# ### end Alembic commands ### |
@ -0,0 +1,117 @@ |
|||
import datetime |
|||
|
|||
from flask import current_app |
|||
from flask_security import SQLAlchemyUserDatastore, UserMixin, RoleMixin |
|||
from flask_security.utils import verify_password |
|||
from flask_sqlalchemy import SQLAlchemy |
|||
from sqlalchemy.ext.hybrid import Comparator |
|||
from sqlalchemy.sql import func |
|||
from sqlalchemy_searchable import make_searchable |
|||
from sqlalchemy.dialects.postgresql import UUID |
|||
import uuid |
|||
from sqlalchemy import event |
|||
|
|||
|
|||
db = SQLAlchemy() |
|||
make_searchable(db.metadata) |
|||
|
|||
# #### Base #### |
|||
|
|||
|
|||
class CaseInsensitiveComparator(Comparator): |
|||
def __eq__(self, other): |
|||
return func.lower(self.__clause_element__()) == func.lower(other) |
|||
|
|||
|
|||
# #### User #### |
|||
|
|||
roles_users = db.Table( |
|||
"roles_users", |
|||
db.Column("user_id", db.Integer(), db.ForeignKey("user.id")), |
|||
db.Column("role_id", db.Integer(), db.ForeignKey("role.id")), |
|||
) |
|||
|
|||
|
|||
class Role(db.Model, RoleMixin): |
|||
id = db.Column(db.Integer(), primary_key=True) |
|||
name = db.Column(db.String(80), unique=True, nullable=False, info={"label": "Name"}) |
|||
description = db.Column(db.String(255), info={"label": "Description"}) |
|||
|
|||
__mapper_args__ = {"order_by": name} |
|||
|
|||
|
|||
class User(db.Model, UserMixin): |
|||
id = db.Column(db.Integer, primary_key=True) |
|||
email = db.Column(db.String(255), unique=True, nullable=True, info={"label": "Email"}) |
|||
name = db.Column(db.String(255), nullable=False, info={"label": "Username"}) |
|||
password = db.Column(db.String(255), nullable=True, info={"label": "Password"}) |
|||
active = db.Column(db.Boolean()) |
|||
confirmed_at = db.Column(db.DateTime()) |
|||
fs_uniquifier = db.Column(db.String(255)) |
|||
|
|||
created_at = db.Column(db.DateTime(timezone=False), default=datetime.datetime.utcnow) |
|||
updated_at = db.Column( |
|||
db.DateTime(timezone=False), default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow |
|||
) |
|||
|
|||
locale = db.Column(db.String(5), default="en") |
|||
|
|||
flake_id = db.Column(UUID(as_uuid=True), unique=False, nullable=True) |
|||
|
|||
slug = db.Column(db.String(255), nullable=True) |
|||
|
|||
# Relations |
|||
|
|||
roles = db.relationship( |
|||
"Role", secondary=roles_users, backref=db.backref("users", lazy="dynamic"), cascade_backrefs=False |
|||
) |
|||
loggings = db.relationship("Logging", backref="user", lazy="dynamic", cascade="delete") |
|||
|
|||
__mapper_args__ = {"order_by": name} |
|||
|
|||
def is_admin(self): |
|||
admin_role = db.session.query(Role).filter(Role.name == "admin").one() |
|||
return admin_role in self.roles |
|||
|
|||
def join_roles(self, string): |
|||
return string.join([i.description for i in self.roles]) |
|||
|
|||
@staticmethod |
|||
def generate_slug(target, value, oldvalue, initiator): |
|||
if value and (not target.slug or value != oldvalue): |
|||
target.slug = value |
|||
|
|||
def __repr__(self): |
|||
return f"<User(id='{self.id}', name='{self.name}')>" |
|||
|
|||
def check_password(self, password): |
|||
return verify_password(password, self.password) |
|||
|
|||
|
|||
event.listen(User.name, "set", User.generate_slug, retval=False) |
|||
|
|||
|
|||
@event.listens_for(User, "after_insert") |
|||
def generate_user_flakeid(mapper, connection, target): |
|||
if not target.flake_id: |
|||
flake_id = uuid.UUID(int=current_app.flake_id.get()) |
|||
connection.execute(User.__table__.update().where(User.__table__.c.id == target.id).values(flake_id=flake_id)) |
|||
|
|||
|
|||
user_datastore = SQLAlchemyUserDatastore(db, User, Role) |
|||
|
|||
|
|||
class Logging(db.Model): |
|||
__tablename__ = "logging" |
|||
|
|||
id = db.Column(db.Integer, primary_key=True) |
|||
category = db.Column(db.String(255), nullable=False, default="General") |
|||
level = db.Column(db.String(255), nullable=False, default="INFO") |
|||
message = db.Column(db.Text, nullable=False) |
|||
timestamp = db.Column( |
|||
db.DateTime(timezone=False), default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow |
|||
) |
|||
|
|||
user_id = db.Column(db.Integer(), db.ForeignKey("user.id"), nullable=True) |
|||
|
|||
__mapper_args__ = {"order_by": timestamp.desc()} |
@ -0,0 +1,13 @@ |
|||
[tool.black] |
|||
line-length = 120 |
|||
exclude = ''' |
|||
/( |
|||
\.git |
|||
| \.eggs |
|||
| \.idea |
|||
| \__pycache__ |
|||
| venv |
|||
| front |
|||
)/ |
|||
''' |
|||
skip-numeric-underscore-normalization = true |
@ -0,0 +1,21 @@ |
|||
psycopg2-binary==2.8.4 |
|||
texttable==1.6.2 |
|||
Unidecode==1.1.1 |
|||
flask==1.1.1 |
|||
python-slugify==4.0.0 |
|||
sqlalchemy==1.3.12 |
|||
bcrypt==3.1.7 |
|||
celery==4.4.0 |
|||
flask_mail==0.9.1 |
|||
flask_migrate==2.5.2 |
|||
git+https://github.com/maxcountryman/flask-uploads#egg=flask_uploads |
|||
sqlalchemy_searchable==1.1.0 |
|||
redis==3.3.11 |
|||
flask-accept==0.0.6 |
|||
PyQRCode==1.2.1 |
|||
flask-cors>=3.0.8 |
|||
cachetools==4.0.0 |
|||
flasgger==0.9.5 |
|||
waitress==1.4.3 |
|||
Flask-SQLAlchemy==2.4.1 |
|||
git+https://github.com/jwag956/flask-security#egg=Flask-Security-too |
@ -0,0 +1,26 @@ |
|||
[aliases] |
|||
test=pytest |
|||
|
|||
[tool:pytest] |
|||
addopts = --verbose --cov=. --cov-report=xml:test-reports/coverage.xml --junitxml=test-reports/junit.xml |
|||
python_files = tests/*.py |
|||
junit_family=legacy |
|||
|
|||
[flake8] |
|||
max-line-length = 120 |
|||
ignore = E501,W504,W503 |
|||
exclude = .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,venv,.eggs |
|||
|
|||
[coverage:run] |
|||
omit = |
|||
tests/* |
|||
.eggs/* |
|||
config.py |
|||
configtest.py |
|||
**/*__init__.py |
|||
venv/* |
|||
front/* |
|||
|
|||
[isort] |
|||
line_length=120 |
|||
force_single_line=false |
@ -0,0 +1,31 @@ |
|||
from setuptools import setup |
|||
|
|||
setup( |
|||
name="omnomnomnom", |
|||
version="0.1", |
|||
license="AGPL", |
|||
python_requires=">=3.6", |
|||
long_description=open("../README.md").read(), |
|||
url="http://dev.sigpipe.me/dashie/omnomnomnom", |
|||
author="Dashie", |
|||
author_email="dashie@sigpipe.me", |
|||
install_requires=[ |
|||
"Flask==1.1.1", |
|||
"SQLAlchemy==1.3.12", |
|||
"SQLAlchemy-Searchable==1.1.0", |
|||
"SQLAlchemy-Utils==0.36.1", |
|||
"Flask-Mail==0.9.1", |
|||
"Flask-Migrate==2.4.0", |
|||
"bcrypt==3.1.7", |
|||
"psycopg2-binary==2.8.4", |
|||
"unidecode==1.1.1", |
|||
"Flask_Babelex==0.9.4", |
|||
"texttable==1.6.1", |
|||
"python-slugify==4.0.0", |
|||
"redis==3.3.11", |
|||
"celery==4.4.0", |
|||
"flask-accept==0.0.6", |
|||
], |
|||
setup_requires=["pytest-runner"], |
|||
tests_require=["pytest==5.3.2", "pytest-cov==2.8.1"], |
|||
) |
@ -0,0 +1,82 @@ |
|||
import time |
|||
import threading |
|||
import os |
|||
import random |
|||
import binascii |
|||
import warnings |
|||
|
|||
""" |
|||
k-ordered ID generation thingy |
|||
adapted from https://git.pleroma.social/pleroma/flake_id |
|||
""" |
|||
|
|||
# Hack for python <=3.6; time_ns is >=3.7 |
|||
if not hasattr(time, "time_ns"): |
|||
time.time_ns = lambda: int(time.time() * 1e9) |
|||
|
|||
|
|||
# The FlakeId.get() should return a 128bit integer |
|||
# Which should be able to be fed to uuid.UUID(int=FlakeId.get()) |
|||
|
|||
|
|||
class FlakeId(object): |
|||
def __init__(self): |
|||
self.state_node = self.worker_id() |
|||
self.state_time = 0 |
|||
self.state_sq = 0 |
|||
|
|||
def worker_id(self): |
|||
"""Worker ID generated from PID, ThreadID and 16bit random thing""" |
|||
random_bits = random.getrandbits(16) |
|||
# more cursed random things |
|||
pid = os.getpid() |
|||
tid = threading.get_ident() |
|||
ptid = f"{pid}{random_bits}{tid}" |
|||
return binascii.crc32(ptid.encode("utf-8")) |
|||
|
|||
def time(self): |
|||
"""some length cursed timestamp""" |
|||
# docs don't seem to guarantee this to be 64bits so here's a bitmask |
|||
return time.time_ns() & 0xFFFF_FFFF_FFFF_FFFF |
|||
|
|||
def gen_flakeid(self): |
|||
fid_time = format(self.state_time, "064b") # 64bit |
|||
fid_node = format(self.state_node, "048b") # 48bit |
|||
fid_seq = format(self.state_sq, "016b") # 16bit |
|||
# return a big 128bit binary string |
|||
return f"{fid_time}{fid_node}{fid_seq}" |
|||
|
|||
def get(self): |
|||
"""Return a Flake ID""" |
|||
# Increment sequence on call |
|||
self.state_sq = (self.state_sq + 1) & 0xFFFF |
|||
# Get time |
|||
self.state_time = self.time() |
|||
# Get a Flake ID |
|||
flake_id = self.gen_flakeid() |
|||
return int(flake_id, 2) |
|||
|
|||
|
|||
def gen_flakeid(): |
|||
warnings.warn("Use the class FlakeId.get() instead", DeprecationWarning, stacklevel=2) |
|||
# (64 bits) timestamp in ns |
|||
t = time.time_ns() |
|||
|
|||
# (47 bits + 1 extra) "worker id", rand |
|||
# https://stackoverflow.com/questions/17125237/getrandbits-does-not-produce-constant-length-numbers |
|||
# computers are bad |
|||
w = random.getrandbits(47) + (1 << 47) |
|||
|
|||
pid = os.getpid() |
|||
tid = threading.get_ident() |
|||
ptid = f"{pid}{tid}" |
|||
|
|||
# (16b) sequence from PID+threadID |
|||
# might be possible to throw in the |
|||
# object id of the flask request |
|||
# 32bit crc |
|||
s = binascii.crc32(ptid.encode("utf-8")) |
|||
# butcher it |
|||
s = s & 0xFFFF |
|||
|
|||
return int(f"{t}{w}{s}") |
@ -0,0 +1,112 @@ |
|||
import datetime |
|||
import hashlib |
|||
from os.path import splitext |
|||
import random |
|||
import string |
|||
|
|||
from flask_security import current_user |
|||
|
|||
from models import db, Role, Logging |
|||
|
|||
|
|||
class InvalidUsage(Exception): |
|||
status_code = 400 |
|||
|
|||
def __init__(self, message, status_code=None, payload=None): |
|||
Exception.__init__(self) |
|||
self.message = message |
|||
if status_code is not None: |
|||
self.status_code = status_code |
|||
self.payload = payload |
|||
|
|||
def to_dict(self): |
|||
rv = dict(self.payload or ()) |
|||
rv["message"] = self.message |
|||
rv["status"] = "error" |
|||
rv["code"] = self.status_code |
|||
return rv |
|||
|
|||
|
|||
def is_admin(): |
|||
adm = Role.query.filter(Role.name == "admin").first() |
|||
if not current_user or not current_user.is_authenticated or not adm: |
|||
return False |
|||
if adm in current_user.roles: |
|||
return True |
|||
return False |
|||
|
|||
|
|||
def add_log(category, level, message): |
|||
if not category or not level or not message: |
|||
print("!! Fatal error in add_log() one of three variables not set") |
|||
print("[LOG][{0}][{1}] {2}".format(level.upper(), category, message)) |
|||
a = Logging(category=category, level=level.upper(), message=message) |
|||
db.session.add(a) |
|||
db.session.commit() |
|||
|
|||
|
|||
def duration_human(seconds): |
|||
if seconds is None: |
|||
return "error" |
|||
seconds = float(seconds) |
|||
minutes, seconds = divmod(seconds, 60) |
|||
hours, minutes = divmod(minutes, 60) |
|||
days, hours = divmod(hours, 24) |
|||
years, days = divmod(days, 365.242199) |
|||
|
|||
minutes = int(minutes) |
|||
hours = int(hours) |
|||
days = int(days) |
|||
years = int(years) |
|||
|
|||
if years > 0: |
|||
return "%d year" % years + "s" * (years != 1) |
|||
elif days > 0: |
|||
return "%d day" % days + "s" * (days != 1) |
|||
elif hours > 0: |
|||
return "%d hour" % hours + "s" * (hours != 1) |
|||
elif minutes > 0: |
|||
return "%d mn" % minutes + "s" * (minutes != 1) |
|||
else: |
|||
return "%.2f sec" % seconds + "s" * (seconds != 1) |
|||
|
|||
|
|||
def get_hashed_filename(filename): |
|||
f_n, f_e = splitext(filename) |
|||
|
|||
fs_fname = hashlib.sha256() |
|||
hashed_format = "%s-%s" % (f_n, datetime.datetime.now()) |
|||
fs_fname.update(hashed_format.encode("utf-8")) |
|||
fs_fname = fs_fname.hexdigest() |
|||
|
|||
return fs_fname + f_e |
|||
|
|||
|
|||
def generate_random_token(): |
|||
t = hashlib.sha256() |
|||
|
|||
magic_sauce = "".join([random.choice(string.ascii_letters + string.digits) for n in range(250)]) |
|||
magic_sauce += str(datetime.datetime.now()) |
|||
|
|||
t.update(magic_sauce.encode("utf-8")) |
|||
return t.hexdigest()[:250] |
|||
|
|||
|
|||
def join_url(start, end): |
|||
if end.startswith("http://") or end.startswith("https://"): |
|||
# alread a full URL, joining makes no sense |
|||
return end |
|||
if start.endswith("/") and end.startswith("/"): |
|||
return start + end[1:] |
|||
|
|||
if not start.endswith("/") and not end.startswith("/"): |
|||
return start + "/" + end |
|||
|
|||
return start + end |
|||
|
|||
|
|||
# Also present in models.py, thx circle import |
|||
def strip_end(text, suffix): |
|||
if not text.endswith(suffix): |
|||
return text |
|||
return text[: len(text) - len(suffix)] |
@ -0,0 +1 @@ |
|||
VERSION = "0.0.1" |
@ -0,0 +1,4 @@ |
|||
[python: api/**.py] |
|||
[jinja2: api/**/templates/**.jinja2] |
|||
[jinja2: api/**/templates/**.html] |
|||
extensions=jinja2.ext.autoescape,jinja2.ext.with_ |
Loading…
Reference in new issue