postgres automatic schema management roughly working
This commit is contained in:
commit
4a1924587c
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
notes.txt
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
instance/
|
||||||
|
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
.venv
|
41
README.md
Normal file
41
README.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# capsulflask
|
||||||
|
|
||||||
|
Python Flask web application for capsul.org
|
||||||
|
|
||||||
|
## postgres database schema management
|
||||||
|
|
||||||
|
capsulflask has a concept of a schema version. When the application starts, it will query the database for a table named
|
||||||
|
`schemaversion` that has one row and one column (`version`). If the `version` it finds is not equal to the `desiredSchemaVersion` variable set in `db.py`, it will run migration scripts from the `schema_migrations` folder one by one until the `schemaversion` table shows the correct version.
|
||||||
|
|
||||||
|
For example, the script named `02_up_xyz.sql` should contain code that migrates the database in a reverse-able fashion from schema version 1 to schema version 2. Likewise, the script `02_down_xyz.sql` should contain code that migrates from schema version 2 back to schema version 1.
|
||||||
|
|
||||||
|
**IMPORTANT: if you need to make changes to the schema, make a NEW schema version. DO NOT EDIT the existing schema versions.**
|
||||||
|
|
||||||
|
## how to run locally
|
||||||
|
|
||||||
|
Ensure you have the pre-requisites for the psycopg2 Postgres database adapter package
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt-get install python3-dev libpq-dev
|
||||||
|
pg_config --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Create python virtual environment and install packages
|
||||||
|
|
||||||
|
```
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Run an instance of Postgres (I used docker for this, point is its listening on localhost:5432)
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -it -e POSTGRES_PASSWORD=dev -p 5432:5432 postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the app
|
||||||
|
|
||||||
|
```
|
||||||
|
FLASK_APP=capsulflask flask run
|
||||||
|
```
|
24
capsulflask/__init__.py
Normal file
24
capsulflask/__init__.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
import os
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_mapping(
|
||||||
|
SECRET_KEY=os.environ.get("SECRET_KEY", default="dev"),
|
||||||
|
DATABASE_URL=os.environ.get("DATABASE_URL", default="sql://postgres:dev@localhost:5432/postgres"),
|
||||||
|
DATABASE_SCHEMA=os.environ.get("DATABASE_SCHEMA", default="public"),
|
||||||
|
)
|
||||||
|
|
||||||
|
from capsulflask import db
|
||||||
|
|
||||||
|
db.init_app(app)
|
||||||
|
|
||||||
|
# from capsulflask import auth, blog
|
||||||
|
|
||||||
|
# app.register_blueprint(auth.bp)
|
||||||
|
# app.register_blueprint(blog.bp)
|
||||||
|
|
||||||
|
app.add_url_rule("/", endpoint="index")
|
||||||
|
|
||||||
|
return app
|
117
capsulflask/db.py
Normal file
117
capsulflask/db.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import psycopg2
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from os import listdir
|
||||||
|
from os.path import isfile, join
|
||||||
|
from psycopg2 import pool
|
||||||
|
from flask import current_app
|
||||||
|
from flask import g
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app):
|
||||||
|
databaseUrl = urlparse(app.config['DATABASE_URL'])
|
||||||
|
|
||||||
|
app.config['PSYCOPG2_CONNECTION_POOL'] = psycopg2.pool.SimpleConnectionPool(
|
||||||
|
1,
|
||||||
|
20,
|
||||||
|
user = databaseUrl.username,
|
||||||
|
password = databaseUrl.password,
|
||||||
|
host = databaseUrl.hostname,
|
||||||
|
port = databaseUrl.port,
|
||||||
|
database = databaseUrl.path[1:]
|
||||||
|
)
|
||||||
|
|
||||||
|
schemaMigrations = {}
|
||||||
|
schemaMigrationsPath = join(app.root_path, 'schema_migrations')
|
||||||
|
print("loading schema migration scripts from {}".format(schemaMigrationsPath))
|
||||||
|
for filename in listdir(schemaMigrationsPath):
|
||||||
|
key = re.search(r"^\d+_(up|down)", filename).group()
|
||||||
|
with open(join(schemaMigrationsPath, filename), 'rb') as file:
|
||||||
|
schemaMigrations[key] = file.read().decode("utf8")
|
||||||
|
|
||||||
|
db = app.config['PSYCOPG2_CONNECTION_POOL'].getconn()
|
||||||
|
|
||||||
|
hasSchemaVersionTable = False
|
||||||
|
actionWasTaken = False
|
||||||
|
schemaVersion = 0
|
||||||
|
desiredSchemaVersion = 2
|
||||||
|
|
||||||
|
cursor = db.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT table_name, table_schema FROM information_schema.tables WHERE table_schema = '{}'
|
||||||
|
""".format(app.config['DATABASE_SCHEMA']))
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
if row[0] == "schemaversion":
|
||||||
|
hasSchemaVersionTable = True
|
||||||
|
|
||||||
|
if hasSchemaVersionTable == False:
|
||||||
|
print("no table named schemaversion found in the {} schema. running migration 01_up".format(app.config['DATABASE_SCHEMA']))
|
||||||
|
try:
|
||||||
|
cursor.execute(schemaMigrations["01_up"])
|
||||||
|
db.commit()
|
||||||
|
except:
|
||||||
|
print("unable to create the schemaversion table because: {}".format(my_exec_info_message(sys.exc_info())))
|
||||||
|
exit(1)
|
||||||
|
actionWasTaken = True
|
||||||
|
|
||||||
|
cursor.execute("SELECT Version FROM schemaversion")
|
||||||
|
schemaVersion = cursor.fetchall()[0][0]
|
||||||
|
|
||||||
|
# print(schemaVersion)
|
||||||
|
while schemaVersion < desiredSchemaVersion:
|
||||||
|
migrationKey = "%02d_up" % (schemaVersion+1)
|
||||||
|
print("schemaVersion ({}) < desiredSchemaVersion ({}). running migration {}".format(
|
||||||
|
schemaVersion, desiredSchemaVersion, migrationKey
|
||||||
|
))
|
||||||
|
try:
|
||||||
|
cursor.execute(schemaMigrations[migrationKey])
|
||||||
|
db.commit()
|
||||||
|
except KeyError:
|
||||||
|
print("missing schema migration script: {}_xyz.sql".format(migrationKey))
|
||||||
|
exit(1)
|
||||||
|
except:
|
||||||
|
print("unable to execute the schema migration {} because: {}".format(migrationKey, my_exec_info_message(sys.exc_info())))
|
||||||
|
exit(1)
|
||||||
|
actionWasTaken = True
|
||||||
|
|
||||||
|
schemaVersion += 1
|
||||||
|
cursor.execute("SELECT Version FROM schemaversion")
|
||||||
|
versionFromDatabase = cursor.fetchall()[0][0]
|
||||||
|
|
||||||
|
if schemaVersion != versionFromDatabase:
|
||||||
|
print("incorrect schema version value \"{}\" after running migration {}, expected \"{}\". exiting.".format(
|
||||||
|
versionFromDatabase,
|
||||||
|
migrationKey,
|
||||||
|
schemaVersion
|
||||||
|
))
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
app.config['PSYCOPG2_CONNECTION_POOL'].putconn(db)
|
||||||
|
|
||||||
|
print("schema migration completed. {}current schemaVersion: \"{}\"".format(
|
||||||
|
("" if actionWasTaken else "(no action was taken). "), schemaVersion
|
||||||
|
))
|
||||||
|
|
||||||
|
app.teardown_appcontext(close_db)
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
if 'db' not in g:
|
||||||
|
g.db = current_app.config['PSYCOPG2_CONNECTION_POOL'].getconn()
|
||||||
|
return g.db
|
||||||
|
|
||||||
|
|
||||||
|
def close_db(e=None):
|
||||||
|
db = g.pop("db", None)
|
||||||
|
|
||||||
|
if db is not None:
|
||||||
|
current_app.config['PSYCOPG2_CONNECTION_POOL'].putconn(db)
|
||||||
|
|
||||||
|
def my_exec_info_message(exec_info):
|
||||||
|
return "{}: {}".format(".".join([exec_info[0].__module__, exec_info[0].__name__]), exec_info[1])
|
@ -0,0 +1,5 @@
|
|||||||
|
CREATE TABLE schemaversion (
|
||||||
|
version INT PRIMARY KEY NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO schemaversion(version) VALUES (1);
|
1
capsulflask/schema_migrations/02_up_test.sql
Normal file
1
capsulflask/schema_migrations/02_up_test.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
UPDATE schemaversion SET version = 2;
|
17
requirements.txt
Normal file
17
requirements.txt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
astroid==2.4.1
|
||||||
|
click==7.1.2
|
||||||
|
Flask==1.1.2
|
||||||
|
isort==4.3.21
|
||||||
|
itsdangerous==1.1.0
|
||||||
|
Jinja2==2.11.2
|
||||||
|
lazy-object-proxy==1.4.3
|
||||||
|
MarkupSafe==1.1.1
|
||||||
|
mccabe==0.6.1
|
||||||
|
pkg-resources==0.0.0
|
||||||
|
psycopg2==2.8.5
|
||||||
|
pylint==2.5.2
|
||||||
|
six==1.14.0
|
||||||
|
toml==0.10.0
|
||||||
|
typed-ast==1.4.1
|
||||||
|
Werkzeug==1.0.1
|
||||||
|
wrapt==1.12.1
|
17
setup.cfg
Normal file
17
setup.cfg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[metadata]
|
||||||
|
name = capsulflask
|
||||||
|
version = 0.0.0
|
||||||
|
url = https://giit.cyberia.club/~forest/capsul-flask
|
||||||
|
license = BSD-3-Clause
|
||||||
|
maintainer = cyberia
|
||||||
|
maintainer_email = forest.n.johnson@gmail.com
|
||||||
|
description = Python Flask web application for capsul.org
|
||||||
|
long_description = file: README.md
|
||||||
|
long_description_content_type = text/markdown
|
||||||
|
|
||||||
|
[options]
|
||||||
|
packages = find:
|
||||||
|
include_package_data = true
|
||||||
|
install_requires =
|
||||||
|
Flask
|
||||||
|
psycopg2
|
Loading…
Reference in New Issue
Block a user