├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── flask_db ├── __init__.py ├── cli.py ├── init.py └── templates │ ├── alembic.ini │ └── db │ ├── __init__.py │ ├── env.py │ ├── script.py.mako │ ├── seeds.py │ └── versions │ └── .keep ├── setup.py └── tests └── example_app ├── README.md ├── alembic.ini ├── db ├── __init__.py ├── env.py ├── script.py.mako ├── seeds.py └── versions │ └── .keep ├── example ├── __init__.py └── app.py └── requirements.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | github: "nickjj" 4 | custom: ["https://www.paypal.me/nickjanetakis"] 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - "master" 8 | 9 | jobs: 10 | test: 11 | runs-on: "ubuntu-latest" 12 | 13 | steps: 14 | - uses: "actions/checkout@v2" 15 | 16 | - name: "Set up Python 3.11" 17 | uses: "actions/setup-python@v1" 18 | with: 19 | python-version: "3.11" 20 | 21 | - name: "Lint extension" 22 | run: | 23 | pip3 install flake8 24 | flake8 . 25 | 26 | - name: "Install dependencies and local package" 27 | run: | 28 | pip3 install Flask==2.3.3 alembic==1.12.0 29 | make install 30 | 31 | - name: "Ensure everything works" 32 | run: | 33 | cd tests/example_app 34 | 35 | rm -rf db/ 36 | rm alembic.ini 37 | sed -i "s|db/seeds.py|a/b/c/seeds.py|g" example/app.py 38 | 39 | flask db migrate -h 40 | flask db init a/b/c 41 | 42 | stat a/b/c/versions/.keep 43 | stat a/b/c/__init__.py 44 | stat a/b/c/script.py.mako 45 | stat a/b/c/seeds.py 46 | stat a/b/c/__init__.py 47 | 48 | grep -q "script_location = a/b/c" alembic.ini 49 | grep -q "example.app import create_app" a/b/c/env.py 50 | 51 | echo "print('make this file produce output')" >> a/b/c/seeds.py 52 | flask db reset --with-testdb > default_results 53 | stat exampledb 54 | stat exampledb_test 55 | [ -s default_results ] 56 | 57 | flask db migrate revision -m "cool" 58 | flask db migrate 59 | env: 60 | FLASK_APP: "example.app" 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python,osx 2 | 3 | ### OSX ### 4 | *.DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | 30 | ### Python ### 31 | # Byte-compiled / optimized / DLL files 32 | __pycache__/ 33 | *.py[cod] 34 | *$py.class 35 | 36 | # C extensions 37 | *.so 38 | 39 | # Distribution / packaging 40 | .Python 41 | build/ 42 | develop-eggs/ 43 | dist/ 44 | downloads/ 45 | eggs/ 46 | .eggs/ 47 | lib/ 48 | lib64/ 49 | parts/ 50 | sdist/ 51 | var/ 52 | wheels/ 53 | *.egg-info/ 54 | .installed.cfg 55 | *.egg 56 | 57 | # PyInstaller 58 | # Usually these files are written by a python script from a template 59 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 60 | *.manifest 61 | *.spec 62 | 63 | # Installer logs 64 | pip-log.txt 65 | pip-delete-this-directory.txt 66 | 67 | # Unit test / coverage reports 68 | htmlcov/ 69 | .tox/ 70 | .coverage 71 | .coverage.* 72 | .cache 73 | .pytest_cache/ 74 | nosetests.xml 75 | coverage.xml 76 | *.cover 77 | .hypothesis/ 78 | 79 | # Translations 80 | *.mo 81 | *.pot 82 | 83 | # End of https://www.gitignore.io/api/python,osx 84 | 85 | tests/example_app/exampledb 86 | tests/example_app/exampledb_test 87 | tests/example_app/example/exampledb 88 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a 6 | Changelog](https://keepachangelog.com/en/1.0.0/). 7 | 8 | ## [0.4.1] - 2024-01-09 9 | 10 | - Ensure `setuptools` is installed as a dependency for Python 3.12+ support 11 | 12 | ## [0.4.0] - 2023-09-13 13 | 14 | - In `.env.py`, remove `.db` from `app.extensions["sqlalchemy"]` to work with Flask-SQLAlchemy 3.1+ 15 | 16 | ## [0.3.2] - 2021-06-03 17 | 18 | ### Changed 19 | 20 | - Remove `$` in template placeholders to avoid certain code editor errors 21 | 22 | ## [0.3.1] - 2021-03-01 23 | 24 | ### Fixed 25 | 26 | - `alembic.ini.new` now gets its `script_location` replaced if `alembic.ini` existed 27 | 28 | ## [0.3.0] - 2020-11-27 29 | 30 | ### Removed 31 | 32 | - `--no-with-testdb` option for `flask db reset` since that is the behavior 33 | when `--with-testdb` is omit 34 | 35 | ## [0.2.0] - 2020-11-20 36 | 37 | ### Added 38 | 39 | - `flask db init` to generate Alembic config files and a `seeds.py` file 40 | - `flask db migrate` which forwards all commands straight to the `alembic` CLI 41 | 42 | ### Changed 43 | 44 | - `flask db init` in its original form has been replaced with `flask db reset` 45 | - `flask db seed` will fail with a helpful error if `seeds.py` cannot be found 46 | 47 | ## [0.1.1] - 2020-11-19 48 | 49 | ### Fixed 50 | 51 | - A flake8 malfunction that caused a syntax error in the example `seeds.py` file 52 | 53 | ## [0.1.0] - 2020-11-19 54 | 55 | ### Added 56 | 57 | - Everything! 58 | 59 | [Unreleased]: https://github.com/nickjj/flask-db/compare/0.4.1...HEAD 60 | [0.4.1]: https://github.com/nickjj/flask-db/compare/0.4.0...0.4.1 61 | [0.4.0]: https://github.com/nickjj/flask-db/compare/0.3.2...0.4.0 62 | [0.3.2]: https://github.com/nickjj/flask-db/compare/0.3.1...0.3.2 63 | [0.3.1]: https://github.com/nickjj/flask-db/compare/0.3.0...0.3.1 64 | [0.3.0]: https://github.com/nickjj/flask-db/compare/0.2.0...0.3.0 65 | [0.2.0]: https://github.com/nickjj/flask-db/compare/0.1.1...0.2.0 66 | [0.1.1]: https://github.com/nickjj/flask-db/compare/0.1.0...0.1.1 67 | [0.1.0]: https://github.com/nickjj/flask-db/releases/tag/0.1.0 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Nick Janetakis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | help: 3 | @printf "%s\n" "Useful targets:" 4 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m make %-20s\033[0m %s\n", $$1, $$2}' 5 | 6 | .PHONY: install 7 | install: ## Install package to dev environment 8 | pip3 install --user --editable . 9 | 10 | .PHONY: uninstall 11 | uninstall: ## Uninstall package from dev environment 12 | python3 setup.py develop --user -u 13 | 14 | .PHONY: clean 15 | clean: ## Remove build related files 16 | python3 setup.py sdist clean --all 17 | rm -rf build/ dist/ *.egg-info/ 18 | 19 | .PHONY: build 20 | build: ## Build package and wheel 21 | python3 setup.py sdist bdist_wheel 22 | 23 | .PHONY: publish 24 | publish: ## Publish package to PyPI 25 | twine upload dist/* 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is Flask-DB? ![CI](https://github.com/nickjj/flask-db/workflows/CI/badge.svg?branch=master) 2 | 3 | It's a Flask CLI extension that helps you migrate, drop, create and seed your 4 | SQL database. 5 | 6 | After installing it you'll gain access to a `flask db` command that will give 7 | you a few database management commands to use. 8 | 9 | For the migrations it uses Alembic. Anything you can do with Alembic can be 10 | done with this CLI extension. If you're wondering why you might want to use 11 | this tool instead of Alembic directly or Flask-Migrate, [check out this FAQ 12 | item](#differences-between-alembic-flask-migrate-flask-alembic-and-flask-db). 13 | 14 | ## Table of contents 15 | 16 | - [Demo video](#demo-video) 17 | - [Installation](#installation) 18 | - [Ensuring the `db` command is available](#ensuring-the-db-command-is-available) 19 | - [Going over the `db` command and its sub-commands](#going-over-the-db-command-and-its-sub-commands) 20 | - [FAQ](#faq) 21 | - [Differences between Alembic, Flask-Migrate, Flask-Alembic and Flask-DB](#differences-between-alembic-flask-migrate-flask-alembic-and-flask-db) 22 | - [Migrating from using Alembic directly or Flask-Migrate](#migrating-from-using-alembic-directly-or-flask-migrate) 23 | - [Is it safe to edit the files that `flask db init` created?](#is-it-safe-to-edit-the-files-that-flask-db-init-created) 24 | - [Should I add migration the files to version control?](#should-i-add-the-migration-files-to-version-control) 25 | - [I keep getting a module not found error when migrating](#i-keep-getting-a-module-not-found-error-when-migrating) 26 | - [About the Author](#about-the-author) 27 | 28 | ## Demo video 29 | 30 | Here's a run down on how this extension works and how you can add it to your 31 | project: 32 | 33 | [![Demo 34 | Video](https://img.youtube.com/vi/iDTmyF5HZ0Y/0.jpg)](https://www.youtube.com/watch?v=iDTmyF5HZ0Y) 35 | 36 | If you prefer reading instead of video this README file covers installing, 37 | configuring and using this extension too. 38 | 39 | ## Installation 40 | 41 | `pip3 install Flask-DB` 42 | 43 | That's it! 44 | 45 | There's no need to even import or initialize anything in your Flask app because 46 | it's just a CLI command that gets added to your Flask app. 47 | 48 | *But if you're curious, a complete example Flask app can be found in the 49 | [tests/ 50 | directory](https://github.com/nickjj/flask-db/tree/master/tests/example_app).* 51 | 52 | #### Requirements: 53 | 54 | - Python 3.6+ 55 | - Flask 1.0+ 56 | - SQLAlchemy 1.2+ 57 | - Alembic 1.3+ 58 | 59 | ## Ensuring the `db` command is available and everything works 60 | 61 | You'll want to at least set the `FLASK_APP` and `PYTHONPATH` environment 62 | variables: 63 | 64 | ```sh 65 | # Replace `hello.app` with your app's name. 66 | export FLASK_APP=hello.app 67 | 68 | # Due to how Alembic works you'll want to set this to be able to migrate 69 | # your database. It would be expected that you run your migrate commands from 70 | # the root of your project (more on this migrate command later). 71 | export PYTHONPATH="." 72 | 73 | export FLASK_DEBUG=true 74 | ``` 75 | 76 | Then run the `flask` binary to see its help menu: 77 | 78 | ```sh 79 | 80 | Usage: flask [OPTIONS] COMMAND [ARGS]... 81 | 82 | ... 83 | 84 | Commands: 85 | db Migrate and manage your SQL database. 86 | ``` 87 | 88 | If all went as planned you should see the new `db` command added to your 89 | list of commands. 90 | 91 | ## Going over the `db` command and its sub-commands 92 | 93 | Running `flask db --help` will produce this help menu: 94 | 95 | ```sh 96 | Usage: flask db [OPTIONS] COMMAND [ARGS]... 97 | 98 | Migrate and manage your SQL database. 99 | 100 | Options: 101 | --help Show this message and exit. 102 | 103 | Commands: 104 | init Generate Alembic config files and seeds.py. 105 | reset Drop, create and seed your database (careful in production). 106 | seed Seed the database with your custom records. 107 | migrate Wrap the alembic CLI tool (defaults to running upgrade head). 108 | ``` 109 | 110 | ### `init` 111 | 112 | If you've ever used Alembic before, you'll be familiar with `alembic init`. If 113 | you've never used it before it creates an `alembic.ini` file in the root of 114 | your project along with a few other Alembic files in a `yourappname/migrations` 115 | directory by default. 116 | 117 | You can treat `flask db init` as a drop in replacement for `alembic init`. 118 | 119 | `flask db init` does something similar. The 4 main differences are: 120 | 121 | 1. It defaults to `db/` instead of `yourappname/migrations`, but you can modify 122 | this path by running `flask db init any/path/you/want`. 123 | 124 | 2. It will create the same Alembic config files but it modifies them to be a 125 | bit more generic and portable when it comes to finding your 126 | `SQLALCHEMY_DATABASE_URI`. 127 | 128 | 3. It also configures Alembic to support auto generating migrations in case you 129 | want to use `revision --autogenerate` as a starting point for your 130 | migrations (always review them afterwards!). 131 | 132 | 4. It creates a `seeds.py` file in the same directory you initialized things to 133 | (more on this soon). 134 | 135 | ### `reset` 136 | 137 | Creates your database if it needs to be created along with doing a 138 | `db.drop_all()` and `db.create_all()`. That is going to purge all of your 139 | existing data and create any tables based on whatever SQLAlchemy models you 140 | have. 141 | 142 | It also automatically calls `flask db seed` for you (more on seeding soon). 143 | 144 | #### Options 145 | 146 | ``` 147 | Options: 148 | --with-testdb Create a test DB in addition to your main DB? 149 | ``` 150 | 151 | If you run `flask db reset --with-testdb` then a second database will also be 152 | created based on whatever your database name is from your 153 | `SQLALCHEMY_DATABASE_URI` along with appending `_test` to its name. 154 | 155 | For example, if you had a db named `hello` and you used `--with-testdb` then 156 | you would end up with both a `hello` and `hello_test` database. It's very 157 | useful to have a dedicated test database so you don't clobber your dev data. 158 | 159 | #### When should you use this command? 160 | 161 | That depends on the state of your project. 162 | 163 | ##### Brand new project that you never deployed? 164 | 165 | This is something you'll be running all the time in development as you change 166 | your database models. 167 | 168 | It's also something you'd typically run once in production the first time you 169 | deploy your app. This will handle creating your database and syncing all of 170 | your models as tables in your database. 171 | 172 | ##### Deployed your app at least once? 173 | 174 | Purging all of your data isn't an option anymore. If you want to make database 175 | changes you should be creating database migrations with `flask db migrate`, 176 | which is really running [Alembic](https://alembic.sqlalchemy.org/en/latest/) 177 | commands behind the scenes. That is the official migration tool created by the 178 | same folks who made SQLAlchemy. 179 | 180 | We'll go over the `migrate` command shortly. 181 | 182 | ### `seed` 183 | 184 | Seeds your database with initial records of your choosing. 185 | 186 | When you set up your app for the first time in production chances are you'll 187 | want certain things to be created in your database. At the very least probably 188 | an initial admin user. 189 | 190 | This command will read in and execute a `seeds.py` file that exists in the 191 | directory that you initialized with `flask db init`. By default that will be 192 | `db/seeds.py`. 193 | 194 | If you supplied a custom init path you must change your seeds path. You can do 195 | that by setting `FLASK_DB_SEEDS_PATH` in your app's config. For example if you 196 | ran `flask db init yourappname/migrations` then you'd set `FLASK_DB_SEEDS_PATH 197 | = "yourappname/migrations/seeds.py"`. 198 | 199 | As for the seeds file, you have full control over what you want to do. You can 200 | add whatever records that make sense for your app or keep the file empty to not 201 | seed anything. It's simply a file that's going to get called and execute any 202 | code you've placed into it. 203 | 204 | #### Best practices for seeding data 205 | 206 | It's a good idea to make your seeds file idempotent. Meaning, if you run it 1 207 | time or 100 times the end result should be the same. The [example in the seeds 208 | file](https://github.com/nickjj/flask-db/tree/master/tests/example_app/db/seeds.py) 209 | is idempotent because it first checks to see if the user exists before adding 210 | it. 211 | 212 | If the user exists, it skips trying to create the user. Without this check then 213 | you would end up getting a database uniqueness constraint error on the 2nd run. 214 | That's because the example test app added a [unique index on the 215 | username](https://github.com/nickjj/flask-db/blob/master/tests/example_app/example/app.py#L20). 216 | 217 | ### `migrate` 218 | 219 | The `flask db migrate` command is an alias to the `alembic` command. 220 | 221 | Here's a few examples: 222 | 223 | ```sh 224 | flask db migrate revision -m "hi" -> alembic revision -m "hi" 225 | flask db migrate upgrade head -> alembic upgrade head 226 | flask db migrate -> alembic upgrade head (convenience shortcut) 227 | flask db migrate -h -> alembic -h 228 | ``` 229 | 230 | Every possible thing you can run with `alembic` can now be run with `flask db 231 | migrate`. That means you can follow Alembic's tutorials and documentation 232 | exactly. 233 | 234 | You can still use alembic commands directly if you prefer. I just liked the 235 | idea of having all db related commands under the same namespace and the `flask 236 | db migrate` shortcut is handy. 237 | 238 | You'll also be happy to hear that this tool doesn't hard code all of Alembic's 239 | commands, options and arguments with hundreds of lines of code. What that means 240 | is, as Alembic's CLI API changes in the future this `flask db migrate` command 241 | will continue to work with all future versions. 242 | 243 | All it does is forward everything you pass in, similar to `$@` in Bash. 244 | 245 | That's also why you need to run `-h` to get Alembic's help menu instead of 246 | `--help` because that will ultimately call the internal help menu for `flask db 247 | migrate`. 248 | 249 | ## FAQ 250 | 251 | ### Differences between Alembic, Flask-Migrate, Flask-Alembic and Flask-DB 252 | 253 | Here's the breakdown of these tools: 254 | 255 | *Disclaimer: I'm not here to crap on the work of others, I'm simply giving my 256 | opinion on why I chose to create Flask-DB and don't use the Alembic CLI 257 | directly, Flask-Migrate or Flask-Alembic.* 258 | 259 | #### Alembic 260 | 261 | The official database migration tool for SQLAlchemy written by the same team 262 | who made SQLAlchemy. 263 | 264 | Alembic generates a bunch of config files and often times you'll want to go in 265 | and edit them to be more dynamic for finding your `SQLALCHEMY_DATABASE_URI` as 266 | well as adding a few useful opinions that you'll want in 99.999% of apps. 267 | 268 | It got a little tedious making these adjustments in every app. 269 | 270 | Besides having config files, Alembic also includes an `alembic` CLI tool to 271 | interact with your database such as generating migration files, executing 272 | migrations and more. This tool is fantastic. 273 | 274 | Flask-Migrate, Flask-Alembic and Flask-DB all use Alembic behind the scenes, 275 | but they are implemented in much different ways. 276 | 277 | #### Flask-Migrate 278 | 279 | At the time of writing this FAQ item (November 2020) the approach this library 280 | takes it to import the Alembic Python module and then build a custom CLI tool 281 | with various commands, options and arguments that lets you access functionality 282 | that Alembic already provides. 283 | 284 | In my opinion this is very error prone because Alembic already has a battle 285 | hardened `alembic` CLI tool that comes part of the Alembic Python package. You 286 | get this out of the box when you `pip3 install alembic`. Flask-Migrate is error 287 | prone because it has hundreds upon hundreds of lines of code re-creating the 288 | same `alembic` CLI tool that already exists. 289 | 290 | Also it's not future proof because if Alembic decides to change its API or add 291 | new features now you'll need to wait for Flask-Migrate to update all of its 292 | code and push a new release instead of just grabbing the new `alembic` package. 293 | 294 | For example, here's a tiny snippet of code from [Flask-Migrate's `migrate` 295 | command](https://github.com/miguelgrinberg/Flask-Migrate/blob/4887bd53bc08f10087fe27a4a7d9fe853031cdcf/flask_migrate/cli.py#L64=L90): 296 | 297 | ```py 298 | @db.command() 299 | @click.option('-d', '--directory', default=None, 300 | help=('Migration script directory (default is "migrations")')) 301 | @click.option('-m', '--message', default=None, help='Revision message') 302 | @click.option('--sql', is_flag=True, 303 | help=('Don\'t emit SQL to database - dump to standard output ' 304 | 'instead')) 305 | @click.option('--head', default='head', 306 | help=('Specify head revision or @head to base new ' 307 | 'revision on')) 308 | @click.option('--splice', is_flag=True, 309 | help=('Allow a non-head revision as the "head" to splice onto')) 310 | @click.option('--branch-label', default=None, 311 | help=('Specify a branch label to apply to the new revision')) 312 | @click.option('--version-path', default=None, 313 | help=('Specify specific path from config for version file')) 314 | @click.option('--rev-id', default=None, 315 | help=('Specify a hardcoded revision id instead of generating ' 316 | 'one')) 317 | @click.option('-x', '--x-arg', multiple=True, 318 | help='Additional arguments consumed by custom env.py scripts') 319 | @with_appcontext 320 | def migrate(directory, message, sql, head, splice, branch_label, version_path, 321 | rev_id, x_arg): 322 | """Autogenerate a new revision file (Alias for 'revision --autogenerate')""" 323 | _migrate(directory, message, sql, head, splice, branch_label, version_path, 324 | rev_id, x_arg) 325 | ``` 326 | 327 | Needless to say when you do this for a dozen commands with many dozens of flags 328 | it's easy for errors to slip by. 329 | 330 | Another thing is Flask-Migrate's `flask db migrate` command defaults to using 331 | auto-generated migrations. This feature is useful for speeding up the process 332 | of creating migration files but Alembic doesn't detect everything which can be 333 | very confusing if you're not aware of [Alembic's limitations with 334 | auto-generate](https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect). 335 | 336 | There's also no help for doing things like resetting and seeding your database. 337 | It's a tool exclusively designed for running database migrations with Alembic. 338 | 339 | #### Flask-Alembic 340 | 341 | This tool internally imports the Alembic Python library and creates its own CLI 342 | around that. It's similar to Flask-Migrate in that regard but not of all of the 343 | commands are API compatible with the `alembic` CLI. 344 | 345 | For example, here's a snippet from [Flask-Alembic's `revision` 346 | command](https://github.com/davidism/flask-alembic/blob/c8f202d4522123760a52865b6d3806470fa396e9/src/flask_alembic/cli/script.py#L94-L117): 347 | 348 | ```py 349 | @manager.option("message") 350 | @manager.option("--empty", action="store_true", help="Create empty script.") 351 | @manager.option( 352 | "-b", "--branch", default="default", help="Use this independent branch name." 353 | ) 354 | @manager.option( 355 | "-p", 356 | "--parent", 357 | default="head", 358 | type=str.split, 359 | help="Parent revision(s) of this revision.", 360 | ) 361 | @manager.option("--splice", action="store_true", help="Allow non-head parent revision.") 362 | @manager.option( 363 | "-d", "--depend", type=str.split, help="Revision(s) this revision depends on." 364 | ) 365 | @manager.option( 366 | "-l", "--label", type=str.split, help="Label(s) to apply to the revision." 367 | ) 368 | @manager.option("--path", help="Where to store the revision.") 369 | def revision(message, empty, branch, parent, splice, depend, label, path): 370 | """Create new migration.""" 371 | 372 | base.revision(message, empty, branch, parent, splice, depend, label, path) 373 | ``` 374 | 375 | It's missing a few flags that `alembic revision --help` would provide you. 376 | Personally I'd rather use the official `alembic` CLI since it already exists 377 | and it's very well tested and developed by the Alembic team. 378 | 379 | Also, it doesn't handle `alembic init` or have its own form of `init` so you're 380 | left having to generate and modify the default Alembic config files in every 381 | project. 382 | 383 | Like Flask-Migrate it also doesn't help with resetting and seeding your 384 | database. 385 | 386 | #### Flask-DB 387 | 388 | Flask-DB is more than a database migration tool that wraps Alembic. It also 389 | includes being able to reset and seed your database. 390 | 391 | Unlike using Alembic directly for the `init` command it modernizes and applies 392 | a few opinions to the default Alembic configuration so that you can usually use 393 | these files as is in your projects without modification. If you do need to 394 | modify them, that's ok. They are generated in your project which you can edit. 395 | 396 | As for migrations, it aliases the `alembic` CLI but it does it with about 5 397 | lines of code for everything rather than hundreds of lines of code. The lines 398 | of code aren't that important, it's mainly being less error prone and more 399 | future proof. It's taking advantage of the well tested `alembic` CLI tool that 400 | Alembic gave us. 401 | 402 | That means if a future version of Alembic comes out that adds new features it 403 | will automatically work with Flask-DB without having to update Flask-DB. All 404 | you would do is upgrade your `alembic` package version and you're done. 405 | 406 | Since about 2015 I used to create my own `db` CLI command in each project which 407 | handled resetting and seeding the database. Then I used the `alembic` CLI 408 | directly. Anyone who has taken my [Build a SAAS App with Flask 409 | course](https://buildasaasappwithflask.com/?utm_source=github&utm_medium=flaskdb&utm_campaign=readme) 410 | is probably used to that. 411 | 412 | Flask-DB was created as an improvement to that work flow. Now it's as easy as 413 | pip installing this tool and you're good to go with ready to go configs, a 414 | consistent Alembic CLI API and the added reset + seed functionality. 415 | 416 | ### Migrating from using Alembic directly or Flask-Migrate 417 | 418 | There's 2 ways to go about this and it's up to you on which one to pick. 419 | 420 | *In both cases if you're using Flask-Migrate you'll want to `pip3 uninstall 421 | Flask-Migrate` since both Flask-DB and Flask-Migrate add a `db` command to 422 | Flask.* 423 | 424 | #### 1. Keep all of your existing Alembic files and directory structure 425 | 426 | By default Alembic (and therefore Flask-Migrate) will put a `migrations/` 427 | directory inside of your application. 428 | 429 | You can still keep your files there with Flask-DB too. You'll just need to 430 | configure `FLASK_DB_SEEDS_PATH` to be `yourappname/migrations/seeds.py`. You'll 431 | also want to create that file manually (keeping it empty for now is ok). 432 | 433 | That's pretty much all you need to do. 434 | 435 | If you're using Flask-Migrate, Flask-DB has more up to date Alembic config 436 | files so you may still want to initialize a new directory with `flask db init`. 437 | Then you can pick and choose what you want to keep from your existing Alembic 438 | configs and what's been generated by the Flask-DB's init command. 439 | 440 | #### 2. Initialize a new directory and move your `versions/` into it (recommended) 441 | 442 | The other option would be to run `flask db init` and let it create a `db/` 443 | directory in the root of your project. 444 | 445 | Chances are you already have an `alembic.ini` file. The init command will 446 | recognize that and create an `alembic.ini.new` file to avoid clobbering your 447 | existing file. Then you can delete your old `alembic.init` file and `mv 448 | alembic.ini.new alembic.ini` to use the new one. 449 | 450 | From here you can take your existing migration files in your old `versions/` 451 | directory and move them into `db/versions/`. At this point you can delete your 452 | old `yourappname/migrations/` directory as long as you haven't done any drastic 453 | customizations to your alembic related config files. 454 | 455 | If you've done a bunch of custom configurations that's ok, you can manually 456 | merge in your changes. You're free to edit these files however you see fit. 457 | 458 | ### Is it safe to edit the files that `flask db init` created? 459 | 460 | Absolutely. The init command is there to set up the initial files that Alembic 461 | expects to be created, along with adding things like the `seeds.py` file too. 462 | 463 | The Alembic files will likely be good to go for you without having to modify 464 | anything but you can change them however you see fit. One thing worth 465 | mentioning is if your app factory function isn't called `create_app` you will 466 | want to change the `db/env.py` file's import near the top to use your custom 467 | factory function's name instead. 468 | 469 | You also have free reign to add whatever you want to the `seeds.py` file. By 470 | default it generates a couple of comments to help show how you can use it 471 | generate an initial user in your database. 472 | 473 | ### Should I add the migration files to version control? 474 | 475 | Yes! Your `alembic.ini` along with your entire `db/` directory (default `init` 476 | directory) including the `versions/*` files should all be commit to version 477 | control. 478 | 479 | This way you can develop and test your migrations locally on your dev box, make 480 | sure everything works then commit and push your code. At that point you can run 481 | your migrations in CI and ultimately in production with confidence that it all 482 | works. 483 | 484 | ### I keep getting a module not found error when migrating 485 | 486 | You might have seen this error: `ModuleNotFoundError: No module named 487 | 'yourappname'` 488 | 489 | Chances are you haven't set your `PYTHONPATH="."` env variable. If you're using 490 | Docker you can set this in your Dockerfile and you'll be good to go. Here's a 491 | working example from my [Build a SAAS App with 492 | Flask](https://github.com/nickjj/build-a-saas-app-with-flask/blob/f1ccd0aaae39cce89e53989dd498d97e1de95820/Dockerfile#L55) 493 | repo. 494 | 495 | If you're not using Docker please make sure that your `PYTHONPATH` environment 496 | variable is available on your shell by doing any of the things below: 497 | 498 | - `export PYTHONPATH="."` in your terminal and now it'll be set until you close your terminal 499 | - Create a `.env` file and then run `source .env` and it'll be set until you close your terminal 500 | - Run `PYTHONPATH="." flask db migrate` instead of `flask db migrate` 501 | 502 | In all cases it would be expected that you run the `flask db migrate` command 503 | from the root of your project. That's the same directory as where your 504 | `alembic.ini` file is located. 505 | 506 | ## About the author 507 | 508 | - Nick Janetakis | | [@nickjanetakis](https://twitter.com/nickjanetakis) 509 | 510 | If you're interested in learning Flask I have a 20+ hour video course called 511 | [Build a SAAS App with 512 | Flask](https://buildasaasappwithflask.com/?utm_source=github&utm_medium=flaskdb&utm_campaign=readme). 513 | It's a course where we build a real world SAAS app. Everything about the course 514 | and demo videos of what we build is on the site linked above. 515 | -------------------------------------------------------------------------------- /flask_db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickjj/flask-db/f964ca1a45b4db0a13f3e1ba678048b91544d57f/flask_db/__init__.py -------------------------------------------------------------------------------- /flask_db/cli.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | 4 | import click 5 | 6 | from flask import current_app 7 | from flask.cli import with_appcontext 8 | from sqlalchemy_utils import database_exists, create_database 9 | 10 | from flask_db.init import generate_configs 11 | 12 | 13 | DEFAULT_SEEDS_PATH = os.path.join("db", "seeds.py") 14 | 15 | 16 | @click.group() 17 | def db(): 18 | """ 19 | Migrate and manage your SQL database. 20 | """ 21 | pass 22 | 23 | 24 | @db.command() 25 | @click.argument("path", default="db/") 26 | @with_appcontext 27 | def init(path): 28 | """ 29 | Generate Alembic config files and seeds.py. 30 | """ 31 | copied_files, existing_files = generate_configs(path, 32 | current_app.import_name) 33 | 34 | if copied_files is not None: 35 | msg = f"""alembic.ini was created in the root of your project 36 | {path} was created with your Alembic configs, versions/ directory and seeds.py 37 | """ 38 | click.echo(msg) 39 | 40 | if existing_files: 41 | click.echo("Also, you already had these files in your project:\n") 42 | 43 | for file in existing_files: 44 | click.echo(f" {file[0]}") 45 | 46 | msg = """ 47 | Instead of aborting or erroring out, a new version of any existing files have 48 | been created with a .new file extension in their respective directories. 49 | 50 | Now you can diff them and decide on what to do next. 51 | 52 | Chances are you'll want to use the .new version of any file but if you have 53 | any custom Alembic configuration you may want to copy those changes over. 54 | 55 | If you want to use the .new file as is you can delete your original file and 56 | then rename the .new file by removing its .new file extension.""" 57 | click.echo(msg) 58 | 59 | return None 60 | 61 | 62 | @db.command() 63 | @click.option("--with-testdb", is_flag=True, 64 | help="Create a test DB in addition to your main DB?") 65 | @click.pass_context 66 | @with_appcontext 67 | def reset(ctx, with_testdb): 68 | """ 69 | Drop, create and seed your database (careful in production). 70 | """ 71 | db = current_app.extensions["sqlalchemy"] 72 | db_uri = current_app.config["SQLALCHEMY_DATABASE_URI"] 73 | 74 | if not database_exists(db_uri): 75 | create_database(db_uri) 76 | 77 | db.drop_all() 78 | db.create_all() 79 | 80 | if with_testdb: 81 | db_uri = f"{db_uri}_test" 82 | 83 | if not database_exists(db_uri): 84 | create_database(db_uri) 85 | 86 | ctx.invoke(seed) 87 | 88 | return None 89 | 90 | 91 | @db.command() 92 | @with_appcontext 93 | def seed(): 94 | """ 95 | Seed the database with your custom records. 96 | """ 97 | seeds_path = current_app.config.get("FLASK_DB_SEEDS_PATH", 98 | DEFAULT_SEEDS_PATH) 99 | 100 | if os.path.isfile and os.path.exists(seeds_path): 101 | exec(open(seeds_path).read()) 102 | else: 103 | msg = f"""{seeds_path} does not exist 104 | 105 | If you haven't done so already, run: flask db init 106 | 107 | If you're using a custom init path (ie. not db/ (the default)) then you can 108 | define a custom seeds path by setting FLASK_DB_SEEDS_PATH in your app's config. 109 | 110 | For example if you did flask db init myapp/migrations then you would want 111 | to set FLASK_DB_SEEDS_PATH = "myapp/migrations/seeds.py".""" 112 | click.echo(msg) 113 | 114 | return None 115 | 116 | 117 | @db.command(context_settings=dict(ignore_unknown_options=True)) 118 | @click.argument("alembic_args", nargs=-1, type=click.UNPROCESSED) 119 | @with_appcontext 120 | def migrate(alembic_args): 121 | """Wrap the alembic CLI tool (defaults to running upgrade head).""" 122 | if not alembic_args: 123 | alembic_args = ("upgrade", "head") 124 | 125 | cmdline = ["alembic"] + list(alembic_args) 126 | 127 | subprocess.call(cmdline) 128 | 129 | return None 130 | -------------------------------------------------------------------------------- /flask_db/init.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import os 3 | 4 | from shutil import copy 5 | from distutils.dir_util import copy_tree 6 | 7 | 8 | TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "templates") 9 | existing_files = [] 10 | copied_files = [] 11 | 12 | 13 | def cp_path(src, dst): 14 | src = os.path.join(TEMPLATE_PATH, src) 15 | 16 | if os.path.isfile(src): 17 | # We never want to clobber or disrupt existing files that may exist. 18 | if os.path.exists(dst): 19 | dst = append_new_extension(dst) 20 | 21 | result = copy(src, dst) 22 | copied_files.append(dst) 23 | else: 24 | result = copy_tree(src, dst) 25 | 26 | return result 27 | 28 | 29 | def append_new_extension(path): 30 | new_path = f"{path}.new" 31 | existing_files.append((path, new_path)) 32 | 33 | return new_path 34 | 35 | 36 | def replace_in_file(path, search, replace): 37 | if not os.path.exists(path): 38 | return None 39 | 40 | with open(path, "r") as file: 41 | content = file.read() 42 | 43 | content = content.replace(search, replace) 44 | 45 | with open(path, "w") as file: 46 | file.write(content) 47 | 48 | return content 49 | 50 | 51 | def file_depth_count(path): 52 | dir = os.path.dirname(path) 53 | depth_count = len(dir.split(os.path.sep)) 54 | 55 | return depth_count 56 | 57 | 58 | def alembic_ini_dst_path(path): 59 | path = os.path.join(path, "alembic.ini") 60 | 61 | return pathlib.Path(path).parents[file_depth_count(path)] 62 | 63 | 64 | def mkdir_init(path): 65 | try: 66 | pathlib.Path(path).mkdir(parents=True) 67 | 68 | return f"{path} was created successfully" 69 | except FileExistsError: 70 | return None 71 | 72 | 73 | def generate_configs(path, current_app_import_name): 74 | if mkdir_init(path) is None: 75 | print(f"Aborting! {path} already exists") 76 | return None, None 77 | 78 | cp_path("db", path) 79 | cp_path("alembic.ini", 80 | os.path.join(alembic_ini_dst_path(path), "alembic.ini")) 81 | 82 | replace_in_file("alembic.ini", "SCRIPT_LOCATION", path) 83 | replace_in_file("alembic.ini.new", "SCRIPT_LOCATION", path) 84 | replace_in_file(os.path.join(path, "env.py"), 85 | "CURRENT_APP_IMPORT_NAME", current_app_import_name) 86 | replace_in_file(os.path.join(path, "env.py"), " # noqa: E999", "") 87 | 88 | return copied_files, existing_files 89 | -------------------------------------------------------------------------------- /flask_db/templates/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = SCRIPT_LOCATION 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # timezone to use when rendering the date 11 | # within the migration file as well as the filename. 12 | # string value is passed to dateutil.tz.gettz() 13 | # leave blank for localtime 14 | # timezone = 15 | 16 | # max length of characters to apply to the 17 | # "slug" field 18 | # truncate_slug_length = 40 19 | 20 | # set to 'true' to run the environment during 21 | # the 'revision' command, regardless of autogenerate 22 | # revision_environment = false 23 | 24 | # set to 'true' to allow .pyc and .pyo files without 25 | # a source .py file to be detected as revisions in the 26 | # versions/ directory 27 | # sourceless = false 28 | 29 | # version location specification; this defaults 30 | # to foo/versions. When using multiple version 31 | # directories, initial revisions must be specified with --version-path 32 | # version_locations = %(here)s/bar %(here)s/bat foo/versions 33 | 34 | # the output encoding used when revision files 35 | # are written from script.py.mako 36 | # output_encoding = utf-8 37 | 38 | [post_write_hooks] 39 | # post_write_hooks defines scripts or Python functions that are run 40 | # on newly generated revision scripts. See the documentation for further 41 | # detail and examples 42 | 43 | # format using "black" - use the console_scripts runner, against the entrypoint 44 | # hooks=black 45 | # black.type=console_scripts 46 | # black.entrypoint=black 47 | # black.options=-l 79 48 | 49 | # Logging configuration 50 | [loggers] 51 | keys = root,sqlalchemy,alembic 52 | 53 | [handlers] 54 | keys = console 55 | 56 | [formatters] 57 | keys = generic 58 | 59 | [logger_root] 60 | level = WARN 61 | handlers = console 62 | qualname = 63 | 64 | [logger_sqlalchemy] 65 | level = WARN 66 | handlers = 67 | qualname = sqlalchemy.engine 68 | 69 | [logger_alembic] 70 | level = INFO 71 | handlers = 72 | qualname = alembic 73 | 74 | [handler_console] 75 | class = StreamHandler 76 | args = (sys.stderr,) 77 | level = NOTSET 78 | formatter = generic 79 | 80 | [formatter_generic] 81 | format = %(levelname)-5.5s [%(name)s] %(message)s 82 | datefmt = %H:%M:%S 83 | -------------------------------------------------------------------------------- /flask_db/templates/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickjj/flask-db/f964ca1a45b4db0a13f3e1ba678048b91544d57f/flask_db/templates/db/__init__.py -------------------------------------------------------------------------------- /flask_db/templates/db/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from alembic import context 4 | from sqlalchemy import engine_from_config 5 | from sqlalchemy import pool 6 | 7 | from CURRENT_APP_IMPORT_NAME import create_app 8 | 9 | 10 | # There's no access to current_app here so we must create our own app. 11 | app = create_app() 12 | db_uri = app.config["SQLALCHEMY_DATABASE_URI"] 13 | db = app.extensions["sqlalchemy"] 14 | 15 | # Provide access to the values within alembic.ini. 16 | config = context.config 17 | 18 | # Sets up Python logging. 19 | fileConfig(config.config_file_name) 20 | 21 | # Sets up metadata for autogenerate support, 22 | config.set_main_option("sqlalchemy.url", db_uri) 23 | target_metadata = db.metadata 24 | 25 | # Configure anything else you deem important, example: 26 | # my_important_option = config.get_main_option("my_important_option") 27 | 28 | 29 | def run_migrations_offline(): 30 | """ 31 | Run migrations in 'offline' mode. 32 | 33 | This configures the context with just a URL and not an Engine, though an 34 | Engine is acceptable here as well. By skipping the Engine creation we 35 | don't even need a DBAPI to be available. 36 | 37 | Calls to context.execute() here emit the given string to the script output. 38 | """ 39 | url = config.get_main_option("sqlalchemy.url") 40 | context.configure( 41 | url=url, 42 | target_metadata=target_metadata, 43 | literal_binds=True, 44 | dialect_opts={"paramstyle": "named"}, 45 | ) 46 | 47 | with context.begin_transaction(): 48 | context.run_migrations() 49 | 50 | 51 | def run_migrations_online(): 52 | """ 53 | Run migrations in 'online' mode. 54 | 55 | In this scenario we need to create an Engine and associate a connection 56 | with the context. 57 | """ 58 | # If you use Alembic revision's --autogenerate flag this function will 59 | # prevent Alembic from creating an empty migration file if nothing changed. 60 | # Source: https://alembic.sqlalchemy.org/en/latest/cookbook.html 61 | def process_revision_directives(context, revision, directives): 62 | if config.cmd_opts.autogenerate: 63 | script = directives[0] 64 | if script.upgrade_ops.is_empty(): 65 | directives[:] = [] 66 | 67 | connectable = engine_from_config( 68 | config.get_section(config.config_ini_section), 69 | prefix="sqlalchemy.", 70 | poolclass=pool.NullPool, 71 | ) 72 | 73 | with connectable.connect() as connection: 74 | context.configure( 75 | connection=connection, 76 | target_metadata=target_metadata, 77 | process_revision_directives=process_revision_directives 78 | ) 79 | 80 | with context.begin_transaction(): 81 | context.run_migrations() 82 | 83 | 84 | if context.is_offline_mode(): 85 | run_migrations_offline() 86 | else: 87 | run_migrations_online() 88 | -------------------------------------------------------------------------------- /flask_db/templates/db/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /flask_db/templates/db/seeds.py: -------------------------------------------------------------------------------- 1 | # This file should contain records you want created when you run flask db seed. 2 | # 3 | # Example: 4 | # from yourapp.models import User 5 | 6 | 7 | # initial_user = { 8 | # 'username': 'superadmin' 9 | # } 10 | # if User.find_by_username(initial_user['username']) is None: 11 | # User(**initial_user).save() 12 | -------------------------------------------------------------------------------- /flask_db/templates/db/versions/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickjj/flask-db/f964ca1a45b4db0a13f3e1ba678048b91544d57f/flask_db/templates/db/versions/.keep -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | desc = 'A Flask CLI extension to help migrate and manage your SQL database.' 4 | 5 | setup( 6 | name='Flask-DB', 7 | version='0.4.1', 8 | author='Nick Janetakis', 9 | author_email='nick.janetakis@gmail.com', 10 | url='https://github.com/nickjj/flask-db', 11 | description=desc, 12 | license='MIT', 13 | packages=['flask_db'], 14 | setup_requires=['setuptools_scm'], 15 | include_package_data=True, 16 | platforms='any', 17 | python_requires='>=3.6', 18 | zip_safe=False, 19 | install_requires=[ 20 | 'Flask>=1.0', 21 | 'SQLAlchemy>=1.2', 22 | 'SQLAlchemy-Utils', 23 | 'Flask-SQLAlchemy>=2.4', 24 | 'alembic>=1.3', 25 | 'setuptools' 26 | ], 27 | entry_points={ 28 | 'flask.commands': [ 29 | 'db=flask_db.cli:db' 30 | ], 31 | }, 32 | classifiers=[ 33 | 'Environment :: Web Environment', 34 | 'Framework :: Flask', 35 | 'Intended Audience :: Developers', 36 | 'License :: OSI Approved :: MIT License', 37 | 'Operating System :: OS Independent', 38 | 'Programming Language :: Python', 39 | 'Programming Language :: Python :: 3', 40 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 41 | 'Topic :: Software Development :: Libraries :: Python Modules', 42 | 'Topic :: Database' 43 | ] 44 | ) 45 | -------------------------------------------------------------------------------- /tests/example_app/README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | ```sh 4 | # Ensure the FLASK_APP is set. 5 | export FLASK_APP=example.app 6 | 7 | # Install the dependencies. 8 | pip install -r requirements.txt 9 | 10 | # Check out the DB commands. 11 | flask db 12 | ``` 13 | -------------------------------------------------------------------------------- /tests/example_app/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = db/ 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # timezone to use when rendering the date 11 | # within the migration file as well as the filename. 12 | # string value is passed to dateutil.tz.gettz() 13 | # leave blank for localtime 14 | # timezone = 15 | 16 | # max length of characters to apply to the 17 | # "slug" field 18 | # truncate_slug_length = 40 19 | 20 | # set to 'true' to run the environment during 21 | # the 'revision' command, regardless of autogenerate 22 | # revision_environment = false 23 | 24 | # set to 'true' to allow .pyc and .pyo files without 25 | # a source .py file to be detected as revisions in the 26 | # versions/ directory 27 | # sourceless = false 28 | 29 | # version location specification; this defaults 30 | # to foo/versions. When using multiple version 31 | # directories, initial revisions must be specified with --version-path 32 | # version_locations = %(here)s/bar %(here)s/bat foo/versions 33 | 34 | # the output encoding used when revision files 35 | # are written from script.py.mako 36 | # output_encoding = utf-8 37 | 38 | [post_write_hooks] 39 | # post_write_hooks defines scripts or Python functions that are run 40 | # on newly generated revision scripts. See the documentation for further 41 | # detail and examples 42 | 43 | # format using "black" - use the console_scripts runner, against the entrypoint 44 | # hooks=black 45 | # black.type=console_scripts 46 | # black.entrypoint=black 47 | # black.options=-l 79 48 | 49 | # Logging configuration 50 | [loggers] 51 | keys = root,sqlalchemy,alembic 52 | 53 | [handlers] 54 | keys = console 55 | 56 | [formatters] 57 | keys = generic 58 | 59 | [logger_root] 60 | level = WARN 61 | handlers = console 62 | qualname = 63 | 64 | [logger_sqlalchemy] 65 | level = WARN 66 | handlers = 67 | qualname = sqlalchemy.engine 68 | 69 | [logger_alembic] 70 | level = INFO 71 | handlers = 72 | qualname = alembic 73 | 74 | [handler_console] 75 | class = StreamHandler 76 | args = (sys.stderr,) 77 | level = NOTSET 78 | formatter = generic 79 | 80 | [formatter_generic] 81 | format = %(levelname)-5.5s [%(name)s] %(message)s 82 | datefmt = %H:%M:%S 83 | -------------------------------------------------------------------------------- /tests/example_app/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickjj/flask-db/f964ca1a45b4db0a13f3e1ba678048b91544d57f/tests/example_app/db/__init__.py -------------------------------------------------------------------------------- /tests/example_app/db/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from alembic import context 4 | from sqlalchemy import engine_from_config 5 | from sqlalchemy import pool 6 | 7 | from tests.example_app.example.app import create_app 8 | 9 | 10 | # There's no access to current_app here so we must create our own app. 11 | app = create_app() 12 | db_uri = app.config["SQLALCHEMY_DATABASE_URI"] 13 | db = app.extensions["sqlalchemy"] 14 | 15 | # Provide access to the values within alembic.ini. 16 | config = context.config 17 | 18 | # Sets up Python logging. 19 | fileConfig(config.config_file_name) 20 | 21 | # Sets up metadata for autogenerate support, 22 | config.set_main_option("sqlalchemy.url", db_uri) 23 | target_metadata = db.metadata 24 | 25 | # Configure anything else you deem important, example: 26 | # my_important_option = config.get_main_option("my_important_option") 27 | 28 | 29 | def run_migrations_offline(): 30 | """ 31 | Run migrations in 'offline' mode. 32 | 33 | This configures the context with just a URL and not an Engine, though an 34 | Engine is acceptable here as well. By skipping the Engine creation we 35 | don't even need a DBAPI to be available. 36 | 37 | Calls to context.execute() here emit the given string to the script output. 38 | """ 39 | url = config.get_main_option("sqlalchemy.url") 40 | context.configure( 41 | url=url, 42 | target_metadata=target_metadata, 43 | literal_binds=True, 44 | dialect_opts={"paramstyle": "named"}, 45 | ) 46 | 47 | with context.begin_transaction(): 48 | context.run_migrations() 49 | 50 | 51 | def run_migrations_online(): 52 | """ 53 | Run migrations in 'online' mode. 54 | 55 | In this scenario we need to create an Engine and associate a connection 56 | with the context. 57 | """ 58 | # If you use Alembic revision's --autogenerate flag this function will 59 | # prevent Alembic from creating an empty migration file if nothing changed. 60 | # Source: https://alembic.sqlalchemy.org/en/latest/cookbook.html 61 | def process_revision_directives(context, revision, directives): 62 | if config.cmd_opts.autogenerate: 63 | script = directives[0] 64 | if script.upgrade_ops.is_empty(): 65 | directives[:] = [] 66 | 67 | connectable = engine_from_config( 68 | config.get_section(config.config_ini_section), 69 | prefix="sqlalchemy.", 70 | poolclass=pool.NullPool, 71 | ) 72 | 73 | with connectable.connect() as connection: 74 | context.configure( 75 | connection=connection, 76 | target_metadata=target_metadata, 77 | process_revision_directives=process_revision_directives 78 | ) 79 | 80 | with context.begin_transaction(): 81 | context.run_migrations() 82 | 83 | 84 | if context.is_offline_mode(): 85 | run_migrations_offline() 86 | else: 87 | run_migrations_online() 88 | -------------------------------------------------------------------------------- /tests/example_app/db/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /tests/example_app/db/seeds.py: -------------------------------------------------------------------------------- 1 | # This file should contain records you want created when you run flask db seed. 2 | # 3 | # Example: 4 | # from yourapp.models import User 5 | 6 | 7 | # initial_user = { 8 | # 'username': 'superadmin' 9 | # } 10 | # if User.find_by_username(initial_user['username']) is None: 11 | # User(**initial_user).save() 12 | -------------------------------------------------------------------------------- /tests/example_app/db/versions/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickjj/flask-db/f964ca1a45b4db0a13f3e1ba678048b91544d57f/tests/example_app/db/versions/.keep -------------------------------------------------------------------------------- /tests/example_app/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nickjj/flask-db/f964ca1a45b4db0a13f3e1ba678048b91544d57f/tests/example_app/example/__init__.py -------------------------------------------------------------------------------- /tests/example_app/example/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from flask_sqlalchemy import SQLAlchemy 4 | 5 | 6 | # Inlining a bunch of things here for the sake of simplicity. Normally you'd 7 | # break these out into separate files. 8 | db = SQLAlchemy() 9 | 10 | settings = { 11 | "SQLALCHEMY_DATABASE_URI": "sqlite:///exampledb", 12 | "SQLALCHEMY_TRACK_MODIFICATIONS": False, 13 | "FLASK_DB_SEEDS_PATH": "db/seeds.py" 14 | } 15 | 16 | 17 | class User(db.Model): 18 | __tablename__ = "users" 19 | id = db.Column(db.Integer, primary_key=True) 20 | username = db.Column(db.String(24), unique=True, index=True) 21 | 22 | @classmethod 23 | def find_by_username(cls, username): 24 | return User.query.filter(User.username == username).first() 25 | 26 | def save(self): 27 | db.session.add(self) 28 | db.session.commit() 29 | 30 | return self 31 | 32 | 33 | def create_app(): 34 | app = Flask(__name__) 35 | 36 | app.config.update(settings) 37 | 38 | db.init_app(app) 39 | 40 | @app.route("/") 41 | def index(): 42 | return "Hello world" 43 | 44 | return app 45 | -------------------------------------------------------------------------------- /tests/example_app/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.3 2 | SQLAlchemy==2.0.20 3 | SQLAlchemy-Utils==0.41.0 4 | Flask-SQLAlchemy==3.1.1 5 | Flask-DB==0.4.1 6 | --------------------------------------------------------------------------------