├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ └── tests.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGES.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── pyproject.toml ├── src └── flask_migrate │ ├── __init__.py │ ├── cli.py │ └── templates │ ├── aioflask-multidb │ ├── README │ ├── alembic.ini.mako │ ├── env.py │ └── script.py.mako │ ├── aioflask │ ├── README │ ├── alembic.ini.mako │ ├── env.py │ └── script.py.mako │ ├── flask-multidb │ ├── README │ ├── alembic.ini.mako │ ├── env.py │ └── script.py.mako │ └── flask │ ├── README │ ├── alembic.ini.mako │ ├── env.py │ └── script.py.mako ├── tests ├── __init__.py ├── app.py ├── app_compare_type1.py ├── app_compare_type2.py ├── app_custom_directory.py ├── app_custom_directory_path.py ├── app_multidb.py ├── custom_template │ ├── README │ ├── alembic.ini.mako │ ├── env.py │ └── script.py.mako ├── test_custom_template.py ├── test_migrate.py └── test_multidb_migrate.py └── tox.ini /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: File a bug report 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **IMPORTANT**: If you have a question, or if you are not sure if you have found a bug in this package or in your own code, then you are in the wrong place. Hit back in your web browser, and then open a GitHub Discussion instead. Likewise, if you are unable to provide the information requested below, open a Discussion to troubleshoot your issue. 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. If you are getting errors, please include the complete error message, including the stack trace. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Logs** 26 | Please provide relevant logs or script output. 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: GitHub Discussions 4 | url: https://github.com/miguelgrinberg/Flask-Migrate/discussions 5 | about: Ask questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Logs** 20 | Please provide relevant logs or script output. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | lint: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-python@v3 16 | - run: python -m pip install --upgrade pip wheel 17 | - run: pip install tox tox-gh-actions 18 | - run: tox -eflake8 19 | - run: tox -edocs 20 | tests: 21 | name: tests 22 | strategy: 23 | matrix: 24 | # TODO: add windows-latest 25 | os: [ubuntu-latest, macos-latest] 26 | python: ['3.8', '3.9', '3.10', '3.11', 'pypy-3.9'] 27 | sqla: ['sqlalchemy<2 flask-sqlalchemy<3.1', 'sqlalchemy>=2 flask-sqlalchemy>=3.1'] 28 | fail-fast: false 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - uses: actions/checkout@v3 32 | - uses: actions/setup-python@v3 33 | with: 34 | python-version: ${{ matrix.python }} 35 | - run: python -m pip install --upgrade pip wheel 36 | - run: pip install tox tox-gh-actions 37 | - run: SQLALCHEMY_VERSION="${{ matrix.sqla }}" tox 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | var 14 | sdist 15 | develop-eggs 16 | .installed.cfg 17 | lib 18 | lib64 19 | 20 | # Installer logs 21 | pip-log.txt 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | .tox 26 | nosetests.xml 27 | 28 | # Unit test temporary files 29 | tests/temp_folder 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Sphinx build files 40 | docs/_build/ -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - method: pip 14 | path: . 15 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Flask-Migrate Change Log 2 | 3 | **Release 4.1.0** - 2025-01-10 4 | 5 | - Accept arguments such as `--directory` in environment variables [#553](https://github.com/miguelgrinberg/flask-migrate/issues/553) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/12a181ce0542659f10b4ce154a10cfd41fa1f501)) 6 | - Fix minor typos in documentation [#552](https://github.com/miguelgrinberg/flask-migrate/issues/552) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/3b073a2c500303894c495d2e4fa0ef449b6a2833)) (thanks **Kevin Kirsche**!) 7 | 8 | **Release 4.0.7** - 2024-03-11 9 | 10 | - Regression from #438: check `g.x_arg` exists before accessing it [#541](https://github.com/miguelgrinberg/flask-migrate/issues/541) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/7e8032c61e5c47f4e50b03dcf98c3b20dd5a8e55)) (thanks **Skye Im**!) 11 | 12 | **Release 4.0.6** - 2024-03-09 13 | 14 | - Accept `-x` options for all db commands [#438](https://github.com/miguelgrinberg/flask-migrate/issues/438) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/6f3f889c36030134f87dc1db327c2385d873a4d6)) 15 | - Add `--purge` flag to the `stamp` command [#540](https://github.com/miguelgrinberg/flask-migrate/issues/540) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/af67bb093df5652c867b88880c5934bfc68313cc)) (thanks **Jono N**!) 16 | 17 | **Release 4.0.5** - 2023-09-12 18 | 19 | - Compatibility fixes for Flask-SQLAlchemy >= 3.1 [#526](https://github.com/miguelgrinberg/flask-migrate/issues/526) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/f562178bbe1912912f3cb6877cbae8b0899c74da)) (thanks **David Lord**!) 20 | - Allow `process_revision_directives` option to be configurable [#523](https://github.com/miguelgrinberg/flask-migrate/issues/523) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/821e37fcc4a5e339f197153cdbb4dd2316cbd44b)) (thanks **llc**!) 21 | - Stop testing Python 3.7, as Flask-SQLAlchemy 3.1 stopped supporting it ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/8d175193f00bf4e5578f0142d011093d8cd53d57)) 22 | 23 | **Release 4.0.4** - 2023-02-02 24 | 25 | - Correctly obtain database URL with SQLAlchemy 2.0 [#505](https://github.com/miguelgrinberg/flask-migrate/issues/505) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/c8cd02c5d3d43bbab462b863db5417b5d69228bb)) 26 | 27 | **Release 4.0.3** - 2023-01-29 28 | 29 | - Remove legacy __future__ import in Alembic templates [#504](https://github.com/miguelgrinberg/flask-migrate/issues/504) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/7a388cfe320254735f4ed65ac655caaf0cae8b28)) (thanks **Pamela Fox**!) 30 | - Add SQLAlchemy 1.4 and 2.0 to the test matrix ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/7a725f2e3267f1c3bb4920cd3bff3a9ff1d7eb6e)) 31 | - Switch to pytest as test runner ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/5acd794048d050f85b5dea93052f96abd8a583f2)) 32 | 33 | **Release 4.0.2** - 2023-01-18 34 | 35 | - Support "check" command [#502](https://github.com/miguelgrinberg/flask-migrate/issues/502) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/1a893b4fca280f82b1aada6458b7c866c6d3c953)) (thanks **Masamitsu MURASE**!) 36 | 37 | **Release 4.0.1** - 2023-01-05 38 | 39 | - Do not use deprecated functions in Flask-SQLAlchemy 3.0 ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/7cb4236327ea04fc6be8a17bbfadae6de7bfbc8b)) 40 | - Stop building Python 3.6 ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/c9534b39df49884e1b62592c486ed0d5565b3321)) 41 | - Remove tests from pypi package ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/2dd0c25caa5c43b452109f64c8e33ccc048ca210)) 42 | 43 | **Release 4.0.0** - 2022-11-13 44 | 45 | - Updates for Flask-SQLAlchemy 3.x compatibility ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/51752948aabdb68f7c032e1c1fc8317f895e10a6)) 46 | - Enable type comparison and batch mode by default ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/a3085b34e5b1865d2b773248b37468764df7c312)) 47 | - Option to rename "db" command group to a custom name ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/b9c9d35744a08f4f62084ce6e3ddf30d21431dc7)) 48 | - Better handling of MetaData instances in templates ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/c051a000c1518a71e0a5d045c1f8065b9add5122)) 49 | - Set options correctly when `revision --autogenerate` is used [#463](https://github.com/miguelgrinberg/flask-migrate/issues/463) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/f7f15e2623866110974fddcdbea87ccbf1d74a40)) (thanks **Frazer McLean**!) 50 | - Documentation section on configuring Alembic ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/28522143f4e1371f08fa8bac8d3ba1f6b04e0f72)) 51 | - Upgrade build to pypy-3.9 ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/dfaeeff71739f75655f9d1e7f88bc70cb87a1f2b)) 52 | - Add Python 3.10 to build ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/a41df8748e8d3b1a6d0909d5d7fe46a55c7f1c9b)) 53 | - Add Python 3.11 to build ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/370b9151b6ae3e23675c1a7566d8f09402beb3d6)) 54 | - Specify license in project metadata [#489](https://github.com/miguelgrinberg/flask-migrate/issues/489) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/095b0ecbdfd496326978708ad2e7fc0025832964)) (thanks **Frazer McLean**!) 55 | - Remove tests from pypi package ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/2dd0c25caa5c43b452109f64c8e33ccc048ca210)) 56 | 57 | **Release 3.1.0** - 2021-08-01 58 | 59 | - Added `list-templates` command and support for custom templates ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/0f9094a750205c1db1fe178d0d037e529de403ae)) 60 | - Alembic templates for [aioflask](https://github.com/miguelgrinberg/aioflask) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/fbaaa3787d0e03f5aafaea6fd7c2956362a57c52)) 61 | - Improved project structure ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/074cbc9cae4b6ebb7d013adcec42e070be1ae6b3)) 62 | 63 | **Release 3.0.1** - 2021-05-31 64 | 65 | - Add support for Alchemical in addition to Flask-SQLAlchemy ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/113115d7f37a713d1f32be53a1e43564b9bb3dea)) 66 | - Remove Flask-Script references from the documentation ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/699e136e1ff8e2e75e6fcd957c4ebf332a4969a9)) 67 | 68 | **Release 3.0.0** - 2021-05-15 69 | 70 | - Remove support for Flask-Script [#403](https://github.com/miguelgrinberg/flask-migrate/issues/403) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/a1787cf18fb4d5ec7369280afe1a59349f7544b8)) 71 | - Use unittest testrunner [#397](https://github.com/miguelgrinberg/flask-migrate/issues/397) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/5e75b1b574dd7ee991ca2fae0b2ccd63a0f98d81)) (thanks **Jürgen Gmach**!) 72 | - Remove dependency on six package [#395](https://github.com/miguelgrinberg/flask-migrate/issues/395) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/4ad897f1c3522ecf529cb83f70ef72bc3c32ba6f)) (thanks **Jürgen Gmach**!) 73 | - Added sphinx build files to .gitignore file [#394](https://github.com/miguelgrinberg/flask-migrate/issues/394) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/6566e3dc5d5aa6dc7ca7a6228655f0a9d78d42e6)) (thanks **Jürgen Gmach**!) 74 | - Fix Sphinx warning [#393](https://github.com/miguelgrinberg/flask-migrate/issues/393) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/a2d31b723517a9f72a9735ff70d858f7158dd4b3)) (thanks **Jürgen Gmach**!) 75 | 76 | **Release 2.7.0** - 2021-02-21 77 | 78 | - Reuse engine from Flask-SQLAlchemy [#343](https://github.com/miguelgrinberg/flask-migrate/issues/343) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/8f8ded8799c65e2b3490a82b5e3a3953c33c58dd)) 79 | - Update logging configuration to include Flask-Migrate's logger ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/3a11cd8392733bd8315458c47d769d952494bdd7)) 80 | 81 | **Release 2.6.0** - 2021-01-19 82 | 83 | - Removed deprecated --head-only option [#380](https://github.com/miguelgrinberg/flask-migrate/issues/380) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/ae0a5a922106d67605adcebe9e3f13b1ed5f84e8)) 84 | - Initialize logger with a name [#374](https://github.com/miguelgrinberg/flask-migrate/issues/374) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/4887bd53bc08f10087fe27a4a7d9fe853031cdcf)) (thanks **maquino1985**!) 85 | - Move import to the top in env.py file to avoid linter warnings [#349](https://github.com/miguelgrinberg/flask-migrate/issues/349) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/b34d9e3ff79ffb2f6c0204289f697a08852d0859)) (thanks **James Addison**!) 86 | - Add a note to the documentation regarding logging [#330](https://github.com/miguelgrinberg/flask-migrate/issues/330) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/f969b5ea087f2d9bf646492e1a5ca23535dfac5f)) (thanks **Oliver Evans**!) 87 | - Move builds to GitHub actions ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/c4a515105e84ae201208cd02159116653dc5e821)) 88 | 89 | **Release 2.5.3** - 2020-03-14 90 | 91 | - Allow `Path` objects to be used as `directory` parameter [#319](https://github.com/miguelgrinberg/flask-migrate/issues/319) Closes [#318](https://github.com/miguelgrinberg/flask-migrate/issues/318). ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/399cb28cc128539111234f7ecea0b3187325af82)) (thanks **Nicolas Schneider**!) 92 | - Use same database URLs as Flask-SQLAlchemy [#276](https://github.com/miguelgrinberg/flask-migrate/issues/276) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/4a180b711bf87572617a0b7caad0a7151f53fde7)) 93 | - Document how to set up with init_app method [#302](https://github.com/miguelgrinberg/flask-migrate/issues/302) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/6a76c245740d9af3ad5eef56ee9ff15f8205a0ca)) (thanks **Kyle Lawlor**!) 94 | - Document how to include a message in initial migrate. [#313](https://github.com/miguelgrinberg/flask-migrate/issues/313) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/aa05b836a2fe89363bc2d61a699acd54aca52bd5)) (thanks **Bernardo Gomes**!) 95 | - Remove checks for alembic 0.7.0 [#278](https://github.com/miguelgrinberg/flask-migrate/issues/278) Flask-Migrate requires alembic >= 0.7 in its setup.py file, which makes all the checks for this version obsolete. ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/97b8d334324ecb043fb0ddaef1660f36832af02c)) (thanks **Tadej Borovšak**!) 96 | - Use sys.executable in tests [#290](https://github.com/miguelgrinberg/flask-migrate/issues/290) Also re-order imports. Closes https://github.com/miguelgrinberg/Flask-Migrate/issues/289 ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/e5135e5a6a31675d5fb10febe815b257d82632a2)) (thanks **John Vandenberg**!) 97 | - Cosmetic improvements to help messages [#284](https://github.com/miguelgrinberg/flask-migrate/issues/284) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/d501d8b2923187df00d6bb1ec1f04694ab3f9667)) (thanks **Marat Sharafutdinov**!) 98 | 99 | **Release 2.5.2** - 2019-05-25 100 | 101 | - add python 3.7 builds, remove 3.4 ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/1af28ba273de6c88544623b8dc02dd539340294b)) 102 | - auto-generate change log during release ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/250a3110ad84ba331ffc7cb871e5a12fddc55f2d)) 103 | - Nicer change log ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/829edefe09dff82b6c91203dd0f5b5795bf9c8d1)) 104 | 105 | **Release 2.5.1** - 2019-05-20 106 | 107 | - Fix `__version__` file not found in sdist [#267](https://github.com/miguelgrinberg/flask-migrate/issues/267) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/8ba8d7dca6cb233280d4644fc8d81cbba123a5ad)) (thanks **Christoph Gohlke**!) 108 | 109 | **Release 2.5.0** - 2019-05-19 110 | 111 | - helper release script ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/fb041b9b9221e7125e5ee27dd9eb7514cf143181)) 112 | - support % character in database URLs [#59](https://github.com/miguelgrinberg/flask-migrate/issues/59) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/4663819b416dfb5a450fe948e84111af3712078d)) 113 | - log command output in unit tests ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/506f3541c04dd0b2c020dbafc60a93fb541ca324)) 114 | - add a section on why use this extension to the docs [#101](https://github.com/miguelgrinberg/flask-migrate/issues/101) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/5467b295f7912d195333482325052eae81d30f7a)) 115 | 116 | **Release 2.4.0** - 2019-02-16 117 | 118 | - updates to env.py ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/2a1ae1b9aa62c246fca2b2f1566284de8d4b940b)) 119 | - Link to binds is unreachable [#244](https://github.com/miguelgrinberg/flask-migrate/issues/244) The original link redirects to flask-sqlalchemy.pocoo.org Addressing it directly works and redirects to the latest version of flask-sqlalchemy ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/eb2ab8f797a179d807b6560947f4498bb543def0)) (thanks **FaBrand**!) 120 | 121 | **Release 2.3.1** - 2018-11-28 122 | 123 | - Don't swallow transaction errors [#236](https://github.com/miguelgrinberg/flask-migrate/issues/236) A change proposed by @kjohnsen from [#216](https://github.com/miguelgrinberg/flask-migrate/issues/216). You can read more starting with [this comment](https://github.com/miguelgrinberg/Flask-Migrate/issues/216#issuecomment-408159125). ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/87b40ec9b2113ab87f1dc41c4eb517e8ff0f5dd8)) (thanks **Nikolay Shebanov**!) 124 | 125 | **Release 2.3.0** - 2018-10-05 126 | 127 | - use the root logger for alembic error messages ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/fa3b45ea6ecd16f436bbdee210745335128bf514)) 128 | - Add indicate-current option into history command [#192](https://github.com/miguelgrinberg/flask-migrate/issues/192) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/1fa1936375bf609e9dab401e26f46f715f5b272b)) (thanks **misebox**!) 129 | 130 | **Release 2.2.1** - 2018-06-18 131 | 132 | - dependency updates ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/5d61c191d7d8853f81e4fe71609b5ae315604c52)) 133 | - add pypy3 to test matrix ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/1cc819e9f2a17435429c16531043d0a874f76288)) 134 | 135 | **Release 2.2.0** - 2018-06-13 136 | 137 | - suppress stack traces on command errors [#204](https://github.com/miguelgrinberg/flask-migrate/issues/204) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/2dd45d0037cb62bc7a8a981f0de70071b891a2ff)) 138 | - Fix typo in docs [#207](https://github.com/miguelgrinberg/flask-migrate/issues/207) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/817893fe16eb5361469af671c52d8276b69efeab)) (thanks **Grey Li**!) 139 | - Update documentation link [#206](https://github.com/miguelgrinberg/flask-migrate/issues/206) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/98374a6ac1006fe37ef6f09674ebaa094214ff3e)) (thanks **Grey Li**!) 140 | - [#182](https://github.com/miguelgrinberg/flask-migrate/issues/182): clear executable permission [#183](https://github.com/miguelgrinberg/flask-migrate/issues/183) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/ee0b47ede1e88340c390fca9d0c27a9c8a7e3a9c)) (thanks **Christopher Yeleighton**!) 141 | 142 | **Release 2.1.1** - 2017-08-28 143 | 144 | - Make directory as optional parameter ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/7e6f511b050a023c9388868b97d7905b7571c16f)) (thanks **Diego Oliveira**!) 145 | - Update CHANGELOG.md ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/dcffe1e91f9f5cfc29c5cf5f285ef9a4169113bb)) 146 | - updated README.md to use flask cli in examples ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/349826ef53f607fd5e866919f40b16315a2127cc)) 147 | 148 | **Release 2.1.0** - 2017-07-28 149 | 150 | - Remove the Flask-Script dependency in setup.py ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/7bd5ef80016257ea56a1d10f917a361a13b6aad9)) 151 | 152 | **Release 2.0.4** - 2017-05-31 153 | 154 | - Change an Alembic doc link to http from https The SSL certificate is not valid for the alembic subdomain. ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/00e8192f56f778939fd819a799a9b26010b46e2e)) (thanks **bovarysme**!) 155 | - Update links pointing to the old Alembic doc [#152](https://github.com/miguelgrinberg/flask-migrate/issues/152) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/81367408c46ecd96b2352f6e309dcb02dfc359e9)) (thanks **bovarysme**!) 156 | - allow -x argument for migrate command [#148](https://github.com/miguelgrinberg/flask-migrate/issues/148) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/3293d835811924822cf0e8d321760ddc482ea5f2)) (thanks **Tim Mundt**!) 157 | - stop building python 2.6 ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/5f34c5cd9610f2e3194672a350f57c55cb7edd25)) 158 | 159 | **Release 2.0.3** - 2017-01-30 160 | 161 | - remove py33 builds, add py36 ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/c25ef622efc4dc28452124f56fc9bfa550ce858e)) 162 | - Support multiple -x arguments in Flask-Script interface [#103](https://github.com/miguelgrinberg/flask-migrate/issues/103) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/d2947338d673041b6198b909a9f9381b73f6bf99)) 163 | - Fix autogenerate typos. ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/19af3191d8245c110860f01bb48b02b0e2d9351a)) (thanks **Sorin Muntean**!) 164 | 165 | **Release 2.0.2** - 2016-12-09 166 | 167 | - Merge branch 'briancappello-application_factory_kwargs' ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/4ab45f60bea8227f94adbf5de1e8509ef79afa66)) 168 | - support passing alembic context kwargs from constructor into init_app ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/57c82f35366ea9122908e3b0cbdcf536d430c2de)) (thanks **Brian Cappello**!) 169 | - fix link for alembics command reference page ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/bfbfcb24abf3ede22104211157f05d3f370ed467)) (thanks **Brian Cappello**!) 170 | - update autogenerate capabilities ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/118b84d384106ebfde767931119204b80fcbbac5)) (thanks **Brian Cappello**!) 171 | 172 | **Release 2.0.1** - 2016-11-13 173 | 174 | - Imports at top of file as specified by Flake8 and added branch label. Moving the imports to the top of the file will prevent an error: 'E402 module level import not at top of file'. ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/c95c20134655cbe8f771a7e47f6d3fd4ed3e8cb6)) (thanks **Maico Timmerman**!) 175 | - Fix typo [#126](https://github.com/miguelgrinberg/flask-migrate/issues/126) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/109f42fc1752422c0e3899942abe10020641edf6)) (thanks **Wells Lucas Santo**!) 176 | - Fix typo [#123](https://github.com/miguelgrinberg/flask-migrate/issues/123) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/f37bdb70529e5f984b9008eace59244a293ea3db)) (thanks **Jeff Widman**!) 177 | - flask cli documentation updates ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/254b88bc4bdafd8fb9210b37f772a72035a12cff)) 178 | 179 | **Release 2.0.0** - 2016-07-31 180 | 181 | - add travis builds for python 3.5 ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/080b7100e2f8077d95deb7c5dcdd5adb792f528c)) 182 | - Support the new flask cli based on click [#114](https://github.com/miguelgrinberg/flask-migrate/issues/114) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/8c208b54837ccbf65eee8d95fc623057e2753b4e)) 183 | 184 | **Release 1.8.1** - 2016-07-10 185 | 186 | - Allow to init_app correctly without specifying db. [#118](https://github.com/miguelgrinberg/flask-migrate/issues/118) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/e339f27b8ee3e932d74fff91344b615fe6170333)) (thanks **Victor Akimov**!) 187 | 188 | **Release 1.8.0** - 2016-02-24 189 | 190 | - Allow db to be provided separately from the app ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/d612c10c05ceab7e1131839ab8a7eadcc4a71beb)) 191 | - Added missing Python 2 version classifier to package ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/6f321543fec6c04b445c34c312092c45deaf2d29)) 192 | - Fix the link for flask-sqlalchemy binds [#99](https://github.com/miguelgrinberg/flask-migrate/issues/99) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/6bf0452b084f14d06e56784465240e0258b710cb)) (thanks **lord63**!) 193 | - Update the way to import the flask extension [#98](https://github.com/miguelgrinberg/flask-migrate/issues/98) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/c27cfea43d923b991b5f609a94a58efcdca4fe3b)) (thanks **lord63**!) 194 | 195 | **Release 1.7.0** - 2015-12-28 196 | 197 | - configuration callbacks ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/835d3e4b33bfbd7dae2166b88c49aee8f5fbeac0)) 198 | - Update __init__.py Update description of edit command. ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/3a657dcb683ff3b91cb9e2393fbdaaa11ea33b8a)) (thanks **Kostyantyn Leschenko**!) 199 | - fixed link in documentation ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/738d33cd76185455063d9499f7752b846193889a)) 200 | 201 | **Release 1.6.0** - 2015-09-17 202 | 203 | - Add edit command [#76](https://github.com/miguelgrinberg/flask-migrate/issues/76) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/e9c855ca8a935bc55e830481073e377d09cdf66d)) (thanks **Jelte Fennema**!) 204 | - Updates so that directory doesn't get overridden [#78](https://github.com/miguelgrinberg/flask-migrate/issues/78) where directory gets overridden if it is not explicitly specified in init_app. This fix allows it to be set when the `Migrate` object is initialized and not automatically get overridden when the `init_app` is called. This is a non-breaking change. ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/0c9a2cbe35953cdabca571f402883c202ddfa09f)) (thanks **Tim Martin**!) 205 | 206 | **Release 1.5.1** - 2015-08-24 207 | 208 | - Do not generate a migration if no schema changes are found ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/626c83feb49f571c2a4468a8fc9f81a44eecc451)) 209 | - Fix the merge command by allowing more than one argument. [#74](https://github.com/miguelgrinberg/flask-migrate/issues/74) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/9c62c963c0e142a40233dd6dde17925cdc34ea9d)) (thanks **Kevin Dwyer**!) 210 | 211 | **Release 1.5.0** - 2015-08-01 212 | 213 | - Make the multi-database configuration fully automatic ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/ed1606c02dd3e57580d853db6ebe66804a5f0ceb)) 214 | - Add sane default for `db downgrade --sql` [#71](https://github.com/miguelgrinberg/flask-migrate/issues/71) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/01a2e701259e7792c82b2173587750efe5523877)) (thanks **Anthony Miyaguchi**!) 215 | - Add support for multiple databases migration [#70](https://github.com/miguelgrinberg/flask-migrate/issues/70) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/07737da36faf103987a9aa1df1e39ab8259b5181)) (thanks **Nan Jiang**!) 216 | - Added support for -x option ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/a2ff9d668393e4d2da555dc145ee80c065ac932a)) (thanks **jesse**!) 217 | 218 | **release 1.4.0** - 2015-04-27 219 | 220 | - Pass custom Alembic configuration options, such as compare_type. Also the tests have been restructured for clarity. ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/ee32ae4db5f406508dac0b93f1dacb2b8d05a5f9)) 221 | 222 | **release 1.3.1** - 2015-03-20 223 | 224 | - Update __init__.py alembic version now includes parts that are not integers (e.g., 0.7.5.post1). This change will prevent the code from trying to convert non-integer values to integers. ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/49d4d99897a8756c0fa732d879b29900e6adcef4)) (thanks **pjryan126**!) 225 | - Added syntax highlighting to README [#44](https://github.com/miguelgrinberg/flask-migrate/issues/44) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/8cef3eb124c4df443b32cc33ddfeb97a3a810c05)) (thanks **Bulat Bochkariov**!) 226 | 227 | **version 1.3.0** - 2014-12-01 228 | 229 | - new commands and options introduced with Alembic 0.7 ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/d848cb43da247259948270cb0745a99c2e118866)) 230 | - pep8 changes ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/71ff1d717be124d5a1c6c930dc911a5635e323e0)) 231 | - Updated docs: add installation [#37](https://github.com/miguelgrinberg/flask-migrate/issues/37) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/aca0ce8381fbb083a3eae9b0e1d60a3de43a0be2)) (thanks **Mozillazg**!) 232 | - travis CI builds ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/8e8a4d018f9a0a148adef712b9227dab6c990d1b)) 233 | - travis configuration ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/5241108fdb1797a883bd1844f525e35081ceb199)) 234 | - Added Python 3 programming language support to setup.py ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/d13b42bdce60db64ad56f43350f3cb15f5f7bbe7)) (thanks **Fotis Gimian**!) 235 | - Added travis configuration ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/917b2577e58c35b03acca0fcf8f2f09e7686975f)) (thanks **Fotis Gimian**!) 236 | - Added test-related temp_folder to gitignore list ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/ccc87167c68759b4eded529ca2335f2743f7bb4f)) (thanks **Fotis Gimian**!) 237 | - Repaired tests to correctly run under Python 3.3 ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/d4451fddddd951cdb144320e98162ba8616f1109)) (thanks **Fotis Gimian**!) 238 | 239 | **release 1.2.0** - 2014-01-19 240 | 241 | - Added the branches command [#15](https://github.com/miguelgrinberg/flask-migrate/issues/15) ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/b9913c6458adba1e511ee86fd5a7291214f2eb9f)) (thanks **Bogdan Gaza**!) 242 | - added tests directory to tarball ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/9896e370f98bb62d9a363fea564d6758d2566263)) 243 | 244 | **release 1.1.0** - 2013-12-20 245 | 246 | - [#12](https://github.com/miguelgrinberg/flask-migrate/issues/12): pass a default migrations directory in the Migrate constructor ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/189dbb5600c164147751453e72a1c8bb8e4229c3)) 247 | - Merge branch 'master' of github.com:miguelgrinberg/Flask-Migrate ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/3d364a81edacda81ccee60a33616ba15d47b5c27)) 248 | - pass revision range to history command ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/790442da2f9fa8168c6c0aa837ed4a1505980059)) (thanks **David Lord**!) 249 | - scripting interface ([commit](https://github.com/miguelgrinberg/flask-migrate/commit/ba6a0904616b701cc48f066fe3fca18a432b7f10)) 250 | 251 | **release 1.0.0** - 2013-12-03 252 | 253 | - First official release! 254 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Miguel Grinberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE tox.ini 2 | recursive-include docs * 3 | recursive-exclude docs/_build * 4 | recursive-include tests * 5 | exclude **/*.pyc 6 | include src/flask_migrate/templates/flask/* 7 | include src/flask_migrate/templates/flask-multidb/* 8 | include src/flask_migrate/templates/aioflask/* 9 | include src/flask_migrate/templates/aioflask-multidb/* 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flask-Migrate 2 | ============= 3 | 4 | [![Build status](https://github.com/miguelgrinberg/flask-migrate/workflows/build/badge.svg)](https://github.com/miguelgrinberg/flask-migrate/actions) 5 | 6 | Flask-Migrate is an extension that handles SQLAlchemy database migrations for Flask applications using Alembic. The database operations are provided as command-line arguments under the `flask db` command. 7 | 8 | Installation 9 | ------------ 10 | 11 | Install Flask-Migrate with `pip`: 12 | 13 | pip install Flask-Migrate 14 | 15 | Example 16 | ------- 17 | 18 | This is an example application that handles database migrations through Flask-Migrate: 19 | 20 | ```python 21 | from flask import Flask 22 | from flask_sqlalchemy import SQLAlchemy 23 | from flask_migrate import Migrate 24 | 25 | app = Flask(__name__) 26 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db' 27 | 28 | db = SQLAlchemy(app) 29 | migrate = Migrate(app, db) 30 | 31 | class User(db.Model): 32 | id = db.Column(db.Integer, primary_key=True) 33 | name = db.Column(db.String(128)) 34 | ``` 35 | 36 | With the above application you can create the database or enable migrations if the database already exists with the following command: 37 | 38 | $ flask db init 39 | 40 | Note that the `FLASK_APP` environment variable must be set according to the Flask documentation for this command to work. This will add a `migrations` folder to your application. The contents of this folder need to be added to version control along with your other source files. 41 | 42 | You can then generate an initial migration: 43 | 44 | $ flask db migrate 45 | 46 | The migration script needs to be reviewed and edited, as Alembic currently does not detect every change you make to your models. In particular, Alembic is currently unable to detect indexes. Once finalized, the migration script also needs to be added to version control. 47 | 48 | Then you can apply the migration to the database: 49 | 50 | $ flask db upgrade 51 | 52 | Then each time the database models change repeat the `migrate` and `upgrade` commands. 53 | 54 | To sync the database in another system just refresh the `migrations` folder from source control and run the `upgrade` command. 55 | 56 | To see all the commands that are available run this command: 57 | 58 | $ flask db --help 59 | 60 | Resources 61 | --------- 62 | 63 | - [Documentation](http://flask-migrate.readthedocs.io/en/latest/) 64 | - [pypi](https://pypi.python.org/pypi/Flask-Migrate) 65 | - [Change Log](https://github.com/miguelgrinberg/Flask-Migrate/blob/master/CHANGES.md) 66 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'Flask-Migrate' 23 | copyright = '2019, Miguel Grinberg' 24 | author = 'Miguel Grinberg' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | ] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix(es) of source filenames. 48 | # You can specify multiple suffix as a list of string: 49 | # 50 | # source_suffix = ['.rst', '.md'] 51 | source_suffix = '.rst' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | # 59 | # This is also used if you do content translation via gettext catalogs. 60 | # Usually you set "language" from the command line for these cases. 61 | language = None 62 | 63 | # List of patterns, relative to source directory, that match files and 64 | # directories to ignore when looking for source files. 65 | # This pattern also affects html_static_path and html_extra_path. 66 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 67 | 68 | # The name of the Pygments (syntax highlighting) style to use. 69 | pygments_style = None 70 | 71 | 72 | # -- Options for HTML output ------------------------------------------------- 73 | 74 | # The theme to use for HTML and HTML Help pages. See the documentation for 75 | # a list of builtin themes. 76 | # 77 | html_theme = 'alabaster' 78 | 79 | # Theme options are theme-specific and customize the look and feel of a theme 80 | # further. For a list of options available for each theme, see the 81 | # documentation. 82 | # 83 | html_theme_options = { 84 | 'github_user': 'miguelgrinberg', 85 | 'github_repo': 'flask-migrate', 86 | 'github_banner': True, 87 | 'github_button': True, 88 | 'github_type': 'star', 89 | 'fixed_sidebar': True, 90 | } 91 | 92 | # Add any paths that contain custom static files (such as style sheets) here, 93 | # relative to this directory. They are copied after the builtin static files, 94 | # so a file named "default.css" will overwrite the builtin "default.css". 95 | html_static_path = [] 96 | 97 | # Custom sidebar templates, must be a dictionary that maps document names 98 | # to template names. 99 | # 100 | # The default sidebars (for documents that don't match any pattern) are 101 | # defined by theme itself. Builtin themes are using these templates by 102 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 103 | # 'searchbox.html']``. 104 | # 105 | html_sidebars = { 106 | '**': [ 107 | 'about.html', 108 | 'localtoc.html', 109 | 'searchbox.html' 110 | ] 111 | } 112 | 113 | 114 | # -- Options for HTMLHelp output --------------------------------------------- 115 | 116 | # Output file base name for HTML help builder. 117 | htmlhelp_basename = 'Flask-Migratedoc' 118 | 119 | 120 | # -- Options for LaTeX output ------------------------------------------------ 121 | 122 | latex_elements = { 123 | # The paper size ('letterpaper' or 'a4paper'). 124 | # 125 | # 'papersize': 'letterpaper', 126 | 127 | # The font size ('10pt', '11pt' or '12pt'). 128 | # 129 | # 'pointsize': '10pt', 130 | 131 | # Additional stuff for the LaTeX preamble. 132 | # 133 | # 'preamble': '', 134 | 135 | # Latex figure (float) alignment 136 | # 137 | # 'figure_align': 'htbp', 138 | } 139 | 140 | # Grouping the document tree into LaTeX files. List of tuples 141 | # (source start file, target name, title, 142 | # author, documentclass [howto, manual, or own class]). 143 | latex_documents = [ 144 | (master_doc, 'Flask-Migrate.tex', 'Flask-Migrate Documentation', 145 | 'Miguel Grinberg', 'manual'), 146 | ] 147 | 148 | 149 | # -- Options for manual page output ------------------------------------------ 150 | 151 | # One entry per manual page. List of tuples 152 | # (source start file, name, description, authors, manual section). 153 | man_pages = [ 154 | (master_doc, 'flask-migrate', 'Flask-Migrate Documentation', 155 | [author], 1) 156 | ] 157 | 158 | 159 | # -- Options for Texinfo output ---------------------------------------------- 160 | 161 | # Grouping the document tree into Texinfo files. List of tuples 162 | # (source start file, target name, title, author, 163 | # dir menu entry, description, category) 164 | texinfo_documents = [ 165 | (master_doc, 'Flask-Migrate', 'Flask-Migrate Documentation', 166 | author, 'Flask-Migrate', 'One line description of project.', 167 | 'Miscellaneous'), 168 | ] 169 | 170 | 171 | # -- Options for Epub output ------------------------------------------------- 172 | 173 | # Bibliographic Dublin Core info. 174 | epub_title = project 175 | 176 | # The unique identifier of the text. This can be a ISBN number 177 | # or the project homepage. 178 | # 179 | # epub_identifier = '' 180 | 181 | # A unique identification for the text. 182 | # 183 | # epub_uid = '' 184 | 185 | # A list of files that should not be packed into the epub file. 186 | epub_exclude_files = ['search.html'] 187 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Flask-Migrate documentation master file, created by 2 | sphinx-quickstart on Fri Jul 26 14:48:13 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Flask-Migrate 7 | ============= 8 | 9 | **Flask-Migrate** is an extension that handles SQLAlchemy database migrations for Flask applications using Alembic. The database operations are made available through the Flask command-line interface. 10 | 11 | Why Use Flask-Migrate vs. Alembic Directly? 12 | ------------------------------------------- 13 | 14 | Flask-Migrate is an extension that configures Alembic in the proper way to work with your Flask and Flask-SQLAlchemy application. In terms of the actual database migrations, everything is handled by Alembic so you get exactly the same functionality. 15 | 16 | Installation 17 | ------------ 18 | 19 | Install Flask-Migrate with `pip`:: 20 | 21 | pip install Flask-Migrate 22 | 23 | Example 24 | ------- 25 | 26 | This is an example application that handles database migrations through Flask-Migrate:: 27 | 28 | from flask import Flask 29 | from flask_sqlalchemy import SQLAlchemy 30 | from flask_migrate import Migrate 31 | 32 | app = Flask(__name__) 33 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db' 34 | 35 | db = SQLAlchemy(app) 36 | migrate = Migrate(app, db) 37 | 38 | class User(db.Model): 39 | id = db.Column(db.Integer, primary_key=True) 40 | name = db.Column(db.String(128)) 41 | 42 | With the above application you can create a migration repository with the following command:: 43 | 44 | $ flask db init 45 | 46 | This will add a `migrations` folder to your application. The contents of this folder need to be added to version control along with your other source files. 47 | 48 | You can then generate an initial migration:: 49 | 50 | $ flask db migrate -m "Initial migration." 51 | 52 | The migration script needs to be reviewed and edited, as Alembic is not always able to detect every change you make to your models. In particular, Alembic is currently unable to detect table name changes, column name changes, or anonymously named constraints. A detailed summary of limitations can be found in the `Alembic autogenerate documentation `_. Once finalized, the migration script also needs to be added to version control. 53 | 54 | Then you can apply the changes described by the migration script to your database:: 55 | 56 | $ flask db upgrade 57 | 58 | Each time the database models change, repeat the ``migrate`` and ``upgrade`` commands. 59 | 60 | To sync the database in another system just refresh the `migrations` folder from source control and run the ``upgrade`` command. 61 | 62 | To see all the commands that are available run this command:: 63 | 64 | $ flask db --help 65 | 66 | Note that the application script must be set in the ``FLASK_APP`` environment variable for all the above commands to work, as required by the ``flask`` command. 67 | 68 | If the ``db`` command group name is inconvenient, it can be changed to a different with the ``command`` argument passed to the ``Migrate`` class:: 69 | 70 | migrate = Migrate(app, db, command='migrate') 71 | 72 | Alembic Configuration Options 73 | ----------------------------- 74 | 75 | Starting with version 4.0, Flask-Migrate automatically enables the following options that are disabled by default in Alembic: 76 | 77 | - ``compare_type=True``: This option configures the automatic migration generation subsystem to detect column type changes. 78 | - ``render_as_batch=True``: This option generates migration scripts using `batch mode `_, an operational mode that works around limitations of many ``ALTER`` commands in the SQLite database by implementing a "move and copy" workflow. Enabling this mode should make no difference when working with other databases. 79 | 80 | To manually configure these or `other Alembic options `_, pass them as keyword arguments to the ``Migrate`` constructor. Example:: 81 | 82 | migrate = Migrate(app, db, render_as_batch=False) 83 | 84 | Configuration Callbacks 85 | ----------------------- 86 | 87 | Sometimes applications need to dynamically insert their own settings into the Alembic configuration. A function decorated with the ``configure`` callback will be invoked after the configuration is read, and before it is applied. The function can modify the configuration object, or replace it with a different one. 88 | 89 | :: 90 | 91 | @migrate.configure 92 | def configure_alembic(config): 93 | # modify config object 94 | return config 95 | 96 | Multiple configuration callbacks can be defined simply by decorating multiple functions. The order in which multiple callbacks are invoked is undetermined. 97 | 98 | Multiple Database Support 99 | ------------------------- 100 | 101 | Flask-Migrate can integrate with the `binds `_ feature of Flask-SQLAlchemy, making it possible to track migrations to multiple databases associated with an application. 102 | 103 | To create a multiple database migration repository, add the ``--multidb`` argument to the ``init`` command:: 104 | 105 | $ flask db init --multidb 106 | 107 | With this command, the migration repository will be set up to track migrations on your main database, and on any additional databases defined in the ``SQLALCHEMY_BINDS`` configuration option. 108 | 109 | Command Reference 110 | ----------------- 111 | 112 | Flask-Migrate exposes one class called ``Migrate``. This class contains all the functionality of the extension. 113 | 114 | The following example initializes the extension with the standard Flask command-line interface:: 115 | 116 | from flask_migrate import Migrate 117 | migrate = Migrate(app, db) 118 | 119 | The two arguments to ``Migrate`` are the application instance and the Flask-SQLAlchemy database instance. The ``Migrate`` constructor also takes additional keyword arguments, which are passed to Alembic's ``EnvironmentContext.configure()`` method. As is standard for all Flask extensions, Flask-Migrate can be initialized using the ``init_app`` method as well:: 120 | 121 | from flask_sqlalchemy import SQLAlchemy 122 | from flask_migrate import Migrate 123 | 124 | db = SQLAlchemy() 125 | migrate = Migrate() 126 | 127 | def create_app(): 128 | """Application-factory pattern""" 129 | ... 130 | ... 131 | db.init_app(app) 132 | migrate.init_app(app, db) 133 | ... 134 | ... 135 | return app 136 | 137 | After the extension is initialized, a ``db`` group will be added to the command-line options with several sub-commands. Below is a list of the available sub-commands: 138 | 139 | - ``flask db --help`` 140 | Shows a list of available commands. 141 | 142 | - ``flask db list-templates`` 143 | Shows a list of available database repository templates. 144 | 145 | - ``flask db init [--multidb] [--template TEMPLATE] [--package]`` 146 | Initializes migration support for the application. The optional ``--multidb`` enables migrations for multiple databases configured as `Flask-SQLAlchemy binds `_. The ``--template`` option allows you to explicitly select a database repository template, either from the stock templates provided by this package, or a custom one, given as a path to the template directory. The ``--package`` option tells Alembic to add ``__init__.py`` files in the migrations and versions directories. 147 | 148 | - ``flask db revision [--message MESSAGE] [--autogenerate] [--sql] [--head HEAD] [--splice] [--branch-label BRANCH_LABEL] [--version-path VERSION_PATH] [--rev-id REV_ID]`` 149 | Creates an empty revision script. The script needs to be edited manually with the upgrade and downgrade changes. See `Alembic's documentation `_ for instructions on how to write migration scripts. An optional migration message can be included. 150 | 151 | - ``flask db migrate [--message MESSAGE] [--sql] [--head HEAD] [--splice] [--branch-label BRANCH_LABEL] [--version-path VERSION_PATH] [--rev-id REV_ID]`` 152 | Equivalent to ``revision --autogenerate``. The migration script is populated with changes detected automatically. The generated script should to be reviewed and edited as not all types of changes can be detected automatically. This command does not make any changes to the database, just creates the revision script. 153 | 154 | - ``flask db check`` 155 | Checks that a ``migrate`` command would not generate any changes. If pending changes are detected, the command exits with a non-zero status code. 156 | 157 | - ``flask db edit `` 158 | Edit a revision script using $EDITOR. 159 | 160 | - ``flask db upgrade [--sql] [--tag TAG] `` 161 | Upgrades the database. If ``revision`` isn't given then ``"head"`` is assumed. 162 | 163 | - ``flask db downgrade [--sql] [--tag TAG] `` 164 | Downgrades the database. If ``revision`` isn't given then ``-1`` is assumed. 165 | 166 | - ``flask db stamp [--sql] [--tag TAG] `` 167 | Sets the revision in the database to the one given as an argument, without performing any migrations. 168 | 169 | - ``flask db current [--verbose]`` 170 | Shows the current revision of the database. 171 | 172 | - ``flask db history [--rev-range REV_RANGE] [--verbose]`` 173 | Shows the list of migrations. If a range isn't given then the entire history is shown. 174 | 175 | - ``flask db show `` 176 | Show the revision denoted by the given symbol. 177 | 178 | - ``flask db merge [--message MESSAGE] [--branch-label BRANCH_LABEL] [--rev-id REV_ID] `` 179 | Merge two revisions together. Creates a new revision file. 180 | 181 | - ``flask db heads [--verbose] [--resolve-dependencies]`` 182 | Show current available heads in the revision script directory. 183 | 184 | - ``flask db branches [--verbose]`` 185 | Show current branch points. 186 | 187 | Notes: 188 | 189 | - All commands take one or more ``--x-arg ARG=VALUE`` or ``-x ARG=VALUE`` options with custom arguments that can be used in ``env.py``. 190 | - All commands take a ``--directory DIRECTORY`` option that points to the directory containing the migration scripts. If this argument is omitted the directory used is ``migrations``. 191 | - A directory can also be specified as a ``directory`` argument to the ``Migrate`` constructor, or in the ``FLASK_DB_DIRECTORY`` environment variable. 192 | - The ``--sql`` option present in several commands performs an 'offline' mode migration. Instead of executing the database commands the SQL statements that need to be executed are printed to the console. 193 | - Detailed documentation on these commands can be found in the `Alembic's command reference page `_. 194 | 195 | API Reference 196 | ------------- 197 | 198 | The commands exposed by Flask-Migrate's command-line interface can also be accessed programmatically by importing the functions from module ``flask_migrate``. The available functions are: 199 | 200 | - ``init(directory='migrations', multidb=False)`` 201 | Initializes migration support for the application. 202 | 203 | - ``revision(directory='migrations', message=None, autogenerate=False, sql=False, head='head', splice=False, branch_label=None, version_path=None, rev_id=None)`` 204 | Creates an empty revision script. 205 | 206 | - ``migrate(directory='migrations', message=None, sql=False, head='head', splice=False, branch_label=None, version_path=None, rev_id=None)`` 207 | Creates an automatic revision script. 208 | 209 | - ``edit(directory='migrations', revision='head')`` 210 | Edit revision script(s) using $EDITOR. 211 | 212 | - ``merge(directory='migrations', revisions='', message=None, branch_label=None, rev_id=None)`` 213 | Merge two revisions together. Creates a new migration file. 214 | 215 | - ``upgrade(directory='migrations', revision='head', sql=False, tag=None)`` 216 | Upgrades the database. 217 | 218 | - ``downgrade(directory='migrations', revision='-1', sql=False, tag=None)`` 219 | Downgrades the database. 220 | 221 | - ``show(directory='migrations', revision='head')`` 222 | Show the revision denoted by the given symbol. 223 | 224 | - ``history(directory='migrations', rev_range=None, verbose=False)`` 225 | Shows the list of migrations. If a range isn't given then the entire history is shown. 226 | 227 | - ``heads(directory='migrations', verbose=False, resolve_dependencies=False)`` 228 | Show current available heads in the script directory. 229 | 230 | - ``branches(directory='migrations', verbose=False)`` 231 | Show current branch points 232 | 233 | - ``current(directory='migrations', verbose=False, head_only=False)`` 234 | Shows the current revision of the database. 235 | 236 | - ``stamp(directory='migrations', revision='head', sql=False, tag=None)`` 237 | Sets the revision in the database to the one given as an argument, without performing any migrations. 238 | 239 | Notes: 240 | 241 | - These commands will invoke the same functionality that runs from the command-line, including output to the terminal. The logging configuration of the process will be overridden by Alembic according to the contents of the alembic.ini file. 242 | - For greater scripting flexibility you can also use the API exposed by Alembic directly. 243 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "Flask-Migrate" 3 | version = "4.1.1.dev0" 4 | authors = [ 5 | { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, 6 | ] 7 | description = "SQLAlchemy database migrations for Flask applications using Alembic." 8 | classifiers = [ 9 | "Environment :: Web Environment", 10 | "Intended Audience :: Developers", 11 | "Programming Language :: Python :: 3", 12 | "License :: OSI Approved :: MIT License", 13 | "Operating System :: OS Independent", 14 | ] 15 | requires-python = ">=3.6" 16 | dependencies = [ 17 | "Flask >= 0.9", 18 | "Flask-SQLAlchemy >= 1.0", 19 | "alembic >= 1.9.0", 20 | ] 21 | 22 | [project.optional-dependencies] 23 | dev = [ 24 | "tox", 25 | "flake8", 26 | "pytest", 27 | ] 28 | docs = [ 29 | "sphinx", 30 | ] 31 | 32 | [project.license] 33 | text = "MIT" 34 | 35 | [project.readme] 36 | file = "README.md" 37 | content-type = "text/markdown" 38 | 39 | [project.urls] 40 | Homepage = "https://github.com/miguelgrinberg/flask-migrate" 41 | "Bug Tracker" = "https://github.com/miguelgrinberg/flask-migrate/issues" 42 | 43 | [tool.setuptools] 44 | zip-safe = false 45 | include-package-data = true 46 | 47 | [tool.setuptools.package-dir] 48 | "" = "src" 49 | 50 | [tool.setuptools.packages.find] 51 | where = [ 52 | "src", 53 | ] 54 | namespaces = true 55 | 56 | [build-system] 57 | requires = [ 58 | "setuptools>=61.2", 59 | ] 60 | build-backend = "setuptools.build_meta" 61 | -------------------------------------------------------------------------------- /src/flask_migrate/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from functools import wraps 3 | import logging 4 | import os 5 | import sys 6 | from flask import current_app, g 7 | from alembic import __version__ as __alembic_version__ 8 | from alembic.config import Config as AlembicConfig 9 | from alembic import command 10 | from alembic.util import CommandError 11 | 12 | alembic_version = tuple([int(v) for v in __alembic_version__.split('.')[0:3]]) 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class _MigrateConfig(object): 17 | def __init__(self, migrate, db, **kwargs): 18 | self.migrate = migrate 19 | self.db = db 20 | self.directory = migrate.directory 21 | self.configure_args = kwargs 22 | 23 | @property 24 | def metadata(self): 25 | """ 26 | Backwards compatibility, in old releases app.extensions['migrate'] 27 | was set to db, and env.py accessed app.extensions['migrate'].metadata 28 | """ 29 | return self.db.metadata 30 | 31 | 32 | class Config(AlembicConfig): 33 | def __init__(self, *args, **kwargs): 34 | self.template_directory = kwargs.pop('template_directory', None) 35 | super().__init__(*args, **kwargs) 36 | 37 | def get_template_directory(self): 38 | if self.template_directory: 39 | return self.template_directory 40 | package_dir = os.path.abspath(os.path.dirname(__file__)) 41 | return os.path.join(package_dir, 'templates') 42 | 43 | 44 | class Migrate(object): 45 | def __init__(self, app=None, db=None, directory='migrations', command='db', 46 | compare_type=True, render_as_batch=True, **kwargs): 47 | self.configure_callbacks = [] 48 | self.db = db 49 | self.command = command 50 | self.directory = str(directory) 51 | self.alembic_ctx_kwargs = kwargs 52 | self.alembic_ctx_kwargs['compare_type'] = compare_type 53 | self.alembic_ctx_kwargs['render_as_batch'] = render_as_batch 54 | if app is not None and db is not None: 55 | self.init_app(app, db, directory) 56 | 57 | def init_app(self, app, db=None, directory=None, command=None, 58 | compare_type=None, render_as_batch=None, **kwargs): 59 | self.db = db or self.db 60 | self.command = command or self.command 61 | self.directory = str(directory or self.directory) 62 | self.alembic_ctx_kwargs.update(kwargs) 63 | if compare_type is not None: 64 | self.alembic_ctx_kwargs['compare_type'] = compare_type 65 | if render_as_batch is not None: 66 | self.alembic_ctx_kwargs['render_as_batch'] = render_as_batch 67 | if not hasattr(app, 'extensions'): 68 | app.extensions = {} 69 | app.extensions['migrate'] = _MigrateConfig( 70 | self, self.db, **self.alembic_ctx_kwargs) 71 | 72 | from flask_migrate.cli import db as db_cli_group 73 | app.cli.add_command(db_cli_group, name=self.command) 74 | 75 | def configure(self, f): 76 | self.configure_callbacks.append(f) 77 | return f 78 | 79 | def call_configure_callbacks(self, config): 80 | for f in self.configure_callbacks: 81 | config = f(config) 82 | return config 83 | 84 | def get_config(self, directory=None, x_arg=None, opts=None): 85 | if directory is None: 86 | directory = self.directory 87 | directory = str(directory) 88 | config = Config(os.path.join(directory, 'alembic.ini')) 89 | config.set_main_option('script_location', directory) 90 | if config.cmd_opts is None: 91 | config.cmd_opts = argparse.Namespace() 92 | for opt in opts or []: 93 | setattr(config.cmd_opts, opt, True) 94 | if not hasattr(config.cmd_opts, 'x'): 95 | setattr(config.cmd_opts, 'x', []) 96 | for x in getattr(g, 'x_arg', []): 97 | config.cmd_opts.x.append(x) 98 | if x_arg is not None: 99 | if isinstance(x_arg, list) or isinstance(x_arg, tuple): 100 | for x in x_arg: 101 | config.cmd_opts.x.append(x) 102 | else: 103 | config.cmd_opts.x.append(x_arg) 104 | return self.call_configure_callbacks(config) 105 | 106 | 107 | def catch_errors(f): 108 | @wraps(f) 109 | def wrapped(*args, **kwargs): 110 | try: 111 | f(*args, **kwargs) 112 | except (CommandError, RuntimeError) as exc: 113 | log.error('Error: ' + str(exc)) 114 | sys.exit(1) 115 | return wrapped 116 | 117 | 118 | @catch_errors 119 | def list_templates(): 120 | """List available templates.""" 121 | config = Config() 122 | config.print_stdout("Available templates:\n") 123 | for tempname in sorted(os.listdir(config.get_template_directory())): 124 | with open( 125 | os.path.join(config.get_template_directory(), tempname, "README") 126 | ) as readme: 127 | synopsis = next(readme).strip() 128 | config.print_stdout("%s - %s", tempname, synopsis) 129 | 130 | 131 | @catch_errors 132 | def init(directory=None, multidb=False, template=None, package=False): 133 | """Creates a new migration repository""" 134 | if directory is None: 135 | directory = current_app.extensions['migrate'].directory 136 | template_directory = None 137 | if template is not None and ('/' in template or '\\' in template): 138 | template_directory, template = os.path.split(template) 139 | config = Config(template_directory=template_directory) 140 | config.set_main_option('script_location', directory) 141 | config.config_file_name = os.path.join(directory, 'alembic.ini') 142 | config = current_app.extensions['migrate'].\ 143 | migrate.call_configure_callbacks(config) 144 | if multidb and template is None: 145 | template = 'flask-multidb' 146 | elif template is None: 147 | template = 'flask' 148 | command.init(config, directory, template=template, package=package) 149 | 150 | 151 | @catch_errors 152 | def revision(directory=None, message=None, autogenerate=False, sql=False, 153 | head='head', splice=False, branch_label=None, version_path=None, 154 | rev_id=None): 155 | """Create a new revision file.""" 156 | opts = ['autogenerate'] if autogenerate else None 157 | config = current_app.extensions['migrate'].migrate.get_config( 158 | directory, opts=opts) 159 | command.revision(config, message, autogenerate=autogenerate, sql=sql, 160 | head=head, splice=splice, branch_label=branch_label, 161 | version_path=version_path, rev_id=rev_id) 162 | 163 | 164 | @catch_errors 165 | def migrate(directory=None, message=None, sql=False, head='head', splice=False, 166 | branch_label=None, version_path=None, rev_id=None, x_arg=None): 167 | """Alias for 'revision --autogenerate'""" 168 | config = current_app.extensions['migrate'].migrate.get_config( 169 | directory, opts=['autogenerate'], x_arg=x_arg) 170 | command.revision(config, message, autogenerate=True, sql=sql, 171 | head=head, splice=splice, branch_label=branch_label, 172 | version_path=version_path, rev_id=rev_id) 173 | 174 | 175 | @catch_errors 176 | def edit(directory=None, revision='current'): 177 | """Edit current revision.""" 178 | if alembic_version >= (0, 8, 0): 179 | config = current_app.extensions['migrate'].migrate.get_config( 180 | directory) 181 | command.edit(config, revision) 182 | else: 183 | raise RuntimeError('Alembic 0.8.0 or greater is required') 184 | 185 | 186 | @catch_errors 187 | def merge(directory=None, revisions='', message=None, branch_label=None, 188 | rev_id=None): 189 | """Merge two revisions together. Creates a new migration file""" 190 | config = current_app.extensions['migrate'].migrate.get_config(directory) 191 | command.merge(config, revisions, message=message, 192 | branch_label=branch_label, rev_id=rev_id) 193 | 194 | 195 | @catch_errors 196 | def upgrade(directory=None, revision='head', sql=False, tag=None, x_arg=None): 197 | """Upgrade to a later version""" 198 | config = current_app.extensions['migrate'].migrate.get_config(directory, 199 | x_arg=x_arg) 200 | command.upgrade(config, revision, sql=sql, tag=tag) 201 | 202 | 203 | @catch_errors 204 | def downgrade(directory=None, revision='-1', sql=False, tag=None, x_arg=None): 205 | """Revert to a previous version""" 206 | config = current_app.extensions['migrate'].migrate.get_config(directory, 207 | x_arg=x_arg) 208 | if sql and revision == '-1': 209 | revision = 'head:-1' 210 | command.downgrade(config, revision, sql=sql, tag=tag) 211 | 212 | 213 | @catch_errors 214 | def show(directory=None, revision='head'): 215 | """Show the revision denoted by the given symbol.""" 216 | config = current_app.extensions['migrate'].migrate.get_config(directory) 217 | command.show(config, revision) 218 | 219 | 220 | @catch_errors 221 | def history(directory=None, rev_range=None, verbose=False, 222 | indicate_current=False): 223 | """List changeset scripts in chronological order.""" 224 | config = current_app.extensions['migrate'].migrate.get_config(directory) 225 | if alembic_version >= (0, 9, 9): 226 | command.history(config, rev_range, verbose=verbose, 227 | indicate_current=indicate_current) 228 | else: 229 | command.history(config, rev_range, verbose=verbose) 230 | 231 | 232 | @catch_errors 233 | def heads(directory=None, verbose=False, resolve_dependencies=False): 234 | """Show current available heads in the script directory""" 235 | config = current_app.extensions['migrate'].migrate.get_config(directory) 236 | command.heads(config, verbose=verbose, 237 | resolve_dependencies=resolve_dependencies) 238 | 239 | 240 | @catch_errors 241 | def branches(directory=None, verbose=False): 242 | """Show current branch points""" 243 | config = current_app.extensions['migrate'].migrate.get_config(directory) 244 | command.branches(config, verbose=verbose) 245 | 246 | 247 | @catch_errors 248 | def current(directory=None, verbose=False): 249 | """Display the current revision for each database.""" 250 | config = current_app.extensions['migrate'].migrate.get_config(directory) 251 | command.current(config, verbose=verbose) 252 | 253 | 254 | @catch_errors 255 | def stamp(directory=None, revision='head', sql=False, tag=None, purge=False): 256 | """'stamp' the revision table with the given revision; don't run any 257 | migrations""" 258 | config = current_app.extensions['migrate'].migrate.get_config(directory) 259 | command.stamp(config, revision, sql=sql, tag=tag, purge=purge) 260 | 261 | 262 | @catch_errors 263 | def check(directory=None): 264 | """Check if there are any new operations to migrate""" 265 | config = current_app.extensions['migrate'].migrate.get_config(directory) 266 | command.check(config) 267 | -------------------------------------------------------------------------------- /src/flask_migrate/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | from flask import g 3 | from flask.cli import with_appcontext 4 | from flask_migrate import list_templates as _list_templates 5 | from flask_migrate import init as _init 6 | from flask_migrate import revision as _revision 7 | from flask_migrate import migrate as _migrate 8 | from flask_migrate import edit as _edit 9 | from flask_migrate import merge as _merge 10 | from flask_migrate import upgrade as _upgrade 11 | from flask_migrate import downgrade as _downgrade 12 | from flask_migrate import show as _show 13 | from flask_migrate import history as _history 14 | from flask_migrate import heads as _heads 15 | from flask_migrate import branches as _branches 16 | from flask_migrate import current as _current 17 | from flask_migrate import stamp as _stamp 18 | from flask_migrate import check as _check 19 | 20 | 21 | @click.group() 22 | @click.option('-d', '--directory', default=None, 23 | help=('Migration script directory (default is "migrations")')) 24 | @click.option('-x', '--x-arg', multiple=True, 25 | help='Additional arguments consumed by custom env.py scripts') 26 | @with_appcontext 27 | def db(directory, x_arg): 28 | """Perform database migrations.""" 29 | g.directory = directory 30 | g.x_arg = x_arg # these will be picked up by Migrate.get_config() 31 | 32 | 33 | @db.command() 34 | @with_appcontext 35 | def list_templates(): 36 | """List available templates.""" 37 | _list_templates() 38 | 39 | 40 | @db.command() 41 | @click.option('-d', '--directory', default=None, 42 | help=('Migration script directory (default is "migrations")')) 43 | @click.option('--multidb', is_flag=True, 44 | help=('Support multiple databases')) 45 | @click.option('-t', '--template', default=None, 46 | help=('Repository template to use (default is "flask")')) 47 | @click.option('--package', is_flag=True, 48 | help=('Write empty __init__.py files to the environment and ' 49 | 'version locations')) 50 | @with_appcontext 51 | def init(directory, multidb, template, package): 52 | """Creates a new migration repository.""" 53 | _init(directory or g.directory, multidb, template, package) 54 | 55 | 56 | @db.command() 57 | @click.option('-d', '--directory', default=None, 58 | help=('Migration script directory (default is "migrations")')) 59 | @click.option('-m', '--message', default=None, help='Revision message') 60 | @click.option('--autogenerate', is_flag=True, 61 | help=('Populate revision script with candidate migration ' 62 | 'operations, based on comparison of database to model')) 63 | @click.option('--sql', is_flag=True, 64 | help=('Don\'t emit SQL to database - dump to standard output ' 65 | 'instead')) 66 | @click.option('--head', default='head', 67 | help=('Specify head revision or @head to base new ' 68 | 'revision on')) 69 | @click.option('--splice', is_flag=True, 70 | help=('Allow a non-head revision as the "head" to splice onto')) 71 | @click.option('--branch-label', default=None, 72 | help=('Specify a branch label to apply to the new revision')) 73 | @click.option('--version-path', default=None, 74 | help=('Specify specific path from config for version file')) 75 | @click.option('--rev-id', default=None, 76 | help=('Specify a hardcoded revision id instead of generating ' 77 | 'one')) 78 | @with_appcontext 79 | def revision(directory, message, autogenerate, sql, head, splice, branch_label, 80 | version_path, rev_id): 81 | """Create a new revision file.""" 82 | _revision(directory or g.directory, message, autogenerate, sql, head, 83 | splice, branch_label, version_path, rev_id) 84 | 85 | 86 | @db.command() 87 | @click.option('-d', '--directory', default=None, 88 | help=('Migration script directory (default is "migrations")')) 89 | @click.option('-m', '--message', default=None, help='Revision message') 90 | @click.option('--sql', is_flag=True, 91 | help=('Don\'t emit SQL to database - dump to standard output ' 92 | 'instead')) 93 | @click.option('--head', default='head', 94 | help=('Specify head revision or @head to base new ' 95 | 'revision on')) 96 | @click.option('--splice', is_flag=True, 97 | help=('Allow a non-head revision as the "head" to splice onto')) 98 | @click.option('--branch-label', default=None, 99 | help=('Specify a branch label to apply to the new revision')) 100 | @click.option('--version-path', default=None, 101 | help=('Specify specific path from config for version file')) 102 | @click.option('--rev-id', default=None, 103 | help=('Specify a hardcoded revision id instead of generating ' 104 | 'one')) 105 | @click.option('-x', '--x-arg', multiple=True, 106 | help='Additional arguments consumed by custom env.py scripts') 107 | @with_appcontext 108 | def migrate(directory, message, sql, head, splice, branch_label, version_path, 109 | rev_id, x_arg): 110 | """Autogenerate a new revision file (Alias for 111 | 'revision --autogenerate')""" 112 | _migrate(directory or g.directory, message, sql, head, splice, 113 | branch_label, version_path, rev_id, x_arg or g.x_arg) 114 | 115 | 116 | @db.command() 117 | @click.option('-d', '--directory', default=None, 118 | help=('Migration script directory (default is "migrations")')) 119 | @click.argument('revision', default='head') 120 | @with_appcontext 121 | def edit(directory, revision): 122 | """Edit a revision file""" 123 | _edit(directory or g.directory, revision) 124 | 125 | 126 | @db.command() 127 | @click.option('-d', '--directory', default=None, 128 | help=('Migration script directory (default is "migrations")')) 129 | @click.option('-m', '--message', default=None, help='Merge revision message') 130 | @click.option('--branch-label', default=None, 131 | help=('Specify a branch label to apply to the new revision')) 132 | @click.option('--rev-id', default=None, 133 | help=('Specify a hardcoded revision id instead of generating ' 134 | 'one')) 135 | @click.argument('revisions', nargs=-1) 136 | @with_appcontext 137 | def merge(directory, message, branch_label, rev_id, revisions): 138 | """Merge two revisions together, creating a new revision file""" 139 | _merge(directory or g.directory, revisions, message, branch_label, rev_id) 140 | 141 | 142 | @db.command() 143 | @click.option('-d', '--directory', default=None, 144 | help=('Migration script directory (default is "migrations")')) 145 | @click.option('--sql', is_flag=True, 146 | help=('Don\'t emit SQL to database - dump to standard output ' 147 | 'instead')) 148 | @click.option('--tag', default=None, 149 | help=('Arbitrary "tag" name - can be used by custom env.py ' 150 | 'scripts')) 151 | @click.option('-x', '--x-arg', multiple=True, 152 | help='Additional arguments consumed by custom env.py scripts') 153 | @click.argument('revision', default='head') 154 | @with_appcontext 155 | def upgrade(directory, sql, tag, x_arg, revision): 156 | """Upgrade to a later version""" 157 | _upgrade(directory or g.directory, revision, sql, tag, x_arg or g.x_arg) 158 | 159 | 160 | @db.command() 161 | @click.option('-d', '--directory', default=None, 162 | help=('Migration script directory (default is "migrations")')) 163 | @click.option('--sql', is_flag=True, 164 | help=('Don\'t emit SQL to database - dump to standard output ' 165 | 'instead')) 166 | @click.option('--tag', default=None, 167 | help=('Arbitrary "tag" name - can be used by custom env.py ' 168 | 'scripts')) 169 | @click.option('-x', '--x-arg', multiple=True, 170 | help='Additional arguments consumed by custom env.py scripts') 171 | @click.argument('revision', default='-1') 172 | @with_appcontext 173 | def downgrade(directory, sql, tag, x_arg, revision): 174 | """Revert to a previous version""" 175 | _downgrade(directory or g.directory, revision, sql, tag, x_arg or g.x_arg) 176 | 177 | 178 | @db.command() 179 | @click.option('-d', '--directory', default=None, 180 | help=('Migration script directory (default is "migrations")')) 181 | @click.argument('revision', default='head') 182 | @with_appcontext 183 | def show(directory, revision): 184 | """Show the revision denoted by the given symbol.""" 185 | _show(directory or g.directory, revision) 186 | 187 | 188 | @db.command() 189 | @click.option('-d', '--directory', default=None, 190 | help=('Migration script directory (default is "migrations")')) 191 | @click.option('-r', '--rev-range', default=None, 192 | help='Specify a revision range; format is [start]:[end]') 193 | @click.option('-v', '--verbose', is_flag=True, help='Use more verbose output') 194 | @click.option('-i', '--indicate-current', is_flag=True, 195 | help=('Indicate current version (Alembic 0.9.9 or greater is ' 196 | 'required)')) 197 | @with_appcontext 198 | def history(directory, rev_range, verbose, indicate_current): 199 | """List changeset scripts in chronological order.""" 200 | _history(directory or g.directory, rev_range, verbose, indicate_current) 201 | 202 | 203 | @db.command() 204 | @click.option('-d', '--directory', default=None, 205 | help=('Migration script directory (default is "migrations")')) 206 | @click.option('-v', '--verbose', is_flag=True, help='Use more verbose output') 207 | @click.option('--resolve-dependencies', is_flag=True, 208 | help='Treat dependency versions as down revisions') 209 | @with_appcontext 210 | def heads(directory, verbose, resolve_dependencies): 211 | """Show current available heads in the script directory""" 212 | _heads(directory or g.directory, verbose, resolve_dependencies) 213 | 214 | 215 | @db.command() 216 | @click.option('-d', '--directory', default=None, 217 | help=('Migration script directory (default is "migrations")')) 218 | @click.option('-v', '--verbose', is_flag=True, help='Use more verbose output') 219 | @with_appcontext 220 | def branches(directory, verbose): 221 | """Show current branch points""" 222 | _branches(directory or g.directory, verbose) 223 | 224 | 225 | @db.command() 226 | @click.option('-d', '--directory', default=None, 227 | help=('Migration script directory (default is "migrations")')) 228 | @click.option('-v', '--verbose', is_flag=True, help='Use more verbose output') 229 | @with_appcontext 230 | def current(directory, verbose): 231 | """Display the current revision for each database.""" 232 | _current(directory or g.directory, verbose) 233 | 234 | 235 | @db.command() 236 | @click.option('-d', '--directory', default=None, 237 | help=('Migration script directory (default is "migrations")')) 238 | @click.option('--sql', is_flag=True, 239 | help=('Don\'t emit SQL to database - dump to standard output ' 240 | 'instead')) 241 | @click.option('--tag', default=None, 242 | help=('Arbitrary "tag" name - can be used by custom env.py ' 243 | 'scripts')) 244 | @click.option('--purge', is_flag=True, 245 | help=('Delete the version in the alembic_version table before ' 246 | 'stamping')) 247 | @click.argument('revision', default='head') 248 | @with_appcontext 249 | def stamp(directory, sql, tag, revision, purge): 250 | """'stamp' the revision table with the given revision; don't run any 251 | migrations""" 252 | _stamp(directory or g.directory, revision, sql, tag, purge) 253 | 254 | 255 | @db.command() 256 | @click.option('-d', '--directory', default=None, 257 | help=('Migration script directory (default is "migrations")')) 258 | @with_appcontext 259 | def check(directory): 260 | """Check if there are any new operations to migrate""" 261 | _check(directory or g.directory) 262 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/aioflask-multidb/README: -------------------------------------------------------------------------------- 1 | Multi-database configuration for aioflask. 2 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/aioflask-multidb/alembic.ini.mako: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,flask_migrate 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/aioflask-multidb/env.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from logging.config import fileConfig 4 | 5 | from sqlalchemy import MetaData 6 | from flask import current_app 7 | 8 | from alembic import context 9 | 10 | USE_TWOPHASE = False 11 | 12 | # this is the Alembic Config object, which provides 13 | # access to the values within the .ini file in use. 14 | config = context.config 15 | 16 | # Interpret the config file for Python logging. 17 | # This line sets up loggers basically. 18 | fileConfig(config.config_file_name) 19 | logger = logging.getLogger('alembic.env') 20 | 21 | 22 | def get_engine(bind_key=None): 23 | try: 24 | # this works with Flask-SQLAlchemy<3 and Alchemical 25 | return current_app.extensions['migrate'].db.get_engine(bind=bind_key) 26 | except (TypeError, AttributeError): 27 | # this works with Flask-SQLAlchemy>=3 28 | return current_app.extensions['migrate'].db.engines.get(bind_key) 29 | 30 | 31 | def get_engine_url(bind_key=None): 32 | try: 33 | return get_engine(bind_key).url.render_as_string( 34 | hide_password=False).replace('%', '%%') 35 | except AttributeError: 36 | return str(get_engine(bind_key).url).replace('%', '%%') 37 | 38 | 39 | # add your model's MetaData object here 40 | # for 'autogenerate' support 41 | # from myapp import mymodel 42 | # target_metadata = mymodel.Base.metadata 43 | config.set_main_option('sqlalchemy.url', get_engine_url()) 44 | bind_names = [] 45 | if current_app.config.get('SQLALCHEMY_BINDS') is not None: 46 | bind_names = list(current_app.config['SQLALCHEMY_BINDS'].keys()) 47 | else: 48 | get_bind_names = getattr(current_app.extensions['migrate'].db, 49 | 'bind_names', None) 50 | if get_bind_names: 51 | bind_names = get_bind_names() 52 | for bind in bind_names: 53 | context.config.set_section_option( 54 | bind, "sqlalchemy.url", get_engine_url(bind_key=bind)) 55 | target_db = current_app.extensions['migrate'].db 56 | 57 | 58 | # other values from the config, defined by the needs of env.py, 59 | # can be acquired: 60 | # my_important_option = config.get_main_option("my_important_option") 61 | # ... etc. 62 | 63 | 64 | def get_metadata(bind): 65 | """Return the metadata for a bind.""" 66 | if bind == '': 67 | bind = None 68 | if hasattr(target_db, 'metadatas'): 69 | return target_db.metadatas[bind] 70 | 71 | # legacy, less flexible implementation 72 | m = MetaData() 73 | for t in target_db.metadata.tables.values(): 74 | if t.info.get('bind_key') == bind: 75 | t.tometadata(m) 76 | return m 77 | 78 | 79 | def run_migrations_offline(): 80 | """Run migrations in 'offline' mode. 81 | 82 | This configures the context with just a URL 83 | and not an Engine, though an Engine is acceptable 84 | here as well. By skipping the Engine creation 85 | we don't even need a DBAPI to be available. 86 | 87 | Calls to context.execute() here emit the given string to the 88 | script output. 89 | 90 | """ 91 | # for the --sql use case, run migrations for each URL into 92 | # individual files. 93 | 94 | engines = { 95 | '': { 96 | 'url': context.config.get_main_option('sqlalchemy.url') 97 | } 98 | } 99 | for name in bind_names: 100 | engines[name] = rec = {} 101 | rec['url'] = context.config.get_section_option(name, "sqlalchemy.url") 102 | 103 | for name, rec in engines.items(): 104 | logger.info("Migrating database %s" % (name or '')) 105 | file_ = "%s.sql" % name 106 | logger.info("Writing output to %s" % file_) 107 | with open(file_, 'w') as buffer: 108 | context.configure( 109 | url=rec['url'], 110 | output_buffer=buffer, 111 | target_metadata=get_metadata(name), 112 | literal_binds=True, 113 | ) 114 | with context.begin_transaction(): 115 | context.run_migrations(engine_name=name) 116 | 117 | 118 | def do_run_migrations(_, engines): 119 | # this callback is used to prevent an auto-migration from being generated 120 | # when there are no changes to the schema 121 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 122 | def process_revision_directives(context, revision, directives): 123 | if getattr(config.cmd_opts, 'autogenerate', False): 124 | script = directives[0] 125 | if len(script.upgrade_ops_list) >= len(bind_names) + 1: 126 | empty = True 127 | for upgrade_ops in script.upgrade_ops_list: 128 | if not upgrade_ops.is_empty(): 129 | empty = False 130 | if empty: 131 | directives[:] = [] 132 | logger.info('No changes in schema detected.') 133 | 134 | conf_args = current_app.extensions['migrate'].configure_args 135 | if conf_args.get("process_revision_directives") is None: 136 | conf_args["process_revision_directives"] = process_revision_directives 137 | 138 | for name, rec in engines.items(): 139 | rec['sync_connection'] = conn = rec['connection']._sync_connection() 140 | if USE_TWOPHASE: 141 | rec['transaction'] = conn.begin_twophase() 142 | else: 143 | rec['transaction'] = conn.begin() 144 | 145 | try: 146 | for name, rec in engines.items(): 147 | logger.info("Migrating database %s" % (name or '')) 148 | context.configure( 149 | connection=rec['sync_connection'], 150 | upgrade_token="%s_upgrades" % name, 151 | downgrade_token="%s_downgrades" % name, 152 | target_metadata=get_metadata(name), 153 | **conf_args 154 | ) 155 | context.run_migrations(engine_name=name) 156 | 157 | if USE_TWOPHASE: 158 | for rec in engines.values(): 159 | rec['transaction'].prepare() 160 | 161 | for rec in engines.values(): 162 | rec['transaction'].commit() 163 | except: # noqa: E722 164 | for rec in engines.values(): 165 | rec['transaction'].rollback() 166 | raise 167 | finally: 168 | for rec in engines.values(): 169 | rec['sync_connection'].close() 170 | 171 | 172 | async def run_migrations_online(): 173 | """Run migrations in 'online' mode. 174 | 175 | In this scenario we need to create an Engine 176 | and associate a connection with the context. 177 | 178 | """ 179 | 180 | # for the direct-to-DB use case, start a transaction on all 181 | # engines, then run all migrations, then commit all transactions. 182 | engines = { 183 | '': {'engine': get_engine()} 184 | } 185 | for name in bind_names: 186 | engines[name] = rec = {} 187 | rec['engine'] = get_engine(bind_key=name) 188 | 189 | for name, rec in engines.items(): 190 | engine = rec['engine'] 191 | rec['connection'] = await engine.connect().start() 192 | 193 | await engines['']['connection'].run_sync(do_run_migrations, engines) 194 | 195 | for rec in engines.values(): 196 | await rec['connection'].close() 197 | 198 | 199 | if context.is_offline_mode(): 200 | run_migrations_offline() 201 | else: 202 | asyncio.get_event_loop().run_until_complete(run_migrations_online()) 203 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/aioflask-multidb/script.py.mako: -------------------------------------------------------------------------------- 1 | <%! 2 | import re 3 | 4 | %>"""${message} 5 | 6 | Revision ID: ${up_revision} 7 | Revises: ${down_revision | comma,n} 8 | Create Date: ${create_date} 9 | 10 | """ 11 | from alembic import op 12 | import sqlalchemy as sa 13 | ${imports if imports else ""} 14 | 15 | # revision identifiers, used by Alembic. 16 | revision = ${repr(up_revision)} 17 | down_revision = ${repr(down_revision)} 18 | branch_labels = ${repr(branch_labels)} 19 | depends_on = ${repr(depends_on)} 20 | 21 | 22 | def upgrade(engine_name): 23 | globals()["upgrade_%s" % engine_name]() 24 | 25 | 26 | def downgrade(engine_name): 27 | globals()["downgrade_%s" % engine_name]() 28 | 29 | <% 30 | from flask import current_app 31 | bind_names = [] 32 | if current_app.config.get('SQLALCHEMY_BINDS') is not None: 33 | bind_names = list(current_app.config['SQLALCHEMY_BINDS'].keys()) 34 | else: 35 | get_bind_names = getattr(current_app.extensions['migrate'].db, 'bind_names', None) 36 | if get_bind_names: 37 | bind_names = get_bind_names() 38 | db_names = [''] + bind_names 39 | %> 40 | 41 | ## generate an "upgrade_() / downgrade_()" function 42 | ## for each database name in the ini file. 43 | 44 | % for db_name in db_names: 45 | 46 | def upgrade_${db_name}(): 47 | ${context.get("%s_upgrades" % db_name, "pass")} 48 | 49 | 50 | def downgrade_${db_name}(): 51 | ${context.get("%s_downgrades" % db_name, "pass")} 52 | 53 | % endfor 54 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/aioflask/README: -------------------------------------------------------------------------------- 1 | Single-database configuration for aioflask. 2 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/aioflask/alembic.ini.mako: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,flask_migrate 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/aioflask/env.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from logging.config import fileConfig 4 | 5 | from flask import current_app 6 | 7 | from alembic import context 8 | 9 | # this is the Alembic Config object, which provides 10 | # access to the values within the .ini file in use. 11 | config = context.config 12 | 13 | # Interpret the config file for Python logging. 14 | # This line sets up loggers basically. 15 | fileConfig(config.config_file_name) 16 | logger = logging.getLogger('alembic.env') 17 | 18 | 19 | def get_engine(): 20 | try: 21 | # this works with Flask-SQLAlchemy<3 and Alchemical 22 | return current_app.extensions['migrate'].db.get_engine() 23 | except (TypeError, AttributeError): 24 | # this works with Flask-SQLAlchemy>=3 25 | return current_app.extensions['migrate'].db.engine 26 | 27 | 28 | def get_engine_url(): 29 | try: 30 | return get_engine().url.render_as_string(hide_password=False).replace( 31 | '%', '%%') 32 | except AttributeError: 33 | return str(get_engine().url).replace('%', '%%') 34 | 35 | 36 | # add your model's MetaData object here 37 | # for 'autogenerate' support 38 | # from myapp import mymodel 39 | # target_metadata = mymodel.Base.metadata 40 | config.set_main_option('sqlalchemy.url', get_engine_url()) 41 | target_db = current_app.extensions['migrate'].db 42 | 43 | # other values from the config, defined by the needs of env.py, 44 | # can be acquired: 45 | # my_important_option = config.get_main_option("my_important_option") 46 | # ... etc. 47 | 48 | 49 | def get_metadata(): 50 | if hasattr(target_db, 'metadatas'): 51 | return target_db.metadatas[None] 52 | return target_db.metadata 53 | 54 | 55 | def run_migrations_offline(): 56 | """Run migrations in 'offline' mode. 57 | 58 | This configures the context with just a URL 59 | and not an Engine, though an Engine is acceptable 60 | here as well. By skipping the Engine creation 61 | we don't even need a DBAPI to be available. 62 | 63 | Calls to context.execute() here emit the given string to the 64 | script output. 65 | 66 | """ 67 | url = config.get_main_option("sqlalchemy.url") 68 | context.configure( 69 | url=url, target_metadata=get_metadata(), literal_binds=True 70 | ) 71 | 72 | with context.begin_transaction(): 73 | context.run_migrations() 74 | 75 | 76 | def do_run_migrations(connection): 77 | # this callback is used to prevent an auto-migration from being generated 78 | # when there are no changes to the schema 79 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 80 | def process_revision_directives(context, revision, directives): 81 | if getattr(config.cmd_opts, 'autogenerate', False): 82 | script = directives[0] 83 | if script.upgrade_ops.is_empty(): 84 | directives[:] = [] 85 | logger.info('No changes in schema detected.') 86 | 87 | conf_args = current_app.extensions['migrate'].configure_args 88 | if conf_args.get("process_revision_directives") is None: 89 | conf_args["process_revision_directives"] = process_revision_directives 90 | 91 | context.configure( 92 | connection=connection, 93 | target_metadata=get_metadata(), 94 | **conf_args 95 | ) 96 | 97 | with context.begin_transaction(): 98 | context.run_migrations() 99 | 100 | 101 | async def run_migrations_online(): 102 | """Run migrations in 'online' mode. 103 | 104 | In this scenario we need to create an Engine 105 | and associate a connection with the context. 106 | 107 | """ 108 | 109 | connectable = get_engine() 110 | 111 | async with connectable.connect() as connection: 112 | await connection.run_sync(do_run_migrations) 113 | 114 | 115 | if context.is_offline_mode(): 116 | run_migrations_offline() 117 | else: 118 | asyncio.get_event_loop().run_until_complete(run_migrations_online()) 119 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/aioflask/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 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/flask-multidb/README: -------------------------------------------------------------------------------- 1 | Multi-database configuration for Flask. 2 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/flask-multidb/alembic.ini.mako: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,flask_migrate 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/flask-multidb/env.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.config import fileConfig 3 | 4 | from sqlalchemy import MetaData 5 | from flask import current_app 6 | 7 | from alembic import context 8 | 9 | USE_TWOPHASE = False 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | 21 | def get_engine(bind_key=None): 22 | try: 23 | # this works with Flask-SQLAlchemy<3 and Alchemical 24 | return current_app.extensions['migrate'].db.get_engine(bind=bind_key) 25 | except (TypeError, AttributeError): 26 | # this works with Flask-SQLAlchemy>=3 27 | return current_app.extensions['migrate'].db.engines.get(bind_key) 28 | 29 | 30 | def get_engine_url(bind_key=None): 31 | try: 32 | return get_engine(bind_key).url.render_as_string( 33 | hide_password=False).replace('%', '%%') 34 | except AttributeError: 35 | return str(get_engine(bind_key).url).replace('%', '%%') 36 | 37 | 38 | # add your model's MetaData object here 39 | # for 'autogenerate' support 40 | # from myapp import mymodel 41 | # target_metadata = mymodel.Base.metadata 42 | config.set_main_option('sqlalchemy.url', get_engine_url()) 43 | bind_names = [] 44 | if current_app.config.get('SQLALCHEMY_BINDS') is not None: 45 | bind_names = list(current_app.config['SQLALCHEMY_BINDS'].keys()) 46 | else: 47 | get_bind_names = getattr(current_app.extensions['migrate'].db, 48 | 'bind_names', None) 49 | if get_bind_names: 50 | bind_names = get_bind_names() 51 | for bind in bind_names: 52 | context.config.set_section_option( 53 | bind, "sqlalchemy.url", get_engine_url(bind_key=bind)) 54 | target_db = current_app.extensions['migrate'].db 55 | 56 | # other values from the config, defined by the needs of env.py, 57 | # can be acquired: 58 | # my_important_option = config.get_main_option("my_important_option") 59 | # ... etc. 60 | 61 | 62 | def get_metadata(bind): 63 | """Return the metadata for a bind.""" 64 | if bind == '': 65 | bind = None 66 | if hasattr(target_db, 'metadatas'): 67 | return target_db.metadatas[bind] 68 | 69 | # legacy, less flexible implementation 70 | m = MetaData() 71 | for t in target_db.metadata.tables.values(): 72 | if t.info.get('bind_key') == bind: 73 | t.tometadata(m) 74 | return m 75 | 76 | 77 | def run_migrations_offline(): 78 | """Run migrations in 'offline' mode. 79 | 80 | This configures the context with just a URL 81 | and not an Engine, though an Engine is acceptable 82 | here as well. By skipping the Engine creation 83 | we don't even need a DBAPI to be available. 84 | 85 | Calls to context.execute() here emit the given string to the 86 | script output. 87 | 88 | """ 89 | # for the --sql use case, run migrations for each URL into 90 | # individual files. 91 | 92 | engines = { 93 | '': { 94 | 'url': context.config.get_main_option('sqlalchemy.url') 95 | } 96 | } 97 | for name in bind_names: 98 | engines[name] = rec = {} 99 | rec['url'] = context.config.get_section_option(name, "sqlalchemy.url") 100 | 101 | for name, rec in engines.items(): 102 | logger.info("Migrating database %s" % (name or '')) 103 | file_ = "%s.sql" % name 104 | logger.info("Writing output to %s" % file_) 105 | with open(file_, 'w') as buffer: 106 | context.configure( 107 | url=rec['url'], 108 | output_buffer=buffer, 109 | target_metadata=get_metadata(name), 110 | literal_binds=True, 111 | ) 112 | with context.begin_transaction(): 113 | context.run_migrations(engine_name=name) 114 | 115 | 116 | def run_migrations_online(): 117 | """Run migrations in 'online' mode. 118 | 119 | In this scenario we need to create an Engine 120 | and associate a connection with the context. 121 | 122 | """ 123 | 124 | # this callback is used to prevent an auto-migration from being generated 125 | # when there are no changes to the schema 126 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 127 | def process_revision_directives(context, revision, directives): 128 | if getattr(config.cmd_opts, 'autogenerate', False): 129 | script = directives[0] 130 | if len(script.upgrade_ops_list) >= len(bind_names) + 1: 131 | empty = True 132 | for upgrade_ops in script.upgrade_ops_list: 133 | if not upgrade_ops.is_empty(): 134 | empty = False 135 | if empty: 136 | directives[:] = [] 137 | logger.info('No changes in schema detected.') 138 | 139 | conf_args = current_app.extensions['migrate'].configure_args 140 | if conf_args.get("process_revision_directives") is None: 141 | conf_args["process_revision_directives"] = process_revision_directives 142 | 143 | # for the direct-to-DB use case, start a transaction on all 144 | # engines, then run all migrations, then commit all transactions. 145 | engines = { 146 | '': {'engine': get_engine()} 147 | } 148 | for name in bind_names: 149 | engines[name] = rec = {} 150 | rec['engine'] = get_engine(bind_key=name) 151 | 152 | for name, rec in engines.items(): 153 | engine = rec['engine'] 154 | rec['connection'] = conn = engine.connect() 155 | 156 | if USE_TWOPHASE: 157 | rec['transaction'] = conn.begin_twophase() 158 | else: 159 | rec['transaction'] = conn.begin() 160 | 161 | try: 162 | for name, rec in engines.items(): 163 | logger.info("Migrating database %s" % (name or '')) 164 | context.configure( 165 | connection=rec['connection'], 166 | upgrade_token="%s_upgrades" % name, 167 | downgrade_token="%s_downgrades" % name, 168 | target_metadata=get_metadata(name), 169 | **conf_args 170 | ) 171 | context.run_migrations(engine_name=name) 172 | 173 | if USE_TWOPHASE: 174 | for rec in engines.values(): 175 | rec['transaction'].prepare() 176 | 177 | for rec in engines.values(): 178 | rec['transaction'].commit() 179 | except: # noqa: E722 180 | for rec in engines.values(): 181 | rec['transaction'].rollback() 182 | raise 183 | finally: 184 | for rec in engines.values(): 185 | rec['connection'].close() 186 | 187 | 188 | if context.is_offline_mode(): 189 | run_migrations_offline() 190 | else: 191 | run_migrations_online() 192 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/flask-multidb/script.py.mako: -------------------------------------------------------------------------------- 1 | <%! 2 | import re 3 | 4 | %>"""${message} 5 | 6 | Revision ID: ${up_revision} 7 | Revises: ${down_revision | comma,n} 8 | Create Date: ${create_date} 9 | 10 | """ 11 | from alembic import op 12 | import sqlalchemy as sa 13 | ${imports if imports else ""} 14 | 15 | # revision identifiers, used by Alembic. 16 | revision = ${repr(up_revision)} 17 | down_revision = ${repr(down_revision)} 18 | branch_labels = ${repr(branch_labels)} 19 | depends_on = ${repr(depends_on)} 20 | 21 | 22 | def upgrade(engine_name): 23 | globals()["upgrade_%s" % engine_name]() 24 | 25 | 26 | def downgrade(engine_name): 27 | globals()["downgrade_%s" % engine_name]() 28 | 29 | <% 30 | from flask import current_app 31 | bind_names = [] 32 | if current_app.config.get('SQLALCHEMY_BINDS') is not None: 33 | bind_names = list(current_app.config['SQLALCHEMY_BINDS'].keys()) 34 | else: 35 | get_bind_names = getattr(current_app.extensions['migrate'].db, 'bind_names', None) 36 | if get_bind_names: 37 | bind_names = get_bind_names() 38 | db_names = [''] + bind_names 39 | %> 40 | 41 | ## generate an "upgrade_() / downgrade_()" function 42 | ## for each database name in the ini file. 43 | 44 | % for db_name in db_names: 45 | 46 | def upgrade_${db_name}(): 47 | ${context.get("%s_upgrades" % db_name, "pass")} 48 | 49 | 50 | def downgrade_${db_name}(): 51 | ${context.get("%s_downgrades" % db_name, "pass")} 52 | 53 | % endfor 54 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/flask/README: -------------------------------------------------------------------------------- 1 | Single-database configuration for Flask. 2 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/flask/alembic.ini.mako: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,flask_migrate 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/flask/env.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.config import fileConfig 3 | 4 | from flask import current_app 5 | 6 | from alembic import context 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | fileConfig(config.config_file_name) 15 | logger = logging.getLogger('alembic.env') 16 | 17 | 18 | def get_engine(): 19 | try: 20 | # this works with Flask-SQLAlchemy<3 and Alchemical 21 | return current_app.extensions['migrate'].db.get_engine() 22 | except (TypeError, AttributeError): 23 | # this works with Flask-SQLAlchemy>=3 24 | return current_app.extensions['migrate'].db.engine 25 | 26 | 27 | def get_engine_url(): 28 | try: 29 | return get_engine().url.render_as_string(hide_password=False).replace( 30 | '%', '%%') 31 | except AttributeError: 32 | return str(get_engine().url).replace('%', '%%') 33 | 34 | 35 | # add your model's MetaData object here 36 | # for 'autogenerate' support 37 | # from myapp import mymodel 38 | # target_metadata = mymodel.Base.metadata 39 | config.set_main_option('sqlalchemy.url', get_engine_url()) 40 | target_db = current_app.extensions['migrate'].db 41 | 42 | # other values from the config, defined by the needs of env.py, 43 | # can be acquired: 44 | # my_important_option = config.get_main_option("my_important_option") 45 | # ... etc. 46 | 47 | 48 | def get_metadata(): 49 | if hasattr(target_db, 'metadatas'): 50 | return target_db.metadatas[None] 51 | return target_db.metadata 52 | 53 | 54 | def run_migrations_offline(): 55 | """Run migrations in 'offline' mode. 56 | 57 | This configures the context with just a URL 58 | and not an Engine, though an Engine is acceptable 59 | here as well. By skipping the Engine creation 60 | we don't even need a DBAPI to be available. 61 | 62 | Calls to context.execute() here emit the given string to the 63 | script output. 64 | 65 | """ 66 | url = config.get_main_option("sqlalchemy.url") 67 | context.configure( 68 | url=url, target_metadata=get_metadata(), literal_binds=True 69 | ) 70 | 71 | with context.begin_transaction(): 72 | context.run_migrations() 73 | 74 | 75 | def run_migrations_online(): 76 | """Run migrations in 'online' mode. 77 | 78 | In this scenario we need to create an Engine 79 | and associate a connection with the context. 80 | 81 | """ 82 | 83 | # this callback is used to prevent an auto-migration from being generated 84 | # when there are no changes to the schema 85 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 86 | def process_revision_directives(context, revision, directives): 87 | if getattr(config.cmd_opts, 'autogenerate', False): 88 | script = directives[0] 89 | if script.upgrade_ops.is_empty(): 90 | directives[:] = [] 91 | logger.info('No changes in schema detected.') 92 | 93 | conf_args = current_app.extensions['migrate'].configure_args 94 | if conf_args.get("process_revision_directives") is None: 95 | conf_args["process_revision_directives"] = process_revision_directives 96 | 97 | connectable = get_engine() 98 | 99 | with connectable.connect() as connection: 100 | context.configure( 101 | connection=connection, 102 | target_metadata=get_metadata(), 103 | **conf_args 104 | ) 105 | 106 | with context.begin_transaction(): 107 | context.run_migrations() 108 | 109 | 110 | if context.is_offline_mode(): 111 | run_migrations_offline() 112 | else: 113 | run_migrations_online() 114 | -------------------------------------------------------------------------------- /src/flask_migrate/templates/flask/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/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelgrinberg/Flask-Migrate/0878d2354c700f03732e5f30f35f01734ba5569e/tests/__init__.py -------------------------------------------------------------------------------- /tests/app.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | import os 3 | from flask import Flask 4 | from flask_sqlalchemy import SQLAlchemy 5 | from flask_migrate import Migrate 6 | 7 | basedir = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | app = Flask(__name__) 10 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join( 11 | basedir, 'app.db') 12 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 13 | 14 | db = SQLAlchemy(app) 15 | migrate = Migrate(app, db, compare_type=False) 16 | 17 | 18 | class User(db.Model): 19 | id = db.Column(db.Integer, primary_key=True) 20 | name = db.Column(db.String(256)) 21 | 22 | 23 | @app.cli.command() 24 | def add(): 25 | """Add test user.""" 26 | db.session.add(User(name='test')) 27 | db.session.commit() 28 | 29 | 30 | if __name__ == '__main__': 31 | app.run() 32 | -------------------------------------------------------------------------------- /tests/app_compare_type1.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_migrate import Migrate 5 | 6 | basedir = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | app = Flask(__name__) 9 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join( 10 | basedir, 'app.db') 11 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 12 | 13 | db = SQLAlchemy(app) 14 | migrate = Migrate(app, db, command='database') 15 | 16 | 17 | class User(db.Model): 18 | id = db.Column(db.Integer, primary_key=True) 19 | name = db.Column(db.String(128)) 20 | 21 | 22 | if __name__ == '__main__': 23 | app.run() 24 | -------------------------------------------------------------------------------- /tests/app_compare_type2.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_migrate import Migrate 5 | 6 | basedir = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | app = Flask(__name__) 9 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join( 10 | basedir, 'app.db') 11 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 12 | 13 | db = SQLAlchemy(app) 14 | migrate = Migrate(app, db, command='database') 15 | 16 | 17 | class User(db.Model): 18 | id = db.Column(db.Integer, primary_key=True) 19 | name = db.Column(db.String(10)) 20 | 21 | 22 | if __name__ == '__main__': 23 | app.run() 24 | -------------------------------------------------------------------------------- /tests/app_custom_directory.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_migrate import Migrate 5 | 6 | basedir = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | app = Flask(__name__) 9 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join( 10 | basedir, 'app.db') 11 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 12 | 13 | db = SQLAlchemy(app) 14 | migrate = Migrate(app, db, directory='temp_folder/temp_migrations') 15 | 16 | 17 | class User(db.Model): 18 | id = db.Column(db.Integer, primary_key=True) 19 | name = db.Column(db.String(128)) 20 | 21 | 22 | @app.cli.command() 23 | def add(): 24 | """Add test user.""" 25 | db.session.add(User(name='test')) 26 | db.session.commit() 27 | 28 | 29 | if __name__ == '__main__': 30 | app.run() 31 | -------------------------------------------------------------------------------- /tests/app_custom_directory_path.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_migrate import Migrate 5 | from pathlib import Path 6 | 7 | basedir = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | app = Flask(__name__) 10 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join( 11 | basedir, 'app.db') 12 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 13 | 14 | db = SQLAlchemy(app) 15 | migrate = Migrate(app, db, directory=Path('temp_folder/temp_migrations')) 16 | 17 | 18 | class User(db.Model): 19 | id = db.Column(db.Integer, primary_key=True) 20 | name = db.Column(db.String(128)) 21 | 22 | 23 | @app.cli.command() 24 | def add(): 25 | """Add test user.""" 26 | db.session.add(User(name='test')) 27 | db.session.commit() 28 | 29 | 30 | if __name__ == '__main__': 31 | app.run() 32 | -------------------------------------------------------------------------------- /tests/app_multidb.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | import os 3 | from flask import Flask 4 | from flask_sqlalchemy import SQLAlchemy 5 | from flask_migrate import Migrate 6 | 7 | basedir = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | app = Flask(__name__) 10 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join( 11 | basedir, 'app1.db') 12 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False 13 | app.config['SQLALCHEMY_BINDS'] = { 14 | "db1": "sqlite:///" + os.path.join(basedir, "app2.db"), 15 | } 16 | 17 | db = SQLAlchemy(app) 18 | 19 | 20 | class User(db.Model): 21 | id = db.Column(db.Integer, primary_key=True) 22 | name = db.Column(db.String(128)) 23 | 24 | 25 | class Group(db.Model): 26 | __bind_key__ = 'db1' 27 | id = db.Column(db.Integer, primary_key=True) 28 | name = db.Column(db.String(128)) 29 | 30 | 31 | migrate = Migrate(app, db) 32 | 33 | 34 | @app.cli.command() 35 | def add(): 36 | """Add test users.""" 37 | db.session.add(User(name='test')) 38 | db.session.add(Group(name='group')) 39 | db.session.commit() 40 | 41 | 42 | if __name__ == '__main__': 43 | app.run() 44 | -------------------------------------------------------------------------------- /tests/custom_template/README: -------------------------------------------------------------------------------- 1 | Custom template. 2 | -------------------------------------------------------------------------------- /tests/custom_template/alembic.ini.mako: -------------------------------------------------------------------------------- 1 | # Custom template 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,flask_migrate 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /tests/custom_template/env.py: -------------------------------------------------------------------------------- 1 | # Custom template 2 | import logging 3 | from logging.config import fileConfig 4 | 5 | from flask import current_app 6 | 7 | from alembic import context 8 | 9 | # this is the Alembic Config object, which provides 10 | # access to the values within the .ini file in use. 11 | config = context.config 12 | 13 | # Interpret the config file for Python logging. 14 | # This line sets up loggers basically. 15 | fileConfig(config.config_file_name) 16 | logger = logging.getLogger('alembic.env') 17 | 18 | # add your model's MetaData object here 19 | # for 'autogenerate' support 20 | # from myapp import mymodel 21 | # target_metadata = mymodel.Base.metadata 22 | config.set_main_option( 23 | 'sqlalchemy.url', 24 | str(current_app.extensions['migrate'].db.get_engine().url).replace( 25 | '%', '%%')) 26 | target_metadata = current_app.extensions['migrate'].db.metadata 27 | 28 | # other values from the config, defined by the needs of env.py, 29 | # can be acquired: 30 | # my_important_option = config.get_main_option("my_important_option") 31 | # ... etc. 32 | 33 | 34 | def run_migrations_offline(): 35 | """Run migrations in 'offline' mode. 36 | 37 | This configures the context with just a URL 38 | and not an Engine, though an Engine is acceptable 39 | here as well. By skipping the Engine creation 40 | we don't even need a DBAPI to be available. 41 | 42 | Calls to context.execute() here emit the given string to the 43 | script output. 44 | 45 | """ 46 | url = config.get_main_option("sqlalchemy.url") 47 | context.configure( 48 | url=url, target_metadata=target_metadata, literal_binds=True 49 | ) 50 | 51 | with context.begin_transaction(): 52 | context.run_migrations() 53 | 54 | 55 | def run_migrations_online(): 56 | """Run migrations in 'online' mode. 57 | 58 | In this scenario we need to create an Engine 59 | and associate a connection with the context. 60 | 61 | """ 62 | 63 | # this callback is used to prevent an auto-migration from being generated 64 | # when there are no changes to the schema 65 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 66 | def process_revision_directives(context, revision, directives): 67 | if getattr(config.cmd_opts, 'autogenerate', False): 68 | script = directives[0] 69 | if script.upgrade_ops.is_empty(): 70 | directives[:] = [] 71 | logger.info('No changes in schema detected.') 72 | 73 | conf_args = current_app.extensions['migrate'].configure_args 74 | if conf_args.get("process_revision_directives") is None: 75 | conf_args["process_revision_directives"] = process_revision_directives 76 | 77 | connectable = current_app.extensions['migrate'].db.get_engine() 78 | 79 | with connectable.connect() as connection: 80 | context.configure( 81 | connection=connection, 82 | target_metadata=target_metadata, 83 | **conf_args 84 | ) 85 | 86 | with context.begin_transaction(): 87 | context.run_migrations() 88 | 89 | 90 | if context.is_offline_mode(): 91 | run_migrations_offline() 92 | else: 93 | run_migrations_online() 94 | -------------------------------------------------------------------------------- /tests/custom_template/script.py.mako: -------------------------------------------------------------------------------- 1 | # Custom template 2 | """${message} 3 | 4 | Revision ID: ${up_revision} 5 | Revises: ${down_revision | comma,n} 6 | Create Date: ${create_date} 7 | 8 | """ 9 | from alembic import op 10 | import sqlalchemy as sa 11 | ${imports if imports else ""} 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = ${repr(up_revision)} 15 | down_revision = ${repr(down_revision)} 16 | branch_labels = ${repr(branch_labels)} 17 | depends_on = ${repr(depends_on)} 18 | 19 | 20 | def upgrade(): 21 | ${upgrades if upgrades else "pass"} 22 | 23 | 24 | def downgrade(): 25 | ${downgrades if downgrades else "pass"} 26 | -------------------------------------------------------------------------------- /tests/test_custom_template.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import unittest 4 | import subprocess 5 | import shlex 6 | 7 | 8 | def run_cmd(app, cmd): 9 | """Run a command and return a tuple with (stdout, stderr, exit_code)""" 10 | os.environ['FLASK_APP'] = app 11 | process = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, 12 | stderr=subprocess.PIPE) 13 | (stdout, stderr) = process.communicate() 14 | print('\n$ ' + cmd) 15 | print(stdout.decode('utf-8')) 16 | print(stderr.decode('utf-8')) 17 | return stdout, stderr, process.wait() 18 | 19 | 20 | class TestMigrate(unittest.TestCase): 21 | def setUp(self): 22 | os.chdir(os.path.split(os.path.abspath(__file__))[0]) 23 | try: 24 | os.remove('app.db') 25 | except OSError: 26 | pass 27 | try: 28 | shutil.rmtree('migrations') 29 | except OSError: 30 | pass 31 | try: 32 | shutil.rmtree('temp_folder') 33 | except OSError: 34 | pass 35 | 36 | def tearDown(self): 37 | try: 38 | os.remove('app.db') 39 | except OSError: 40 | pass 41 | try: 42 | shutil.rmtree('migrations') 43 | except OSError: 44 | pass 45 | try: 46 | shutil.rmtree('temp_folder') 47 | except OSError: 48 | pass 49 | 50 | def test_alembic_version(self): 51 | from flask_migrate import alembic_version 52 | self.assertEqual(len(alembic_version), 3) 53 | for v in alembic_version: 54 | self.assertTrue(isinstance(v, int)) 55 | 56 | def test_migrate_upgrade(self): 57 | (o, e, s) = run_cmd('app.py', 'flask db init -t ./custom_template') 58 | self.assertTrue(s == 0) 59 | (o, e, s) = run_cmd('app.py', 'flask db migrate') 60 | self.assertTrue(s == 0) 61 | (o, e, s) = run_cmd('app.py', 'flask db upgrade') 62 | self.assertTrue(s == 0) 63 | 64 | from .app import app, db, User 65 | with app.app_context(): 66 | db.engine.dispose() 67 | db.session.add(User(name='test')) 68 | db.session.commit() 69 | 70 | with open('migrations/README', 'rt') as f: 71 | assert f.readline().strip() == 'Custom template.' 72 | with open('migrations/alembic.ini', 'rt') as f: 73 | assert f.readline().strip() == '# Custom template' 74 | with open('migrations/env.py', 'rt') as f: 75 | assert f.readline().strip() == '# Custom template' 76 | with open('migrations/script.py.mako', 'rt') as f: 77 | assert f.readline().strip() == '# Custom template' 78 | -------------------------------------------------------------------------------- /tests/test_migrate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import unittest 4 | import subprocess 5 | import shlex 6 | 7 | 8 | def run_cmd(app, cmd): 9 | """Run a command and return a tuple with (stdout, stderr, exit_code)""" 10 | os.environ['FLASK_APP'] = app 11 | process = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, 12 | stderr=subprocess.PIPE) 13 | (stdout, stderr) = process.communicate() 14 | print('\n$ ' + cmd) 15 | print(stdout.decode('utf-8')) 16 | print(stderr.decode('utf-8')) 17 | return stdout, stderr, process.wait() 18 | 19 | 20 | class TestMigrate(unittest.TestCase): 21 | def setUp(self): 22 | os.chdir(os.path.split(os.path.abspath(__file__))[0]) 23 | try: 24 | os.remove('app.db') 25 | except OSError: 26 | pass 27 | try: 28 | shutil.rmtree('migrations') 29 | except OSError: 30 | pass 31 | try: 32 | shutil.rmtree('temp_folder') 33 | except OSError: 34 | pass 35 | 36 | def tearDown(self): 37 | try: 38 | os.remove('app.db') 39 | except OSError: 40 | pass 41 | try: 42 | shutil.rmtree('migrations') 43 | except OSError: 44 | pass 45 | try: 46 | shutil.rmtree('temp_folder') 47 | except OSError: 48 | pass 49 | 50 | def atest_alembic_version(self): 51 | from flask_migrate import alembic_version 52 | self.assertEqual(len(alembic_version), 3) 53 | for v in alembic_version: 54 | self.assertTrue(isinstance(v, int)) 55 | 56 | def test_migrate_upgrade(self): 57 | (o, e, s) = run_cmd('app.py', 'flask db init') 58 | self.assertTrue(s == 0) 59 | (o, e, s) = run_cmd('app.py', 'flask db check') 60 | self.assertTrue(s != 0) 61 | (o, e, s) = run_cmd('app.py', 'flask db migrate') 62 | self.assertTrue(s == 0) 63 | (o, e, s) = run_cmd('app.py', 'flask db check') 64 | self.assertTrue(s != 0) 65 | (o, e, s) = run_cmd('app.py', 'flask db upgrade') 66 | self.assertTrue(s == 0) 67 | (o, e, s) = run_cmd('app.py', 'flask db check') 68 | self.assertTrue(s == 0) 69 | 70 | from .app import app, db, User 71 | with app.app_context(): 72 | db.engine.dispose() 73 | db.session.add(User(name='test')) 74 | db.session.commit() 75 | 76 | def test_custom_directory(self): 77 | (o, e, s) = run_cmd('app_custom_directory.py', 'flask db init') 78 | self.assertTrue(s == 0) 79 | (o, e, s) = run_cmd('app_custom_directory.py', 'flask db migrate') 80 | self.assertTrue(s == 0) 81 | (o, e, s) = run_cmd('app_custom_directory.py', 'flask db upgrade') 82 | self.assertTrue(s == 0) 83 | 84 | from .app_custom_directory import app, db, User 85 | with app.app_context(): 86 | db.engine.dispose() 87 | db.session.add(User(name='test')) 88 | db.session.commit() 89 | 90 | def test_custom_directory_path(self): 91 | (o, e, s) = run_cmd('app_custom_directory_path.py', 'flask db init') 92 | self.assertTrue(s == 0) 93 | (o, e, s) = run_cmd('app_custom_directory_path.py', 'flask db migrate') 94 | self.assertTrue(s == 0) 95 | (o, e, s) = run_cmd('app_custom_directory_path.py', 'flask db upgrade') 96 | self.assertTrue(s == 0) 97 | 98 | from .app_custom_directory_path import app, db, User 99 | with app.app_context(): 100 | db.engine.dispose() 101 | db.session.add(User(name='test')) 102 | db.session.commit() 103 | 104 | def test_compare_type(self): 105 | (o, e, s) = run_cmd('app_compare_type1.py', 'flask database init') 106 | self.assertTrue(s == 0) 107 | (o, e, s) = run_cmd('app_compare_type1.py', 'flask database migrate') 108 | self.assertTrue(s == 0) 109 | (o, e, s) = run_cmd('app_compare_type1.py', 'flask database upgrade') 110 | self.assertTrue(s == 0) 111 | (o, e, s) = run_cmd('app_compare_type2.py', 'flask database migrate') 112 | self.assertTrue(s == 0) 113 | self.assertTrue(b'Detected type change from VARCHAR(length=128) ' 114 | b'to String(length=10)' in e) 115 | -------------------------------------------------------------------------------- /tests/test_multidb_migrate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import unittest 4 | import subprocess 5 | import shlex 6 | import sqlite3 7 | 8 | 9 | def run_cmd(app, cmd): 10 | """Run a command and return a tuple with (stdout, stderr, exit_code)""" 11 | os.environ['FLASK_APP'] = app 12 | process = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, 13 | stderr=subprocess.PIPE) 14 | (stdout, stderr) = process.communicate() 15 | print('\n$ ' + cmd) 16 | print(stdout.decode('utf-8')) 17 | print(stderr.decode('utf-8')) 18 | return stdout, stderr, process.wait() 19 | 20 | 21 | class TestMigrate(unittest.TestCase): 22 | def setUp(self): 23 | os.chdir(os.path.split(os.path.abspath(__file__))[0]) 24 | try: 25 | os.remove('app1.db') 26 | os.remove('app2.db') 27 | except OSError: 28 | pass 29 | try: 30 | shutil.rmtree('migrations') 31 | except OSError: 32 | pass 33 | 34 | def tearDown(self): 35 | try: 36 | os.remove('app1.db') 37 | os.remove('app2.db') 38 | except OSError: 39 | pass 40 | try: 41 | shutil.rmtree('migrations') 42 | except OSError: 43 | pass 44 | 45 | def test_multidb_migrate_upgrade(self): 46 | (o, e, s) = run_cmd('app_multidb.py', 'flask db init --multidb') 47 | self.assertTrue(s == 0) 48 | (o, e, s) = run_cmd('app_multidb.py', 'flask db migrate') 49 | self.assertTrue(s == 0) 50 | (o, e, s) = run_cmd('app_multidb.py', 'flask db upgrade') 51 | self.assertTrue(s == 0) 52 | 53 | # ensure the tables are in the correct databases 54 | conn1 = sqlite3.connect('app1.db') 55 | c = conn1.cursor() 56 | c.execute('select name from sqlite_master') 57 | tables = c.fetchall() 58 | conn1.close() 59 | self.assertIn(('alembic_version',), tables) 60 | self.assertIn(('user',), tables) 61 | 62 | conn2 = sqlite3.connect('app2.db') 63 | c = conn2.cursor() 64 | c.execute('select name from sqlite_master') 65 | tables = c.fetchall() 66 | conn2.close() 67 | self.assertIn(('alembic_version',), tables) 68 | self.assertIn(('group',), tables) 69 | 70 | # ensure the databases can be written to 71 | from .app_multidb import app, db, User, Group 72 | with app.app_context(): 73 | db.engine.dispose() 74 | db.session.add(User(name='test')) 75 | db.session.add(Group(name='group')) 76 | db.session.commit() 77 | 78 | # ensure the downgrade works 79 | (o, e, s) = run_cmd('app_multidb.py', 'flask db downgrade') 80 | self.assertTrue(s == 0) 81 | 82 | conn1 = sqlite3.connect('app1.db') 83 | c = conn1.cursor() 84 | c.execute('select name from sqlite_master') 85 | tables = c.fetchall() 86 | conn1.close() 87 | self.assertIn(('alembic_version',), tables) 88 | self.assertNotIn(('user',), tables) 89 | 90 | conn2 = sqlite3.connect('app2.db') 91 | c = conn2.cursor() 92 | c.execute('select name from sqlite_master') 93 | tables = c.fetchall() 94 | conn2.close() 95 | self.assertIn(('alembic_version',), tables) 96 | self.assertNotIn(('group',), tables) 97 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=flake8,py37,py38,py39,py310,py311,pypy3,docs 3 | skip_missing_interpreters=True 4 | 5 | [gh-actions] 6 | python = 7 | 3.7: py37 8 | 3.8: py38 9 | 3.9: py39 10 | 3.10: py310 11 | 3.11: py311 12 | pypy-3: pypy3 13 | 14 | [testenv] 15 | commands= 16 | pip install -e . 17 | pip install {env:SQLALCHEMY_VERSION:sqlalchemy>=2} 18 | pytest -p no:logging 19 | deps= 20 | pytest 21 | 22 | [testenv:flake8] 23 | deps= 24 | flake8 25 | commands= 26 | flake8 --exclude=".*" src/flask_migrate tests 27 | 28 | [testenv:docs] 29 | changedir=docs 30 | deps= 31 | sphinx 32 | allowlist_externals= 33 | make 34 | commands= 35 | make html 36 | --------------------------------------------------------------------------------