Initial commit
This commit is contained in:
10
app.py
10
app.py
@@ -1,12 +1,6 @@
|
|||||||
from flask import Flask
|
from eveai_app import create_app
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def hello_world(): # put application's code here
|
|
||||||
return 'Hello World!'
|
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
31
config.py
Normal file
31
config.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from os import environ, path
|
||||||
|
|
||||||
|
basedir = path.abspath(path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
DEBUG = False
|
||||||
|
DEVELOPMENT = False
|
||||||
|
SECRET_KEY = '97867c1491bea5ee6a8e8436eb11bf2ba6a69ff53ab1b17ecba450d0f2e572e1'
|
||||||
|
|
||||||
|
|
||||||
|
class DevConfig(Config):
|
||||||
|
DEVELOPMENT = True
|
||||||
|
DEBUG = True
|
||||||
|
SQLALCHEMY_DATABASE_URI = 'postgresql+pg8000://josako@localhost:5432/eveAI'
|
||||||
|
SQLALCHEMY_BINDS = {'public': 'postgresql+pg8000://josako@localhost:5432/eveAI'}
|
||||||
|
EXPLAIN_TEMPLATE_LOADING = True
|
||||||
|
|
||||||
|
|
||||||
|
class ProdConfig(Config):
|
||||||
|
DEVELOPMENT = False
|
||||||
|
DEBUG = False
|
||||||
|
# SQLALCHEMY_DATABASE_URI = environ.get('SQLALCHEMY_DATABASE_URI') or \
|
||||||
|
# 'sqlite:///' + os.path.join(basedir, 'db.sqlite')
|
||||||
|
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'dev': DevConfig(),
|
||||||
|
'prod': ProdConfig(),
|
||||||
|
'default': DevConfig(),
|
||||||
|
}
|
||||||
43
eveai_app/__init__.py
Normal file
43
eveai_app/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import os
|
||||||
|
from flask import Flask
|
||||||
|
from .extensions import db, migrate, bcrypt, bootstrap, jwt
|
||||||
|
from .models.user import User, Tenant
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(config_file=None):
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
if config_file is None:
|
||||||
|
app.config.from_object('config.DevConfig')
|
||||||
|
else:
|
||||||
|
app.config.from_object(config_file)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(app.instance_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
register_extensions(app)
|
||||||
|
register_blueprints(app)
|
||||||
|
|
||||||
|
print(app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def register_extensions(app):
|
||||||
|
db.init_app(app)
|
||||||
|
migrate.init_app(app, db)
|
||||||
|
bcrypt.init_app(app)
|
||||||
|
bootstrap.init_app(app)
|
||||||
|
jwt.init_app(app)
|
||||||
|
|
||||||
|
|
||||||
|
def register_blueprints(app):
|
||||||
|
from .views.user_views import user_bp
|
||||||
|
app.register_blueprint(user_bp)
|
||||||
|
|
||||||
|
|
||||||
|
def register_api(app):
|
||||||
|
pass
|
||||||
|
# from . import api
|
||||||
|
# app.register_blueprint(api.bp, url_prefix='/api')
|
||||||
4
eveai_app/api/__init__.py
Normal file
4
eveai_app/api/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# from flask import Blueprint, request
|
||||||
|
#
|
||||||
|
# public_api_bp = Blueprint("public", __name__, url_prefix="/api/v1")
|
||||||
|
# tenant_api_bp = Blueprint("tenant", __name__, url_prefix="/api/v1/tenant")
|
||||||
7
eveai_app/api/auth.py
Normal file
7
eveai_app/api/auth.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from flask import request
|
||||||
|
from flask.views import MethodView
|
||||||
|
|
||||||
|
class RegisterAPI(MethodView):
|
||||||
|
def post(self):
|
||||||
|
username = request.json['username']
|
||||||
|
|
||||||
12
eveai_app/extensions.py
Normal file
12
eveai_app/extensions.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
from flask_bcrypt import Bcrypt
|
||||||
|
from flask_bootstrap import Bootstrap
|
||||||
|
from flask_jwt_extended import JWTManager
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
migrate = Migrate()
|
||||||
|
bcrypt = Bcrypt()
|
||||||
|
bootstrap = Bootstrap()
|
||||||
|
jwt = JWTManager()
|
||||||
3
eveai_app/models/__init__.py
Normal file
3
eveai_app/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
0
eveai_app/models/document.py
Normal file
0
eveai_app/models/document.py
Normal file
0
eveai_app/models/interaction.py
Normal file
0
eveai_app/models/interaction.py
Normal file
61
eveai_app/models/user.py
Normal file
61
eveai_app/models/user.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
from ..extensions import db
|
||||||
|
|
||||||
|
|
||||||
|
class Tenant(db.Model):
|
||||||
|
"""Tenant model"""
|
||||||
|
|
||||||
|
__bind_key__ = 'public'
|
||||||
|
__table_args__ = {'schema': 'public'}
|
||||||
|
|
||||||
|
# Versioning Information
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||||
|
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
|
||||||
|
|
||||||
|
# company Information
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
|
website = db.Column(db.String(255), nullable=True)
|
||||||
|
|
||||||
|
# Licensing Information
|
||||||
|
license_start_date = db.Column(db.Date, nullable=True)
|
||||||
|
license_end_date = db.Column(db.Date, nullable=True)
|
||||||
|
allowed_monthly_interactions = db.Column(db.Integer, nullable=True)
|
||||||
|
|
||||||
|
# Relations
|
||||||
|
users = db.relationship('User', backref='tenant')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<Tenant %r>' % self.name
|
||||||
|
|
||||||
|
|
||||||
|
class User(db.Model):
|
||||||
|
"""User model"""
|
||||||
|
|
||||||
|
__bind_key__ = 'public'
|
||||||
|
__table_args__ = {'schema': 'public'}
|
||||||
|
|
||||||
|
# Versioning Information
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||||
|
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
|
||||||
|
|
||||||
|
# User Information
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_name = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
|
email = db.Column(db.String(255), unique=True, nullable=False)
|
||||||
|
password = db.Column(db.String(255), nullable=False)
|
||||||
|
first_name = db.Column(db.String(80), nullable=False)
|
||||||
|
last_name = db.Column(db.String(80), nullable=False)
|
||||||
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
|
is_tester = db.Column(db.Boolean, default=False)
|
||||||
|
is_admin = db.Column(db.Boolean, default=False)
|
||||||
|
valid_to = db.Column(db.Date, nullable=True)
|
||||||
|
|
||||||
|
# Login Information
|
||||||
|
last_login = db.Column(db.DateTime, nullable=True)
|
||||||
|
authenticated = db.Column(db.Boolean, default=False)
|
||||||
|
|
||||||
|
# Relations
|
||||||
|
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<User %r>' % self.name()
|
||||||
26
eveai_app/templates/base.html
Normal file
26
eveai_app/templates/base.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<!-- Required meta tags -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div><a href="/register">Register</a></div>
|
||||||
|
<div><a href="/login">Login</a></div>
|
||||||
|
<hr>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
<!-- Optional JavaScript -->
|
||||||
|
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29
eveai_app/templates/user/tenant.html
Normal file
29
eveai_app/templates/user/tenant.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Tenant Details{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form action="" method="post" novalidate>
|
||||||
|
<p>
|
||||||
|
{{ form.name.label }}<br>
|
||||||
|
{{ form.name(size=80) }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ form.website.label }}<br>
|
||||||
|
{{ form.website(size=80) }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ form.license_start_date.label }}<br>
|
||||||
|
{{ form.license_start_date(size=20) }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ form.license_end_date.label }}<br>
|
||||||
|
{{ form.license_end_date(size=20) }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ form.allowed_monthly_interactions.label }}<br>
|
||||||
|
{{ form.allowed_monthly_interactions(size=20) }}
|
||||||
|
</p>
|
||||||
|
<p>{{ form.submit() }}</p>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
6
eveai_app/views/__init__.py
Normal file
6
eveai_app/views/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import eveai_app.views.user_views
|
||||||
|
|
||||||
|
|
||||||
|
# document_bp = Blueprint('document_bp', __name__, url_prefix='document')
|
||||||
|
# interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='interaction')
|
||||||
|
|
||||||
26
eveai_app/views/user_forms.py
Normal file
26
eveai_app/views/user_forms.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, PasswordField, BooleanField, SubmitField, EmailField, IntegerField, DateField
|
||||||
|
from wtforms.validators import DataRequired, Length, Email, NumberRange
|
||||||
|
|
||||||
|
|
||||||
|
class TenantForm(FlaskForm):
|
||||||
|
name = StringField('Name', validators=[DataRequired(), Length(max=80)])
|
||||||
|
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
|
||||||
|
license_start_date = DateField('License Start Date', id='datepicker')
|
||||||
|
license_end_date = DateField('License End Date', id='datepicker')
|
||||||
|
allowed_monthly_interactions = IntegerField('Allowed Monthly Interactions', validators=[NumberRange(min=0)])
|
||||||
|
submit = SubmitField('Submit')
|
||||||
|
|
||||||
|
|
||||||
|
class UserForm(FlaskForm):
|
||||||
|
user_name = StringField('Name', validators=[DataRequired(), Length(max=80)])
|
||||||
|
email = EmailField('Email', validators=[DataRequired(), Email()])
|
||||||
|
password = PasswordField('Password', validators=[DataRequired(), Length(min=8)])
|
||||||
|
first_name = StringField('First Name', validators=[DataRequired(), Length(max=80)])
|
||||||
|
last_name = StringField('Last Name', validators=[DataRequired(), Length(max=80)])
|
||||||
|
is_active = BooleanField('Is Active')
|
||||||
|
is_tester = BooleanField('Is Tester')
|
||||||
|
is_admin = BooleanField('Is Administrator')
|
||||||
|
valid_to: DateField('Valid To', id='datepicker')
|
||||||
|
tenant = IntegerField('Tenant ID', validators=[NumberRange(min=0)])
|
||||||
|
submit = SubmitField('Submit')
|
||||||
106
eveai_app/views/user_views.py
Normal file
106
eveai_app/views/user_views.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# from . import user_bp
|
||||||
|
from datetime import datetime as dt, timezone as tz
|
||||||
|
from flask import request, redirect, url_for, flash, render_template, Blueprint
|
||||||
|
from ..models.user import User, Tenant
|
||||||
|
from ..extensions import db
|
||||||
|
from .user_forms import TenantForm, UserForm
|
||||||
|
|
||||||
|
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/tenant', methods=['GET', 'POST'])
|
||||||
|
def tenant():
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Handle the required attributes
|
||||||
|
name = request.form.get('name')
|
||||||
|
website = request.form.get('website')
|
||||||
|
error = None
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
error = 'Tenant name is required.'
|
||||||
|
elif not website:
|
||||||
|
error = 'Tenant website is required.'
|
||||||
|
|
||||||
|
# Create new tenant if there is no error
|
||||||
|
if error is None:
|
||||||
|
new_tenant = Tenant(name=name, website=website)
|
||||||
|
|
||||||
|
# Handle optional attributes
|
||||||
|
lic_start = request.form.get('license_start_date')
|
||||||
|
lic_end = request.form.get('license_end_date')
|
||||||
|
monthly = request.form.get('allowed_monthly_interactions')
|
||||||
|
|
||||||
|
if lic_start != '':
|
||||||
|
new_tenant.license_start_date = dt.strptime(lic_start, '%d-%m-%Y')
|
||||||
|
if lic_end != '':
|
||||||
|
new_tenant.license_end_date = dt.strptime(lic_end, '%d-%m-%Y')
|
||||||
|
if monthly != '':
|
||||||
|
new_tenant.allowed_monthly_interactions = int(monthly)
|
||||||
|
|
||||||
|
# Handle Timestamps
|
||||||
|
timestamp = dt.now(tz.utc)
|
||||||
|
new_tenant.created_at = timestamp
|
||||||
|
new_tenant.updated_at = timestamp
|
||||||
|
|
||||||
|
# Add the new tenant to the database and commit the changes
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(new_tenant)
|
||||||
|
db.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
error = e.args
|
||||||
|
|
||||||
|
flash(error) if error else flash('Tenant added successfully.')
|
||||||
|
|
||||||
|
form = TenantForm()
|
||||||
|
return render_template('user/tenant.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/user', methods=['GET', 'POST'])
|
||||||
|
def user():
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Handle the required attributes
|
||||||
|
username = request.form.get('username')
|
||||||
|
email = request.form.get('email')
|
||||||
|
password = request.form.get('password')
|
||||||
|
first_name = request.form.get('first_name')
|
||||||
|
last_name = request.form.get('last_name')
|
||||||
|
error = None
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
error = 'Username is required.'
|
||||||
|
elif not email:
|
||||||
|
error = 'Email is required.'
|
||||||
|
elif not password:
|
||||||
|
error = 'Password is required.'
|
||||||
|
elif not first_name:
|
||||||
|
error = 'First name is required.'
|
||||||
|
elif not last_name:
|
||||||
|
error = 'Last name is required.'
|
||||||
|
if error is None:
|
||||||
|
new_user = User(username=username, email=email, password=password, first_name=first_name, last_name=last_name)
|
||||||
|
|
||||||
|
# Handle optional attributes
|
||||||
|
new_user.is_active = request.form.get('is_active')
|
||||||
|
new_user.is_tester = request.form.get('is_tester')
|
||||||
|
new_user.is_admin = request.form.get('is_admin')
|
||||||
|
new_user.valid_to = request.form.get('valid_to')
|
||||||
|
|
||||||
|
# Handle Timestamps
|
||||||
|
timestamp = dt.now(tz.utc)
|
||||||
|
new_user.created_at = timestamp
|
||||||
|
new_user.updated_at = timestamp
|
||||||
|
|
||||||
|
# Handle the relations
|
||||||
|
new_user.tenant_id = request.form.get('tenant_id')
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
error = e.args
|
||||||
|
|
||||||
|
flash(error) if error else flash('User added successfully.')
|
||||||
|
|
||||||
|
form = UserForm()
|
||||||
|
return render_template('user/user.html', form=form)
|
||||||
1
migrations/public/README
Normal file
1
migrations/public/README
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Single-database configuration for Flask.
|
||||||
50
migrations/public/alembic.ini
Normal file
50
migrations/public/alembic.ini
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# 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,flask_migrate
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
[logger_flask_migrate]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = flask_migrate
|
||||||
|
|
||||||
|
[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
|
||||||
114
migrations/public/env.py
Normal file
114
migrations/public/env.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import logging
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine():
|
||||||
|
try:
|
||||||
|
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||||
|
return current_app.extensions['migrate'].db.get_engine()
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# this works with Flask-SQLAlchemy>=3
|
||||||
|
return current_app.extensions['migrate'].db.engine
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine_url():
|
||||||
|
try:
|
||||||
|
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||||
|
'%', '%%')
|
||||||
|
except AttributeError:
|
||||||
|
return str(get_engine().url).replace('%', '%%')
|
||||||
|
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||||
|
target_db = current_app.extensions['migrate'].db
|
||||||
|
|
||||||
|
# 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 get_metadata():
|
||||||
|
# JOS: Changed to use public schema
|
||||||
|
if hasattr(target_db, 'metadatas'):
|
||||||
|
return target_db.metadatas['public']
|
||||||
|
return target_db.metadata
|
||||||
|
|
||||||
|
|
||||||
|
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=get_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.')
|
||||||
|
|
||||||
|
conf_args = current_app.extensions['migrate'].configure_args
|
||||||
|
if conf_args.get("process_revision_directives") is None:
|
||||||
|
conf_args["process_revision_directives"] = process_revision_directives
|
||||||
|
|
||||||
|
connectable = get_engine()
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=get_metadata(),
|
||||||
|
**conf_args
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
24
migrations/public/script.py.mako
Normal file
24
migrations/public/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${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"}
|
||||||
1
migrations/tenant/README
Normal file
1
migrations/tenant/README
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Single-database configuration for Flask.
|
||||||
50
migrations/tenant/alembic.ini
Normal file
50
migrations/tenant/alembic.ini
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# 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,flask_migrate
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
[logger_flask_migrate]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = flask_migrate
|
||||||
|
|
||||||
|
[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
|
||||||
124
migrations/tenant/env.py
Normal file
124
migrations/tenant/env.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import logging
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
from eveai_app.models.user import Tenant
|
||||||
|
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine():
|
||||||
|
try:
|
||||||
|
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||||
|
return current_app.extensions['migrate'].db.get_engine()
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# this works with Flask-SQLAlchemy>=3
|
||||||
|
return current_app.extensions['migrate'].db.engine
|
||||||
|
|
||||||
|
|
||||||
|
def get_engine_url():
|
||||||
|
try:
|
||||||
|
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||||
|
'%', '%%')
|
||||||
|
except AttributeError:
|
||||||
|
return str(get_engine().url).replace('%', '%%')
|
||||||
|
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
# from myapp import mymodel
|
||||||
|
# target_metadata = mymodel.Base.metadata
|
||||||
|
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||||
|
target_db = current_app.extensions['migrate'].db
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# List of Tenants
|
||||||
|
tenants = [tenant.id for tenant in Tenant.query.all()]
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata():
|
||||||
|
if hasattr(target_db, 'metadatas'):
|
||||||
|
return target_db.metadatas[None]
|
||||||
|
return target_db.metadata
|
||||||
|
|
||||||
|
|
||||||
|
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=get_metadata(), literal_binds=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
"""Updated migration script for handling schema based multi-tenancy
|
||||||
|
|
||||||
|
ref:
|
||||||
|
- https://alembic.sqlalchemy.org/en/latest/cookbook.html#rudimental-schema-level-multi-tenancy-for-postgresql-databases # noqa
|
||||||
|
"""
|
||||||
|
|
||||||
|
connectable = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section),
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
for tenant in tenants:
|
||||||
|
logger.info(f"Migrating tenant: {tenant}")
|
||||||
|
# set search path on the connection, which ensures that
|
||||||
|
# PostgreSQL will emit all CREATE / ALTER / DROP statements
|
||||||
|
# in terms of this schema by default
|
||||||
|
connection.execute(text(f'SET search_path TO "{tenant}"'))
|
||||||
|
# in SQLAlchemy v2+ the search path change needs to be committed
|
||||||
|
# connection.commit()
|
||||||
|
|
||||||
|
# make use of non-supported SQLAlchemy attribute to ensure
|
||||||
|
# the dialect reflects tables in terms of the current tenant name
|
||||||
|
connection.dialect.default_schema_name = tenant
|
||||||
|
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=get_metadata(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
# for checking migrate or upgrade is running
|
||||||
|
if getattr(config.cmd_opts, "autogenerate", False):
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
raise Exception("Offline migrations are not supported")
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
24
migrations/tenant/script.py.mako
Normal file
24
migrations/tenant/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${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"}
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Flask~=3.0.3
|
||||||
|
WTForms~=3.1.2
|
||||||
|
SQLAlchemy~=2.0.29
|
||||||
|
alembic~=1.13.1
|
||||||
Reference in New Issue
Block a user