From 080263cca6b4604b443a35966d52947dd15d3dfe Mon Sep 17 00:00:00 2001 From: Drew Short Date: Tue, 15 May 2018 23:41:35 -0500 Subject: [PATCH] server: Working migrations with alembic --- server/Pipfile | 16 ++ server/Pipfile.lock | 192 ++++++++++++++++++++ server/atheneum/__init__.py | 16 +- server/atheneum/default_settings.py | 1 + server/atheneum/model/User.py | 0 server/atheneum/model/__init__.py | 1 + server/atheneum/model/user.py | 15 ++ server/migrations/README | 1 + server/migrations/alembic.ini | 45 +++++ server/migrations/env.py | 89 +++++++++ server/migrations/script.py.mako | 24 +++ server/migrations/versions/7160f2b96a1c_.py | 38 ++++ 12 files changed, 436 insertions(+), 2 deletions(-) create mode 100644 server/Pipfile create mode 100644 server/Pipfile.lock delete mode 100644 server/atheneum/model/User.py create mode 100644 server/atheneum/model/__init__.py create mode 100644 server/atheneum/model/user.py create mode 100755 server/migrations/README create mode 100644 server/migrations/alembic.ini create mode 100755 server/migrations/env.py create mode 100755 server/migrations/script.py.mako create mode 100644 server/migrations/versions/7160f2b96a1c_.py diff --git a/server/Pipfile b/server/Pipfile new file mode 100644 index 0000000..67737cc --- /dev/null +++ b/server/Pipfile @@ -0,0 +1,16 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +flask = ">=1.0,<1.1" +flask-sqlalchemy = ">=2.3,<2.4" +flask-migrate = ">=2.1,<2.2" +pynacl = ">=1.2,<1.3" + +[dev-packages] +python-dotenv = "*" + +[requires] +python_version = "3.6" diff --git a/server/Pipfile.lock b/server/Pipfile.lock new file mode 100644 index 0000000..cf40d45 --- /dev/null +++ b/server/Pipfile.lock @@ -0,0 +1,192 @@ +{ + "_meta": { + "hash": { + "sha256": "ea0b054cc713e78e3aef06fe53568b66bebd6a78f353b3a118547f7c765ce745" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "alembic": { + "hashes": [ + "sha256:85bd3ea7633024e4930900bc64fb58f9742dedbc6ebb6ecf25be2ea9a3c1b32e" + ], + "version": "==0.9.9" + }, + "cffi": { + "hashes": [ + "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", + "sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef", + "sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50", + "sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f", + "sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93", + "sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257", + "sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3", + "sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc", + "sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04", + "sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6", + "sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359", + "sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596", + "sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b", + "sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd", + "sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95", + "sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e", + "sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6", + "sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca", + "sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31", + "sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1", + "sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085", + "sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801", + "sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4", + "sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184", + "sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917", + "sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f", + "sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb" + ], + "version": "==1.11.5" + }, + "click": { + "hashes": [ + "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", + "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + ], + "version": "==6.7" + }, + "flask": { + "hashes": [ + "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", + "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "flask-migrate": { + "hashes": [ + "sha256:493f9b3795985b9b4915bf3b7d16946697f027b73545384e7d9e3a79f989d2fe", + "sha256:b709ca8642559c3c5a81a33ab10839fa052177accd5ba821047a99db635255ed" + ], + "index": "pypi", + "version": "==2.1.1" + }, + "flask-sqlalchemy": { + "hashes": [ + "sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b", + "sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53" + ], + "index": "pypi", + "version": "==2.3.2" + }, + "itsdangerous": { + "hashes": [ + "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" + ], + "version": "==0.24" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "mako": { + "hashes": [ + "sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae" + ], + "version": "==1.0.7" + }, + "markupsafe": { + "hashes": [ + "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" + ], + "version": "==1.0" + }, + "pycparser": { + "hashes": [ + "sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226" + ], + "version": "==2.18" + }, + "pynacl": { + "hashes": [ + "sha256:04e30e5bdeeb2d5b34107f28cd2f5bbfdc6c616f3be88fc6f53582ff1669eeca", + "sha256:0bfa0d94d2be6874e40f896e0a67e290749151e7de767c5aefbad1121cad7512", + "sha256:11aa4e141b2456ce5cecc19c130e970793fa3a2c2e6fbb8ad65b28f35aa9e6b6", + "sha256:13bdc1fe084ff9ac7653ae5a924cae03bf4bb07c6667c9eb5b6eb3c570220776", + "sha256:14339dc233e7a9dda80a3800e64e7ff89d0878ba23360eea24f1af1b13772cac", + "sha256:1d33e775fab3f383167afb20b9927aaf4961b953d76eeb271a5703a6d756b65b", + "sha256:2a42b2399d0428619e58dac7734838102d35f6dcdee149e0088823629bf99fbb", + "sha256:2dce05ac8b3c37b9e2f65eab56c544885607394753e9613fd159d5e2045c2d98", + "sha256:6453b0dae593163ffc6db6f9c9c1597d35c650598e2c39c0590d1757207a1ac2", + "sha256:73a5a96fb5fbf2215beee2353a128d382dbca83f5341f0d3c750877a236569ef", + "sha256:8abb4ef79161a5f58848b30ab6fb98d8c466da21fdd65558ce1d7afc02c70b5f", + "sha256:8ac1167195b32a8755de06efd5b2d2fe76fc864517dab66aaf65662cc59e1988", + "sha256:8f505f42f659012794414fa57c498404e64db78f1d98dfd40e318c569f3c783b", + "sha256:be71cd5fce04061e1f3d39597f93619c80cdd3558a6c9ba99a546f144a8d8101", + "sha256:cf6877124ae6a0698404e169b3ba534542cfbc43f939d46b927d956daf0a373a", + "sha256:d0eb5b2795b7ee2cbcfcadacbe95a13afbda048a262bd369da9904fecb568975", + "sha256:d795f506bcc9463efb5ebb0f65ed77921dcc9e0a50499dedd89f208445de9ecb", + "sha256:d8aaf7e5d6b0e0ef7d6dbf7abeb75085713d0100b4eb1a4e4e857de76d77ac45", + "sha256:e0d38fa0a75f65f556fb912f2c6790d1fa29b7dd27a1d9cc5591b281321eaaa9", + "sha256:eb2acabbd487a46b38540a819ef67e477a674481f84a82a7ba2234b9ba46f752", + "sha256:eeee629828d0eb4f6d98ac41e9a3a6461d114d1d0aa111a8931c049359298da0", + "sha256:f5ce9e26d25eb0b2d96f3ef0ad70e1d3ae89b5d60255c462252a3e456a48c053", + "sha256:fabf73d5d0286f9e078774f3435601d2735c94ce9e514ac4fb945701edead7e4" + ], + "index": "pypi", + "version": "==1.2.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", + "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8" + ], + "version": "==2.7.3" + }, + "python-editor": { + "hashes": [ + "sha256:a3c066acee22a1c94f63938341d4fb374e3fdd69366ed6603d7b24bed1efc565" + ], + "version": "==1.0.3" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "sqlalchemy": { + "hashes": [ + "sha256:d6cda03b0187d6ed796ff70e87c9a7dce2c2c9650a7bc3c022cd331416853c31" + ], + "version": "==1.2.7" + }, + "werkzeug": { + "hashes": [ + "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", + "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" + ], + "version": "==0.14.1" + } + }, + "develop": { + "python-dotenv": { + "hashes": [ + "sha256:4965ed170bf51c347a89820e8050655e9c25db3837db6602e906b6d850fad85c", + "sha256:509736185257111613009974e666568a1b031b028b61b500ef1ab4ee780089d5" + ], + "index": "pypi", + "version": "==0.8.2" + } + } +} diff --git a/server/atheneum/__init__.py b/server/atheneum/__init__.py index d42a82c..5adb2fb 100644 --- a/server/atheneum/__init__.py +++ b/server/atheneum/__init__.py @@ -1,6 +1,10 @@ import os from flask import Flask +from flask_migrate import Migrate +from flask_sqlalchemy import SQLAlchemy + +db: SQLAlchemy = SQLAlchemy() from atheneum.api import api_blueprint @@ -9,14 +13,14 @@ def create_app(test_config=None): app = Flask(__name__, instance_relative_config=True) app.config.from_mapping( SECRET_KEY='dev', - SQLALCHEMY_DATABASE_URI='sqlite:////tmp/test.db' + SQLALCHEMY_DATABASE_URI='sqlite:////tmp/atheneum-test.db' # SQLALCHEMY_DATABASE_URI=os.path.join(app.instance_path, 'atheneum.db') ) if test_config is None: app.config.from_object('atheneum.default_settings') app.config.from_pyfile('config.py', silent=True) - if (os.getenv('ATHENEUM_SERVER_SETTINGS', None)): + if os.getenv('ATHENEUM_SERVER_SETTINGS', None): app.config.from_envvar('ATHENEUM_SERVER_SETTINGS') else: app.config.from_pyfile(test_config) @@ -28,4 +32,12 @@ def create_app(test_config=None): app.register_blueprint(api_blueprint) + db.init_app(app) + migrate = Migrate(app, db) + return app + + +if __name__ == "__main__": + app = create_app() + app.run() \ No newline at end of file diff --git a/server/atheneum/default_settings.py b/server/atheneum/default_settings.py index 3ff613b..7e5eb6c 100644 --- a/server/atheneum/default_settings.py +++ b/server/atheneum/default_settings.py @@ -1,3 +1,4 @@ DEBUG = True SECRET_KEY = b'\xb4\x89\x0f\x0f\xe5\x88\x97\xfe\x8d<\x0b@d\xe9\xa5\x87%' \ b'\xc6\xf0@l1\xe3\x90g\xfaA.?u=s' # CHANGE ME IN REAL CONFIG +SQLALCHEMY_TRACK_MODIFICATIONS=False diff --git a/server/atheneum/model/User.py b/server/atheneum/model/User.py deleted file mode 100644 index e69de29..0000000 diff --git a/server/atheneum/model/__init__.py b/server/atheneum/model/__init__.py new file mode 100644 index 0000000..b0ea167 --- /dev/null +++ b/server/atheneum/model/__init__.py @@ -0,0 +1 @@ +from atheneum.model.user import User \ No newline at end of file diff --git a/server/atheneum/model/user.py b/server/atheneum/model/user.py new file mode 100644 index 0000000..e538c06 --- /dev/null +++ b/server/atheneum/model/user.py @@ -0,0 +1,15 @@ +from atheneum import db + + +class User(db.Model): + __tablename__ = 'user' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.Unicode(60), unique=True, nullable=False) + password_hash = db.Column('password_hash', db.Unicode(128), nullable=False) + password_dblt = db.Column('password_dblt', db.Unicode(32)) + password_revision = db.Column( + 'password_revision', db.SmallInteger, default=0, nullable=False) + creation_time = db.Column('creation_time', db.DateTime, nullable=False) + last_login_time = db.Column('last_login_time', db.DateTime) + version = db.Column('version', db.Integer, default=1, nullable=False) diff --git a/server/migrations/README b/server/migrations/README new file mode 100755 index 0000000..98e4f9c --- /dev/null +++ b/server/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/server/migrations/alembic.ini b/server/migrations/alembic.ini new file mode 100644 index 0000000..f8ed480 --- /dev/null +++ b/server/migrations/alembic.ini @@ -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 diff --git a/server/migrations/env.py b/server/migrations/env.py new file mode 100755 index 0000000..9be4103 --- /dev/null +++ b/server/migrations/env.py @@ -0,0 +1,89 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool + +# 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')) +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) + + 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.') + + engine = engine_from_config(config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + connection = engine.connect() + context.configure(connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args) + + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/server/migrations/script.py.mako b/server/migrations/script.py.mako new file mode 100755 index 0000000..2c01563 --- /dev/null +++ b/server/migrations/script.py.mako @@ -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"} diff --git a/server/migrations/versions/7160f2b96a1c_.py b/server/migrations/versions/7160f2b96a1c_.py new file mode 100644 index 0000000..ace5836 --- /dev/null +++ b/server/migrations/versions/7160f2b96a1c_.py @@ -0,0 +1,38 @@ +"""empty message + +Revision ID: 7160f2b96a1c +Revises: +Create Date: 2018-05-15 23:39:48.110843 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '7160f2b96a1c' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.Unicode(length=60), nullable=False), + sa.Column('password_hash', sa.Unicode(length=128), nullable=False), + sa.Column('password_dblt', sa.Unicode(length=32), nullable=True), + sa.Column('password_revision', sa.SmallInteger(), nullable=False), + sa.Column('creation_time', sa.DateTime(), nullable=False), + sa.Column('last_login_time', sa.DateTime(), nullable=True), + sa.Column('version', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('user') + # ### end Alembic commands ###