├── docs ├── docs │ ├── assets │ │ ├── js │ │ │ ├── extra.js │ │ │ ├── inconceivable.js │ │ │ └── termynal.js │ │ ├── img │ │ │ ├── social.png │ │ │ ├── favicon.svg │ │ │ └── logo.svg │ │ └── css │ │ │ ├── common.css │ │ │ └── highlight.css │ ├── robots.txt │ ├── api │ │ ├── sqlalchemy-wrapper-class.md │ │ ├── testtransaction-class.md │ │ ├── session-class.md │ │ └── alembic-wrapper-class.md │ ├── index.md │ ├── sqlalchemy-wrapper.md │ ├── alembic-wrapper.md │ ├── working-with-the-session.md │ ├── how-to.md │ └── testing-with-a-real-database.md ├── old │ └── 5.x │ │ ├── assets │ │ ├── js │ │ │ ├── extra.js │ │ │ ├── inconceivable.js │ │ │ └── termynal.js │ │ ├── img │ │ │ ├── social.png │ │ │ ├── favicon.svg │ │ │ └── logo.svg │ │ ├── images │ │ │ └── favicon.png │ │ ├── javascripts │ │ │ └── lunr │ │ │ │ ├── min │ │ │ │ ├── lunr.multi.min.js │ │ │ │ └── lunr.stemmer.support.min.js │ │ │ │ └── tinyseg.js │ │ ├── css │ │ │ ├── common.css │ │ │ └── highlight.css │ │ └── stylesheets │ │ │ └── palette.3f5d1f46.min.css │ │ ├── sitemap.xml.gz │ │ ├── sitemap.xml │ │ └── 404.html ├── overrides │ ├── main.html │ ├── home.html │ └── partials │ │ └── header.html ├── deploy.sh └── mkdocs.yml ├── src └── sqla_wrapper │ ├── py.typed │ ├── cli │ ├── __init__.py │ ├── proper_cli_cli.py │ └── click_cli.py │ ├── __init__.py │ ├── base_model.py │ ├── session.py │ ├── sqlalchemy_wrapper.py │ └── alembic_wrapper.py ├── header.png ├── README.md ├── .pre-commit-config.yaml ├── .gitignore ├── tests ├── test_base_model.py ├── test_testing.py ├── test_session.py ├── conftest.py ├── test_sqlalchemy_wrapper.py └── test_alembic_wrapper.py ├── Makefile ├── .flake8 ├── .github └── workflows │ ├── upload-to-pypi.yml │ └── run_tests.yml ├── MIT-LICENSE └── pyproject.toml /docs/docs/assets/js/extra.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sqla_wrapper/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/old/5.x/assets/js/extra.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sqla_wrapper/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/docs/assets/js/inconceivable.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/old/5.x/assets/js/inconceivable.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/docs/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: old/* 3 | -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpsca/sqla-wrapper/HEAD/header.png -------------------------------------------------------------------------------- /docs/old/5.x/sitemap.xml.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpsca/sqla-wrapper/HEAD/docs/old/5.x/sitemap.xml.gz -------------------------------------------------------------------------------- /docs/docs/assets/img/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpsca/sqla-wrapper/HEAD/docs/docs/assets/img/social.png -------------------------------------------------------------------------------- /docs/old/5.x/assets/img/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpsca/sqla-wrapper/HEAD/docs/old/5.x/assets/img/social.png -------------------------------------------------------------------------------- /docs/old/5.x/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpsca/sqla-wrapper/HEAD/docs/old/5.x/assets/images/favicon.png -------------------------------------------------------------------------------- /src/sqla_wrapper/__init__.py: -------------------------------------------------------------------------------- 1 | from .alembic_wrapper import * # noqa 2 | from .base_model import * # noqa 3 | from .session import * # noqa 4 | from .sqlalchemy_wrapper import * # noqa 5 | -------------------------------------------------------------------------------- /docs/docs/api/sqlalchemy-wrapper-class.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: SQLAlchemy wrapper class 3 | --- 4 | 5 | # `SQLAlchemy( )` 6 | 7 | ::: sqla_wrapper.SQLAlchemy 8 | options: 9 | heading_level: 2 10 | -------------------------------------------------------------------------------- /docs/docs/api/testtransaction-class.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: TestTransaction class 3 | --- 4 | 5 | # `TestTransaction( )` 6 | 7 | ::: sqla_wrapper.TestTransaction 8 | options: 9 | heading_level: 2 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Migrating to SQLAlchemy 2 requires more effort than migrating to a better ORM. 2 | 3 | Don't use this library, use [Peewee ORM](https://github.com/coleifer/peewee) instead, a much elegant and equally powerful Python ORM. 4 | 5 | 🚀🚀🚀 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: ^(docs)/ 2 | fail_fast: true 3 | repos: 4 | - repo: local 5 | hooks: 6 | - id: flake8 7 | name: flake8 8 | entry: make lint 9 | language: system 10 | types: [python] 11 | pass_filenames: false 12 | always_run: true 13 | -------------------------------------------------------------------------------- /docs/docs/api/session-class.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Session class 3 | --- 4 | 5 | # `Session( )` 6 | 7 | ::: sqla_wrapper.Session 8 | options: 9 | heading_level: 2 10 | members: 11 | - all 12 | - create 13 | - first 14 | - first_or_create 15 | - create_or_first 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | 7 | # Distribution / packaging 8 | .Python 9 | *.egg 10 | *.egg-info/ 11 | *.log 12 | .cache 13 | .eggs/ 14 | .env 15 | .installed.cfg 16 | .mypy_cache/* 17 | .tox 18 | .venv 19 | _build/* 20 | build/ 21 | covreport 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | lib/ 27 | lib64/ 28 | MANIFEST 29 | parts/ 30 | pip-wheel-metadata 31 | sdist/ 32 | docs/site/* 33 | var/ 34 | wheels/ 35 | .coverage 36 | -------------------------------------------------------------------------------- /tests/test_base_model.py: -------------------------------------------------------------------------------- 1 | def test_fill(dbs, TestModelA): 2 | obj = dbs.create(TestModelA, title="Remember") 3 | obj.fill(title="lorem ipsum") 4 | dbs.commit() 5 | 6 | updated = dbs.first(TestModelA) 7 | assert updated.title == "lorem ipsum" 8 | 9 | 10 | def test_repr(dbs, TestModelA): 11 | obj = dbs.create(TestModelA, title="Hello world") 12 | dbs.commit() 13 | 14 | repr = str(obj) 15 | assert f" 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {{ super() }} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Juan-Pablo Scaletti 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /docs/docs/assets/css/common.css: -------------------------------------------------------------------------------- 1 | [data-md-color-scheme="default"] { 2 | --md-code-bg-color: #fff; 3 | } 4 | 5 | body[data-md-color-scheme="default"] { 6 | background-color: #fbf7f0; 7 | } 8 | .md-header { 9 | position: initial; 10 | background: var(--md-primary-fg-color); 11 | background: linear-gradient( 12 | 125deg, 13 | var(--md-primary-fg-color) 0%, 14 | var(--md-accent-fg-color) 99% 15 | ); 16 | } 17 | .md-typeset code { 18 | font-size: 0.95em; 19 | } 20 | .md-nav__title .md-nav__button.md-logo img, 21 | .md-nav__title .md-nav__button.md-logo svg, 22 | .md-header__button.md-logo img, 23 | .md-header__button.md-logo svg { 24 | height: auto !important; 25 | width: 1.7rem !important; 26 | } 27 | .md-footer { 28 | margin-top: 40px; 29 | } 30 | 31 | #outdated-warning { 32 | background-color: #FFBABA; 33 | color: #181d27; 34 | position: relative; 35 | display: block; 36 | width: 100%; 37 | padding: 8px 20px 8px; 38 | font-size: 14px; 39 | text-align: center; 40 | z-index: 100000; 41 | } 42 | .version-chooser select {; 43 | border: 1px solid #444; 44 | padding: 9px; 45 | border-radius: 2px; 46 | background-color: rgba(255, 255, 255, 0.5); 47 | } 48 | -------------------------------------------------------------------------------- /src/sqla_wrapper/base_model.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from sqlalchemy import inspect 4 | 5 | 6 | __all__ = ("BaseModel", ) 7 | 8 | 9 | class BaseModel: 10 | def fill(self, **attrs: t.Any) -> t.Any: 11 | """Fill the object with the values of the attrs dict.""" 12 | for name in attrs: 13 | setattr(self, name, attrs[name]) 14 | return self 15 | 16 | def __repr__(self) -> str: 17 | output = ["<", self.__class__.__name__, f" #{id(self)}"] 18 | for attr in self._iter_attrs(): 19 | output.append(f"\n {self._repr_attr(attr)}") 20 | output.append(">") 21 | return "".join(output) 22 | 23 | def _iter_attrs(self) -> t.Generator: 24 | inspection = inspect(self.__class__) 25 | if inspection is None: 26 | raise StopIteration 27 | names = inspection.columns.keys() 28 | for name in names: 29 | yield (name, getattr(self, name)) 30 | 31 | def _repr_attr(self, attr: t.Any) -> str: 32 | name, value = attr 33 | if hasattr(value, "isoformat"): 34 | value = value.isoformat() 35 | else: 36 | value = repr(value) 37 | return f"{name} = {value}" 38 | -------------------------------------------------------------------------------- /tests/test_testing.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import func, select 2 | 3 | 4 | def test_independence_1(db, dbs, TestModelB): 5 | stmt = select(func.count("*")).select_from(TestModelB) 6 | assert db.s.execute(stmt).scalar() == 1 7 | 8 | db.s.add(TestModelB(title="second")) 9 | db.s.flush() 10 | assert db.s.execute(stmt).scalar() == 2 11 | 12 | 13 | def test_independence_2(db, dbs, TestModelB): 14 | stmt = select(func.count("*")).select_from(TestModelB) 15 | assert db.s.execute(stmt).scalar() == 1 16 | 17 | db.s.add(TestModelB(title="second")) 18 | db.s.flush() 19 | assert db.s.execute(stmt).scalar() == 2 20 | 21 | 22 | def test_independence_3(db, dbs, TestModelB): 23 | stmt = select(func.count("*")).select_from(TestModelB) 24 | assert db.s.execute(stmt).scalar() == 1 25 | 26 | db.s.add(TestModelB(title="second")) 27 | db.s.flush() 28 | assert db.s.execute(stmt).scalar() == 2 29 | 30 | 31 | def test_rollback(db, dbs, TestModelB): 32 | stmt = select(func.count("*")).select_from(TestModelB) 33 | assert db.s.execute(stmt).scalar() == 1 34 | 35 | db.s.add(TestModelB(title="second")) 36 | db.s.flush() 37 | db.s.rollback() 38 | assert db.s.execute(stmt).scalar() == 1 39 | -------------------------------------------------------------------------------- /docs/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdocs build 3 | rm site/assets/javascripts/lunr/min/lunr.ar.min.js 4 | rm site/assets/javascripts/lunr/min/lunr.da.min.js 5 | rm site/assets/javascripts/lunr/min/lunr.de.min.js 6 | rm site/assets/javascripts/lunr/min/lunr.du.min.js 7 | rm site/assets/javascripts/lunr/min/lunr.es.min.js 8 | rm site/assets/javascripts/lunr/min/lunr.fi.min.js 9 | rm site/assets/javascripts/lunr/min/lunr.fr.min.js 10 | rm site/assets/javascripts/lunr/min/lunr.hi.min.js 11 | rm site/assets/javascripts/lunr/min/lunr.hu.min.js 12 | rm site/assets/javascripts/lunr/min/lunr.it.min.js 13 | rm site/assets/javascripts/lunr/min/lunr.ja.min.js 14 | rm site/assets/javascripts/lunr/min/lunr.jp.min.js 15 | rm site/assets/javascripts/lunr/min/lunr.nl.min.js 16 | rm site/assets/javascripts/lunr/min/lunr.no.min.js 17 | rm site/assets/javascripts/lunr/min/lunr.pt.min.js 18 | rm site/assets/javascripts/lunr/min/lunr.ro.min.js 19 | rm site/assets/javascripts/lunr/min/lunr.ru.min.js 20 | rm site/assets/javascripts/lunr/min/lunr.sv.min.js 21 | rm site/assets/javascripts/lunr/min/lunr.th.min.js 22 | rm site/assets/javascripts/lunr/min/lunr.tr.min.js 23 | rm site/assets/javascripts/lunr/min/lunr.vi.min.js 24 | rm site/assets/javascripts/lunr/min/lunr.zh.min.js 25 | cp -r old site/old 26 | rsync --recursive --delete --progress site code:/var/www/sqla-wrapper/ 27 | -------------------------------------------------------------------------------- /docs/old/5.x/assets/css/common.css: -------------------------------------------------------------------------------- 1 | [data-md-color-scheme="default"] { 2 | --md-code-bg-color: #fff; 3 | } 4 | 5 | body[data-md-color-scheme="default"] { 6 | background-color: #fbf7f0; 7 | } 8 | .md-header { 9 | background: var(--md-primary-fg-color); 10 | background: linear-gradient( 11 | 125deg, 12 | var(--md-primary-fg-color) 0%, 13 | var(--md-accent-fg-color) 99% 14 | ); 15 | } 16 | .md-typeset code { 17 | font-size: 0.95em; 18 | } 19 | .md-nav__title .md-nav__button.md-logo img, 20 | .md-nav__title .md-nav__button.md-logo svg, 21 | .md-header__button.md-logo img, 22 | .md-header__button.md-logo svg { 23 | height: auto !important; 24 | } 25 | .md-footer { 26 | margin-top: 40px; 27 | } 28 | 29 | .autodoc-signature { 30 | margin-bottom: 20px; 31 | } 32 | .autodoc-docstring { 33 | padding-left: 20px; 34 | margin-bottom: 30px; 35 | border-left: 5px solid rgba(230, 230, 230); 36 | } 37 | .autodoc-members { 38 | padding-left: 20px; 39 | margin-bottom: 15px; 40 | } 41 | 42 | #outdated-warning { 43 | background-color: #FFBABA; 44 | color: #181d27; 45 | position: relative; 46 | display: block; 47 | width: 100%; 48 | padding: 8px 20px 8px; 49 | font-size: 14px; 50 | text-align: center; 51 | z-index: 100000; 52 | } 53 | .version-chooser select { 54 | padding: 9px 4px; 55 | border-radius: 2px; 56 | background: white; 57 | } 58 | -------------------------------------------------------------------------------- /docs/old/5.x/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://sqla-wrapper.scaletti.dev/ 5 | 2021-11-02 6 | daily 7 | 8 | 9 | https://sqla-wrapper.scaletti.dev/alembic-wrapper/ 10 | 2021-11-02 11 | daily 12 | 13 | 14 | https://sqla-wrapper.scaletti.dev/api/ 15 | 2021-11-02 16 | daily 17 | 18 | 19 | https://sqla-wrapper.scaletti.dev/how-to/ 20 | 2021-11-02 21 | daily 22 | 23 | 24 | https://sqla-wrapper.scaletti.dev/sqlalchemy-wrapper/ 25 | 2021-11-02 26 | daily 27 | 28 | 29 | https://sqla-wrapper.scaletti.dev/testing-with-a-real-database/ 30 | 2021-11-02 31 | daily 32 | 33 | 34 | https://sqla-wrapper.scaletti.dev/working-with-the-session/ 35 | 2021-11-02 36 | daily 37 | 38 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: run_tests 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@v2 15 | - uses: actions/setup-python@v2 16 | - name: "Installs dependencies" 17 | run: | 18 | curl -sSL https://install.python-poetry.org | python3 - 19 | - run: ~/.local/share/pypoetry/venv/bin/poetry install --with lint 20 | - run: make lint 21 | tests: 22 | name: tests 23 | strategy: 24 | matrix: 25 | python: ["3.9", "3.10", "3.11"] 26 | fail-fast: false 27 | runs-on: ubuntu-latest 28 | 29 | services: 30 | postgres: 31 | image: postgres 32 | env: 33 | POSTGRES_USER: postgres 34 | POSTGRES_PASSWORD: postgres 35 | POSTGRES_DB: dbtest 36 | ports: 37 | - 5432:5432 38 | # needed because the postgres container does not provide a healthcheck 39 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 40 | 41 | steps: 42 | - uses: actions/checkout@v2 43 | - uses: actions/setup-python@v2 44 | with: 45 | python-version: ${{ matrix.python }} 46 | - name: "Installs dependencies" 47 | run: | 48 | curl -sSL https://install.python-poetry.org | python3 - 49 | - run: ~/.local/share/pypoetry/venv/bin/poetry install --with test 50 | - run: make test 51 | -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: home.html 3 | --- 4 | # SQLA-Wrapper 5 | 6 | SQLA-Wrapper is a wrapper for SQLAlchemy and Alembic that simplifies many aspects of its setup. 7 | 8 | It works with the [newer SQLAlchemy 2.0 style](https://docs.sqlalchemy.org/en/20/glossary.html#term-2.0-style), and can be used with most web frameworks. 9 | 10 | 11 | ## Includes 12 | 13 | - A [SQLAlchemy wrapper](sqlalchemy-wrapper), that does all the SQLAlchemy setup and gives you: 14 | - A scoped session extended with some useful active-record-like methods. 15 | - A declarative base class. 16 | - A helper for performant testing with a real database. 17 | 18 | ```python 19 | from sqla_wrapper import SQLAlchemy 20 | 21 | db = SQLAlchemy("sqlite:///db.sqlite", **options) 22 | # You can also use separated host, name, etc. 23 | # db = SQLAlchemy(user=…, password=…, host=…, port=…, name=…) 24 | ``` 25 | 26 | - An [Alembic wrapper](alembic-wrapper) that loads the config from your application instead of from separated `alembic.ini` and `env.py` files. 27 | 28 | ```python 29 | from sqla_wrapper import Alembic, SQLAlchemy 30 | 31 | db = SQLAlchemy(…) 32 | alembic = Alembic(db, "db/migrations") 33 | ``` 34 | 35 | 36 | ## Installation 37 | 38 | Install the package using `pip`. The `SQLAlchemy` and `Alembic` libraries will be installed as dependencies. 39 | 40 | ```bash 41 | pip install sqla-wrapper 42 | ``` 43 | 44 | 45 | ## Resources 46 | 47 | - [Source code (MIT Licensed)](https://github.com/jpsca/sqla-wrapper) 48 | - [PyPI](https://pypi.org/project/sqla-wrapper/) 49 | - [Change log](https://github.com/jpsca/sqla-wrapper/releases) 50 | -------------------------------------------------------------------------------- /tests/test_session.py: -------------------------------------------------------------------------------- 1 | def test_first(dbs, TestModelA): 2 | dbs.add(TestModelA(title="Lorem")) 3 | dbs.add(TestModelA(title="Ipsum")) 4 | dbs.add(TestModelA(title="Sit")) 5 | dbs.commit() 6 | 7 | obj = dbs.first(TestModelA) 8 | assert obj.title == "Lorem" 9 | 10 | 11 | def test_create(dbs, TestModelA): 12 | dbs.create(TestModelA, title="Remember") 13 | dbs.commit() 14 | obj = dbs.first(TestModelA) 15 | assert obj.title == "Remember" 16 | 17 | 18 | def test_all(dbs, TestModelA): 19 | dbs.create(TestModelA, title="Lorem") 20 | dbs.create(TestModelA, title="Ipsum") 21 | dbs.commit() 22 | 23 | obj_list = dbs.all(TestModelA) 24 | assert len(obj_list) == 2 25 | 26 | 27 | def test_create_or_first_using_create(dbs, TestModelA): 28 | obj1 = dbs.create_or_first(TestModelA, title="Lorem Ipsum") 29 | assert obj1 30 | dbs.commit() 31 | 32 | obj2 = dbs.create_or_first(TestModelA, title="Lorem Ipsum") 33 | assert obj1 == obj2 34 | 35 | 36 | def test_create_or_first_using_first(dbs, TestModelA): 37 | obj1 = dbs.create(TestModelA, title="Lorem Ipsum") 38 | assert obj1 39 | dbs.commit() 40 | 41 | obj2 = dbs.create_or_first(TestModelA, title="Lorem Ipsum") 42 | assert obj1 == obj2 43 | 44 | 45 | def test_first_or_create_using_first(dbs, TestModelA): 46 | obj1 = dbs.create(TestModelA, title="Lorem Ipsum") 47 | assert obj1 48 | dbs.commit() 49 | 50 | obj2 = dbs.first_or_create(TestModelA, title="Lorem Ipsum") 51 | assert obj1 == obj2 52 | 53 | 54 | def test_first_or_create_using_create(dbs, TestModelA): 55 | assert dbs.first_or_create(TestModelA, id=1, title="Lorem Ipsum") 56 | dbs.commit() 57 | 58 | obj = dbs.first_or_create(TestModelA, title="Lorem Ipsum") 59 | assert obj and obj.id == 1 60 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from datetime import datetime 3 | from pathlib import Path 4 | from tempfile import mkdtemp 5 | 6 | import pytest 7 | import sqlalchemy as sa 8 | from sqlalchemy.orm import Mapped, mapped_column 9 | 10 | from sqla_wrapper import SQLAlchemy 11 | 12 | 13 | @pytest.fixture() 14 | def memdb() -> SQLAlchemy: 15 | return SQLAlchemy("sqlite://") 16 | 17 | 18 | @pytest.fixture() 19 | def dst(): 20 | """Return a real temporary folder path which is unique to each test 21 | function invocation. This folder is deleted after the test has finished. 22 | """ 23 | dst = mkdtemp() 24 | dst = Path(dst).resolve() 25 | yield dst 26 | shutil.rmtree(dst, ignore_errors=True) 27 | 28 | 29 | @pytest.fixture(scope="session") 30 | def db() -> SQLAlchemy: 31 | return SQLAlchemy( 32 | dialect="postgresql", 33 | user="postgres", 34 | password="postgres", 35 | name="dbtest", 36 | ) 37 | 38 | 39 | @pytest.fixture(scope="session") 40 | def TestModelA(db): 41 | class TestModelA(db.Model): 42 | __tablename__ = "test_model_a" 43 | id: Mapped[int] = mapped_column(primary_key=True) 44 | title: Mapped[str] = mapped_column(sa.String(50), nullable=False, unique=True) 45 | created_at: Mapped[datetime] = mapped_column(sa.DateTime, default=datetime.utcnow) 46 | 47 | return TestModelA 48 | 49 | 50 | @pytest.fixture(scope="session") 51 | def TestModelB(db): 52 | class TestModelB(db.Model): 53 | __tablename__ = "test_model_b" 54 | id: Mapped[int] = mapped_column(primary_key=True) 55 | title: Mapped[str] = mapped_column(sa.String(50), nullable=False, unique=True) 56 | 57 | return TestModelB 58 | 59 | 60 | @pytest.fixture(scope="session") 61 | def dbsetup(db, TestModelA, TestModelB): 62 | db.drop_all() 63 | db.create_all() 64 | with db.Session() as dbs: 65 | dbs.create(TestModelB, title="first") 66 | dbs.commit() 67 | yield 68 | db.drop_all() 69 | 70 | 71 | @pytest.fixture() 72 | def dbs(db, dbsetup): 73 | tt = db.test_transaction(savepoint=True) 74 | yield db.s 75 | tt.close() 76 | -------------------------------------------------------------------------------- /docs/docs/sqlalchemy-wrapper.md: -------------------------------------------------------------------------------- 1 | # SQLAlchemy wrapper 2 | 3 | The `SQLAlchemy` wrapper class is a *light* wrapper over regular SQLAlchemy, mainly to simplify the configuration. 4 | 5 | A `SQLAlchemy` instance gives you access to the following things: 6 | 7 | - `db.engine`: An engine created with the `future=True` argument 8 | - A scoped session `db.s` and a `db.Session` class to manually create one, both extended with some useful active-record-like methods. (See ["Working with the session"](working-with-the-session).) 9 | - `db.Model`: A declarative base class 10 | - `db.create_all()` and `db.drop_all()` methods to create and drop tables according to the models. 11 | - `db.test_transaction()`: A helper for performant testing with a real database. (See ["Testing with a real database"](testing-with-a-real-database).) 12 | 13 | 14 | ## Set up 15 | 16 | The only required argument is the connection URI. You can give it directly: 17 | 18 | ```python 19 | from sqla_wrapper import SQLAlchemy 20 | 21 | db = SQLAlchemy("postgresql://scott:tiger@localhost/test") 22 | ``` 23 | 24 | or as separated host, user, password, database name, etc. parameters, and SQLA-Wrapper will build the URI for you. 25 | 26 | ```python 27 | from sqla_wrapper import SQLAlchemy 28 | 29 | db = SQLAlchemy( 30 | dialect="postgresql", 31 | user="scott", 32 | password="tiger", 33 | host="localhost", 34 | name="test", 35 | ) 36 | ``` 37 | 38 | After the setup, you will be interacting mostly directly with SQLAlchemy so I recommend reading the official [SQLAlchemy tutorial](https://docs.sqlalchemy.org/en/20/tutorial/index.html) if you haven't done it yet. 39 | 40 | Beyond the URI, the class also accepts an `engine_options` and a `session_options` dictionary to pass special options when creating the engine and/or the session. 41 | 42 | 43 | ## Declaring models 44 | 45 | A `SQLAlchemy` instance provides a `db.Model` class to be used as a declarative base class for your models. Follow the new [type-based way to declare the table columns](https://docs.sqlalchemy.org/en/20/tutorial/metadata.html#declaring-mapped-classes) 46 | 47 | ```python 48 | from sqlalchemy.orm import Mapped, mapped_column 49 | from myapp.models import db 50 | 51 | class User(db.Model): 52 | __tablename__ = "users" 53 | 54 | id: Mapped[int] = mapped_column(primary_key=True) 55 | name: Mapped[str] = mapped_column(String(128)) 56 | ``` 57 | 58 | ## API 59 | 60 | ::: sqla_wrapper.SQLAlchemy 61 | options: 62 | heading_level: 3 63 | filter: 64 | -------------------------------------------------------------------------------- /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: SQLA Wrapper 2 | site_description: A friendly wrapper for modern SQLAlchemy and Alembic 3 | site_url: https://sqla-wrapper.scaletti.dev 4 | repo_url: https://github.com/jpsca/sqla-wrapper 5 | repo_name: jpsca/sqla-wrapper 6 | copyright: Copyright © 2013 Juan-Pablo Scaletti 7 | 8 | nav: 9 | - Home: index.md 10 | - SQLAlchemy: 11 | - sqlalchemy-wrapper.md 12 | - working-with-the-session.md 13 | - testing-with-a-real-database.md 14 | - Alembic: 15 | - alembic-wrapper.md 16 | - how-to.md 17 | - API: 18 | - api/sqlalchemy-wrapper-class.md 19 | - api/session-class.md 20 | - api/testtransaction-class.md 21 | - api/alembic-wrapper-class.md 22 | 23 | theme: 24 | name: material 25 | logo: assets/img/logo.svg 26 | favicon: assets/img/favicon.svg 27 | font: 28 | text: Convergence 29 | code: Roboto Mono 30 | custom_dir: overrides 31 | language: en 32 | features: 33 | - navigation.expand 34 | # - navigation.indexes 35 | # - navigation.instant 36 | # - navigation.sections 37 | - navigation.top 38 | - navigation.tracking 39 | - search.suggest 40 | - search.highligh 41 | icon: 42 | repo: material/github 43 | palette: 44 | - media: "(prefers-color-scheme: light)" 45 | scheme: default 46 | primary: red 47 | accent: pink 48 | toggle: 49 | icon: octicons/sun-16 50 | name: Switch to dark mode 51 | - media: "(prefers-color-scheme: dark)" 52 | scheme: slate 53 | primary: red 54 | accent: pink 55 | toggle: 56 | icon: octicons/moon-16 57 | name: Switch to light mode 58 | 59 | extra_css: 60 | - assets/css/highlight.css 61 | # - assets/css/termynal.css 62 | - assets/css/common.css 63 | # extra_javascript: 64 | # - assets/js/termynal.js 65 | edit_uri: "" 66 | plugins: 67 | - search 68 | - mkdocstrings: 69 | default_handler: python 70 | handlers: 71 | python: 72 | options: 73 | show_bases: false 74 | show_source: false 75 | 76 | 77 | markdown_extensions: 78 | - admonition 79 | - attr_list 80 | - smarty 81 | - tables 82 | - toc: 83 | permalink: "#" 84 | toc_depth: 4 85 | - pymdownx.betterem: 86 | smart_enable: all 87 | - pymdownx.caret 88 | - pymdownx.details 89 | - pymdownx.highlight 90 | - pymdownx.keys 91 | - pymdownx.magiclink 92 | - pymdownx.mark 93 | - pymdownx.smartsymbols 94 | - pymdownx.superfences 95 | - pymdownx.tabbed 96 | - pymdownx.tasklist 97 | - pymdownx.tilde 98 | -------------------------------------------------------------------------------- /docs/overrides/home.html: -------------------------------------------------------------------------------- 1 | {% extends "main.html" %} 2 | 3 | {% block extrahead %} 4 | {{ super() }} 5 | 89 | {% endblock %} 90 | 91 | {% block tabs %} 92 | {{ super() }} 93 |
94 |
95 |
96 | 97 |
98 |

SQLA-Wrapper

99 |

100 | A friendly wrapper for 101 | modern SQLAlchemy 104 | and Alembic. 105 |

106 |
107 |
108 |
109 |
110 | {% endblock %} 111 | -------------------------------------------------------------------------------- /docs/docs/alembic-wrapper.md: -------------------------------------------------------------------------------- 1 | # Alembic wrapper 2 | 3 | Alembic is great but it has a configuration problem. While you web application config is probably one or more python files and parameters loaded from environment variables and other sources, Alembic needs a static `alembic.ini` file. You are suppose to edit the almost-undocumented `env.py` file to customize it to your application. 4 | 5 | The `Alembic` wrapper class aims to simplify that set up so you can just use your application config, without any more config files to maintain. The actual database migrations are still handled by Alembic so you get exactly the same functionality. 6 | 7 | 8 | ## Set up 9 | 10 | The `Alembic()` class require two arguments: A [`SQLAlchemy()`](sqlalchemy-wrapper) instance and the path of the folder that will contain the migrations. 11 | 12 | ```python 13 | from sqla_wrapper import Alembic, SQLAlchemy 14 | 15 | db = SQLAlchemy(…) 16 | alembic = Alembic(db, "db/migrations") 17 | ``` 18 | 19 | If the migrations folder doesn't exists, it will be created. 20 | 21 | 22 | ## CLI integrations 23 | 24 | The only downside is that you can't use the `alembic` command-line tool anymore. Instead, all the usual Alembic command are be available as methods of the wrapper instance and you need to integrate them with your framework/application CLI. 25 | 26 | Is easier than it sounds, specially because the wrapper comes with one-line methods to extend [Click](https://click.palletsprojects.com) (the CLI used by Flask by default) and [Proper CLI](https://github.com/jpsca/proper-cli) (arguably, the best CLI ever made). 27 | 28 | ### Integrating with Flask Click 29 | 30 | ```python 31 | from flask import Flask 32 | 33 | db = SQLAlchemy(…) 34 | alembic = Alembic(…) 35 | 36 | app = Flask(__name__) 37 | app.cli.add_command(alembic.get_flask_cli("db")) 38 | ``` 39 | 40 | ### Integrating with Click 41 | 42 | ```python 43 | import click 44 | 45 | db = SQLAlchemy(…) 46 | alembic = Alembic(…) 47 | 48 | @click.group() 49 | def cli(): 50 | pass 51 | 52 | cli.add_command(alembic.get_click_cli("db")) 53 | 54 | ``` 55 | 56 | ### Integrating with Proper CLI 57 | 58 | ```python 59 | from proper_cli import Cli 60 | 61 | db = SQLAlchemy(…) 62 | alembic = Alembic(…) 63 | 64 | class Manage(Cli): 65 | db = alembic.get_proper_cli("db") 66 | 67 | cli = Manage() 68 | 69 | ``` 70 | 71 | ## API 72 | 73 | For a more in-depth understanding of these methods and the extra options, you can read the [documentation for the Alembic config](https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file). 74 | 75 | ::: sqla_wrapper.Alembic 76 | options: 77 | heading_level: 3 78 | members: 79 | - revision 80 | - upgrade 81 | - downgrade 82 | - get_history 83 | - history 84 | - stamp 85 | - get_current 86 | - current 87 | - get_head 88 | - head 89 | - init 90 | - create_all 91 | - rev_id 92 | - get_proper_cli 93 | - get_click_cli 94 | - get_flask_cli 95 | -------------------------------------------------------------------------------- /docs/docs/working-with-the-session.md: -------------------------------------------------------------------------------- 1 | # Working with the session 2 | 3 | The Session is the mean that you use to communicate with the database. 4 | There are two main ways to use it: 5 | 6 | ## In a web application: Use the scoped session `db.s` 7 | 8 | The "scoped_session" is really a proxy to a session automatically scoped to the current thread. 9 | 10 | This allows having a global session, so the session can be shared without the need to pass it explicitly. 11 | 12 | A scoped session is the recommended way to work in a web application, however, you must remember to call `db.s.remove()` the session at the end of the request. 13 | 14 | Use your framework's "on request end" hook, to do that. For example, in Flask: 15 | 16 | ```python 17 | @app.teardown_request 18 | def remove_db_scoped_session(error=None): 19 | db.s.remove() 20 | ``` 21 | 22 | The `db.s.remove()` method close the current session and dispose it. A new session will be created when `db.s` is called again. 23 | 24 | ### Background job/tasks 25 | 26 | Outside a web request cycle, like in a background job, you still can use the global session, but you must: 27 | 28 | 1. Call `db.engine.dispose()` when each new process is created. 29 | 2. Call `db.s.remove()` at the end of each job/task 30 | 31 | Background jobs libraries, like Celery or RQ, use multiprocessing or `fork()`, to have several "workers" to run these jobs. When that happens, the pool of connections to the database is copied to the child processes, which does causes errors. 32 | 33 | For that reason you should call `db.engine.dispose()` when each worker process is created, so that the engine creates brand new database connections local to that fork. 34 | 35 | You also must remember to call `db.s.remove()` at the end of each job, so a new session is used each time. 36 | 37 | With most background jobs libraries you can set them so it's done automatically, see: 38 | 39 | - [Working with RQ](how-to/#rq) 40 | - [Working with Celery](how-to/#celery) 41 | 42 | 43 | ## In a standalone script: Instantiate `db.Session` 44 | 45 | Instantiating `db.Session` is the recommended way to work when the session is not shared like in a command-line script. 46 | 47 | You can use a context manager: 48 | 49 | ```python 50 | with db.Session() as dbs: 51 | # work with the session here 52 | ``` 53 | 54 | When the session is created in this way, a database transaction is automatically initiated when required, and the `dbs.flush()`, `dbs.commit()`, and `dbs.rollback()` methods can be used as needed. 55 | 56 | The session is automatically closed and returned to the session pool when the context manager block ends. 57 | 58 | You can also create it without a context manager and close it manually: 59 | 60 | ```python 61 | dbs = db.Session(): 62 | # work with the session here 63 | dbs.close() 64 | ``` 65 | 66 | 67 | ## API 68 | 69 | ::: sqla_wrapper.Session 70 | options: 71 | heading_level: 3 72 | members: 73 | - get 74 | - all 75 | - create 76 | - first 77 | - first_or_create 78 | - create_or_first 79 | 80 | --- 81 | 82 | As always, I recommend reading the official [SQLAlchemy tutorial](https://docs.sqlalchemy.org/en/20/tutorial/orm_data_manipulation.html#tutorial-orm-data-manipulation) to learn more how to work with the session. 83 | -------------------------------------------------------------------------------- /tests/test_sqlalchemy_wrapper.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sqlalchemy as sa 3 | from sqlalchemy.exc import OperationalError 4 | from sqlalchemy.orm import Mapped, mapped_column 5 | 6 | from sqla_wrapper import SQLAlchemy 7 | 8 | 9 | def test_repr(memdb): 10 | assert memdb.url in str(memdb) 11 | 12 | 13 | def test_setup_with_params_full(): 14 | db = SQLAlchemy( 15 | dialect="postgresql+psycopg2", 16 | user="scott", 17 | password="tiger", 18 | host="localhost", 19 | port=123, 20 | name="test", 21 | ) 22 | assert db.url == "postgresql+psycopg2://scott:tiger@localhost:123/test" 23 | 24 | 25 | def test_setup_with_some_params(): 26 | db = SQLAlchemy( 27 | dialect="postgresql", 28 | user="postgres", 29 | host="localhost", 30 | name="test", 31 | ) 32 | assert db.url == "postgresql://postgres@localhost/test" 33 | 34 | 35 | def test_setup_with_password(): 36 | db = SQLAlchemy( 37 | dialect="postgresql", 38 | user="postgres", 39 | password="postgres", 40 | name="dbtest", 41 | ) 42 | assert db.url == "postgresql://postgres:postgres@127.0.0.1/dbtest" 43 | 44 | 45 | def test_setup_with_params_minimal(): 46 | db = SQLAlchemy(dialect="sqlite") 47 | assert db.url == "sqlite://" 48 | 49 | 50 | def test_setup_with_sqlite_path(): 51 | db = SQLAlchemy(dialect="sqlite", name="relpath/to/database.db") 52 | assert db.url == "sqlite:///relpath/to/database.db" 53 | 54 | 55 | def test_setup_with_driver_options(): 56 | name = ( 57 | "file:path/to/database.db?" 58 | "check_same_thread=true&timeout=10&mode=ro&nolock=1&uri=true" 59 | ) 60 | db = SQLAlchemy(dialect="sqlite+pysqlite", name=name) 61 | assert db.url == f"sqlite+pysqlite:///{name}" 62 | 63 | 64 | def test_drop_all(memdb): 65 | class ToDo(memdb.Model): 66 | __tablename__ = "todos" 67 | id: Mapped[int] = mapped_column(primary_key=True) 68 | 69 | memdb.create_all() 70 | memdb.drop_all() 71 | 72 | with pytest.raises(OperationalError): 73 | with memdb.Session() as session: 74 | session.execute(sa.select(ToDo)).all() 75 | 76 | 77 | def test_single_table_inhertance(memdb): 78 | class Person(memdb.Model): 79 | __tablename__ = "persons" 80 | id: Mapped[int] = mapped_column(primary_key=True) 81 | type: Mapped[str] = mapped_column(sa.String(50)) 82 | 83 | __mapper_args__ = { 84 | "polymorphic_identity": "person", 85 | "polymorphic_on": type, 86 | } 87 | 88 | class Engineer(Person): 89 | engineer_name: Mapped[str] = mapped_column(sa.String(30), nullable=True) 90 | __mapper_args__ = {"polymorphic_identity": "engineer"} 91 | 92 | class Manager(Person): 93 | manager_name: Mapped[str] = mapped_column(sa.String(30), nullable=True) 94 | __mapper_args__ = {"polymorphic_identity": "manager"} 95 | 96 | memdb.create_all() 97 | 98 | with memdb.Session() as dbs: 99 | dbs.create(Engineer, engineer_name="Bob") 100 | dbs.commit() 101 | 102 | obj = dbs.first(Engineer) 103 | assert obj.engineer_name == "Bob" 104 | assert obj.type == "engineer" 105 | -------------------------------------------------------------------------------- /docs/old/5.x/assets/javascripts/lunr/min/lunr.stemmer.support.min.js: -------------------------------------------------------------------------------- 1 | !function(r,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(r.lunr)}(this,function(){return function(r){r.stemmerSupport={Among:function(r,t,i,s){if(this.toCharArray=function(r){for(var t=r.length,i=new Array(t),s=0;s=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor++,!0}return!1},in_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e<=s&&e>=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor--,!0}return!1},out_grouping:function(t,i,s){if(this.cursors||e>3]&1<<(7&e)))return this.cursor++,!0}return!1},out_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e>s||e>3]&1<<(7&e)))return this.cursor--,!0}return!1},eq_s:function(t,i){if(this.limit-this.cursor>1),f=0,l=o0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n+_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n+_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},find_among_b:function(t,i){for(var s=0,e=i,n=this.cursor,u=this.limit_backward,o=0,h=0,c=!1;;){for(var a=s+(e-s>>1),f=0,l=o=0;m--){if(n-l==u){f=-1;break}if(f=r.charCodeAt(n-1-l)-_.s[m])break;l++}if(f<0?(e=a,h=l):(s=a,o=l),e-s<=1){if(s>0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n-_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n-_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},replace_s:function(t,i,s){var e=s.length-(i-t),n=r.substring(0,t),u=r.substring(i);return r=n+s+u,this.limit+=e,this.cursor>=i?this.cursor+=e:this.cursor>t&&(this.cursor=t),e},slice_check:function(){if(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>r.length)throw"faulty slice operation"},slice_from:function(r){this.slice_check(),this.replace_s(this.bra,this.ket,r)},slice_del:function(){this.slice_from("")},insert:function(r,t,i){var s=this.replace_s(r,t,i);r<=this.bra&&(this.bra+=s),r<=this.ket&&(this.ket+=s)},slice_to:function(){return this.slice_check(),r.substring(this.bra,this.ket)},eq_v_b:function(r){return this.eq_s_b(r.length,r)}}}},r.trimmerSupport={generateTrimmer:function(r){var t=new RegExp("^[^"+r+"]+"),i=new RegExp("[^"+r+"]+$");return function(r){return"function"==typeof r.update?r.update(function(r){return r.replace(t,"").replace(i,"")}):r.replace(t,"").replace(i,"")}}}}}); -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | 6 | [tool.poetry] 7 | name = "sqla_wrapper" 8 | version = "6.0.0" 9 | description = "A framework-independent modern wrapper for SQLAlchemy & Alembic" 10 | authors = ["Juan-Pablo Scaletti "] 11 | license = "MIT" 12 | readme = "README.md" 13 | homepage = "https://sqla-wrapper.scaletti.dev/" 14 | repository = "https://github.com/jpsca/sqla-wrapper" 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Environment :: Web Environment", 18 | "Intended Audience :: Developers", 19 | "License :: OSI Approved :: MIT License", 20 | "Programming Language :: Python :: 3 :: Only", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "Topic :: Software Development :: Libraries", 26 | "Topic :: Software Development :: Libraries :: Python Modules", 27 | "Topic :: Database", 28 | "Typing :: Typed", 29 | ] 30 | 31 | [tool.poetry.dependencies] 32 | python = "^3.9" 33 | sqlalchemy = "^2.0" 34 | alembic = "^1.9" 35 | 36 | [tool.poetry.group.dev] 37 | optional = true 38 | 39 | [tool.poetry.group.dev.dependencies] 40 | black = "^22.10.0" 41 | pre-commit = "^2.20.0" 42 | tox = "*" 43 | 44 | [tool.poetry.group.lint] 45 | optional = true 46 | 47 | [tool.poetry.group.lint.dependencies] 48 | flake8 = "*" 49 | flake8-bugbear = "*" 50 | pyright = "^1.1.282" 51 | 52 | [tool.poetry.group.test] 53 | optional = true 54 | 55 | [tool.poetry.group.test.dependencies] 56 | pytest = ">=7.2.0" 57 | pytest-cov = "*" 58 | flask = "^2.2" 59 | proper-cli = "^1.2" 60 | psycopg2-binary = "^2.9" 61 | 62 | [tool.poetry.group.docs] 63 | optional = true 64 | 65 | [tool.poetry.group.docs.dependencies] 66 | mkdocs = "^1.4.2" 67 | mkdocs-material = "^9.0.12" 68 | pygments-github-lexers = "^0.0.5" 69 | mkdocstrings-python = "^0.8.3" 70 | 71 | 72 | [tool.black] 73 | line-length = 88 74 | include = '\.pyi?$' 75 | exclude = ''' 76 | /( 77 | \.git 78 | | \.hg 79 | | \.tox 80 | | \.venv 81 | | _build 82 | | build 83 | | dist 84 | )/ 85 | ''' 86 | 87 | 88 | [tool.isort] 89 | profile = "black" 90 | force_single_line = true 91 | include_trailing_comma=true 92 | atomic = true 93 | lines_after_imports = 2 94 | lines_between_types = 1 95 | 96 | 97 | [tool.coverage.run] 98 | branch = true 99 | omit = [ 100 | "*/cli/*", 101 | ] 102 | 103 | [tool.coverage.report] 104 | exclude_lines = [ 105 | "pragma: no cover", 106 | "TYPE_CHECKING", 107 | "def __repr__", 108 | "def __str__", 109 | "raise AssertionError", 110 | "raise NotImplementedError", 111 | "if __name__ == .__main__.:" 112 | ] 113 | 114 | [tool.coverage.html] 115 | directory = "covreport" 116 | 117 | 118 | [tool.pyright] 119 | include = ["src"] 120 | exclude = [ 121 | "**/__pycache__", 122 | "**/tests", 123 | ] 124 | ignore = [] 125 | reportPrivateImportUsage = false 126 | reportWildcardImportFromLibrary = false 127 | 128 | 129 | [tool.pytest.ini_options] 130 | addopts = "--doctest-modules" 131 | 132 | 133 | [tool.tox] 134 | legacy_tox_ini = """ 135 | [tox] 136 | skipsdist = True 137 | envlist = py39,py310,py311 138 | 139 | [testenv] 140 | skip_install = true 141 | allowlist_externals = poetry 142 | commands = 143 | pip install -U pip wheel 144 | poetry install --with test 145 | pytest -x src/sqla_wrapper tests 146 | """ 147 | -------------------------------------------------------------------------------- /docs/docs/assets/js/termynal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * termynal.js 3 | * A lightweight, modern and extensible animated terminal window, using 4 | * async/await. 5 | * 6 | * @author Ines Montani 7 | * @version 0.0.1 8 | * @license MIT 9 | */ 10 | 'use strict';var _createClass=function(){function a(b,c){for(var e,d=0;dz){break;}}}},{key:'_wait',value:function _wait(b){return new Promise(function(c){return setTimeout(c,b)})}}]),a} ();if(document.currentScript.hasAttribute('data-termynal-container')){var containers=document.currentScript.getAttribute('data-termynal-container');containers.split('|').forEach(function(a){return new Termynal(a)})} 11 | -------------------------------------------------------------------------------- /docs/old/5.x/assets/js/termynal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * termynal.js 3 | * A lightweight, modern and extensible animated terminal window, using 4 | * async/await. 5 | * 6 | * @author Ines Montani 7 | * @version 0.0.1 8 | * @license MIT 9 | */ 10 | 'use strict';var _createClass=function(){function a(b,c){for(var e,d=0;dz){break;}}}},{key:'_wait',value:function _wait(b){return new Promise(function(c){return setTimeout(c,b)})}}]),a} ();if(document.currentScript.hasAttribute('data-termynal-container')){var containers=document.currentScript.getAttribute('data-termynal-container');containers.split('|').forEach(function(a){return new Termynal(a)})} 11 | -------------------------------------------------------------------------------- /docs/overrides/partials/header.html: -------------------------------------------------------------------------------- 1 | {% set class = "md-header" %} 2 | {% if "navigation.tabs.sticky" in features %} 3 | {% set class = class ~ " md-header--lifted" %} 4 | {% endif %} 5 |
6 | 87 | {% if "navigation.tabs.sticky" in features %} 88 | {% if "navigation.tabs" in features %} 89 | {% include "partials/tabs.html" %} 90 | {% endif %} 91 | {% endif %} 92 |
93 | -------------------------------------------------------------------------------- /src/sqla_wrapper/cli/click_cli.py: -------------------------------------------------------------------------------- 1 | def get_flask_cli(alembic, name): 2 | from flask.cli import FlaskGroup 3 | 4 | group = FlaskGroup(name) 5 | return _get_cli(alembic, group) 6 | 7 | 8 | def get_click_cli(alembic, name): 9 | from click import Group 10 | 11 | group = Group(name) 12 | return _get_cli(alembic, group) 13 | 14 | 15 | def _get_cli(alembic, group): 16 | import click 17 | 18 | @group.command() 19 | @click.argument("message", default=None) 20 | @click.option( 21 | "--empty", is_flag=True, default=False, 22 | help="Generate just an empty migration file, not the operations.", 23 | ) 24 | @click.option( 25 | "--parent", default=None, 26 | help="Parent revision of this new revision.", 27 | ) 28 | def revision(message, empty, parent): 29 | """Create a new revision. 30 | Auto-generate operations by comparing models and database. 31 | """ 32 | alembic.revision(message, empty=empty, parent=parent) 33 | 34 | @group.command() 35 | @click.argument("target", default="head") 36 | @click.option( 37 | "--sql", is_flag=True, default=False, 38 | help="Don't emit SQL to database, dump to standard output instead", 39 | ) 40 | def upgrade(target, sql): 41 | """Run migrations to upgrade database.""" 42 | alembic.upgrade(target, sql=sql) 43 | 44 | @group.command() 45 | @click.argument("target", default="-1") 46 | @click.option( 47 | "--sql", is_flag=True, default=False, 48 | help="Don't emit SQL to database, dump to standard output instead", 49 | ) 50 | def downgrade(target, sql): 51 | """Revert to a previous version""" 52 | alembic.downgrade(target, sql=sql) 53 | 54 | @group.command() 55 | @click.option( 56 | "-v", "--verbose", is_flag=True, default=False, 57 | help="Shows also the path and the docstring of each revision file.", 58 | ) 59 | @click.option( 60 | "-s", "--start", default="base", 61 | help="From this revision (including it.)", 62 | ) 63 | @click.option( 64 | "-e", "--end", default="head", 65 | help="To this revision (including it.)", 66 | ) 67 | def history(verbose, start, end): 68 | """Get the list of revisions in chronological order. 69 | You can optionally specify the range of revisions to return. 70 | """ 71 | alembic.history(verbose=verbose, start=start, end=end) 72 | 73 | @group.command() 74 | @click.argument("target", default="head") 75 | @click.option( 76 | "--sql", is_flag=True, default=False, 77 | help="Don't emit SQL to database - dump to standard output instead", 78 | ) 79 | @click.option( 80 | "--purge", is_flag=True, default=False, 81 | help="Delete all entries in the version table before stamping.", 82 | ) 83 | def stamp(target, sql, purge): 84 | """Set the given revision in the revision table. 85 | Don't run migrations.""" 86 | alembic.stamp(target, sql=sql, purge=purge) 87 | 88 | @group.command() 89 | @click.option( 90 | "-v", "--verbose", is_flag=True, default=False, 91 | help="Shows also the path and the docstring of the revision file.", 92 | ) 93 | def current(verbose): 94 | """Print the latest revision(s) applied.""" 95 | alembic.current(verbose=verbose) 96 | 97 | @group.command() 98 | @click.option( 99 | "-v", "--verbose", is_flag=True, default=False, 100 | help="Shows also the path and the docstring of the revision file.", 101 | ) 102 | def head(verbose): 103 | """Print the latest revision(s).""" 104 | alembic.head(verbose=verbose) 105 | 106 | @group.command() 107 | @click.argument("path", default=None) 108 | def init(path): 109 | """Creates a new migration folder 110 | with a `script.py.mako` template file. It doesn't fail if the 111 | folder or file already exists. 112 | """ 113 | alembic.init(path) 114 | 115 | @group.command() 116 | def create_all(): 117 | """Create all the tables from the current models 118 | and stamp the latest revision without running any migration. 119 | """ 120 | alembic.create_all() 121 | 122 | return group 123 | -------------------------------------------------------------------------------- /src/sqla_wrapper/session.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | import sqlalchemy.orm 4 | from sqlalchemy import select 5 | from sqlalchemy.exc import IntegrityError 6 | from sqlalchemy.orm import scoped_session 7 | 8 | 9 | __all__ = ("Session",) 10 | 11 | 12 | class Session(sqlalchemy.orm.Session): 13 | """SQLAlchemy default Session class has the method `.get(Model, pk)` 14 | to query and return a record by its primary key. 15 | 16 | This class extends the `sqlalchemy.orm.Session` class with some useful 17 | active-record-like methods. 18 | """ 19 | 20 | def all(self, Model: t.Any, **attrs) -> t.Sequence[t.Any]: 21 | """Returns all the object found with these attributes. 22 | 23 | The filtering is done with a simple `.filter_by()` so is limited 24 | to “equality” comparisons against the columns of the model. 25 | Also, there is no way to sort the results. If you need sorting or 26 | more complex filtering, you are better served using a `db.select()`. 27 | 28 | **Examples**: 29 | 30 | ```python 31 | users = db.s.all(User) 32 | users = db.s.all(User, deleted=False) 33 | users = db.s.all(User, account_id=123, deleted=False) 34 | ``` 35 | """ 36 | return self.execute(select(Model).filter_by(**attrs)).scalars().all() 37 | 38 | def create(self, Model: t.Any, **attrs: t.Any) -> t.Any: 39 | """Creates a new object and adds it to the session. 40 | 41 | This is a shortcut for: 42 | 43 | ```python 44 | obj = Model(**attrs) 45 | db.s.add(obj) 46 | db.s.flush() 47 | ``` 48 | 49 | Note that this does a `db.s.flush()`, so you must later call 50 | `db.s.commit()` to persist the new object. 51 | 52 | **Example**: 53 | 54 | ```python 55 | new_user = db.s.create(User, email='foo@example.com') 56 | db.s.commit() 57 | ``` 58 | """ 59 | obj = Model(**attrs) 60 | self.add(obj) 61 | self.flush() 62 | return obj 63 | 64 | def first(self, Model: t.Any, **attrs: t.Any) -> t.Any: 65 | """Returns the first object found with these attributes or `None` 66 | if there isn't one. 67 | 68 | The filtering is done with a simple `.filter_by()` so is limited 69 | to “equality” comparisons against the columns of the model. 70 | Also, there is no way to sort the results. If you need sorting or 71 | more complex filtering, you are better served using a `db.select()`. 72 | 73 | **Examples**: 74 | 75 | ```python 76 | user = db.s.first(User) 77 | user = db.s.first(User, deleted=False) 78 | ``` 79 | """ 80 | return self.execute( 81 | select(Model).filter_by(**attrs).limit(1) 82 | ).scalars().first() 83 | 84 | def first_or_create(self, Model: t.Any, **attrs) -> t.Any: 85 | """Tries to find an object and if none exists, it tries to create 86 | a new one first. Use this method when you expect the object to 87 | already exists but want to create it in case it doesn't. 88 | 89 | This does a `db.s.flush()`, so you must later call `db.s.commit()` 90 | to persist the new object (in case one has been created). 91 | 92 | **Examples**: 93 | 94 | ```python 95 | user1 = db.s.first_or_create(User, email='foo@example.com') 96 | user2 = db.s.first_or_create(User, email='foo@example.com') 97 | user1 is user2 98 | ``` 99 | """ 100 | obj = self.first(Model, **attrs) 101 | if obj: 102 | return obj 103 | return self.create_or_first(Model, **attrs) 104 | 105 | def create_or_first(self, Model: t.Any, **attrs: t.Any) -> t.Any: 106 | """Tries to create a new object, and if it fails because already exists, 107 | return the first it founds. For this to work one or more of the 108 | attributes must be unique so it does fail, otherwise you will be creating 109 | a new different object. 110 | 111 | Use this method when you expect that the object does not exists but want 112 | to avoid an exception in case it does. 113 | 114 | This does a `db.s.flush()`, so you must later call `db.s.commit()` 115 | to persist the new object (in case one has been created). 116 | 117 | **Examples**: 118 | 119 | ```python 120 | user1 = db.s.create_or_first(User, email='foo@example.com') 121 | user2 = db.s.create_or_first(User, email='foo@example.com') 122 | user1 is user2 123 | ``` 124 | """ 125 | try: 126 | return self.create(Model, **attrs) 127 | except IntegrityError: 128 | self.rollback() 129 | return self.first(Model, **attrs) 130 | 131 | 132 | class PatchedScopedSession(scoped_session): 133 | def all(self, Model: t.Any, **attrs) -> t.List[t.Any]: 134 | return self.registry().all(Model, **attrs) 135 | 136 | def create(self, Model: t.Any, **attrs) -> t.Any: 137 | return self.registry().create(Model, **attrs) 138 | 139 | def first(self, Model: t.Any, **attrs) -> t.Any: 140 | return self.registry().first(Model, **attrs) 141 | 142 | def first_or_create(self, Model: t.Any, **attrs) -> t.Any: 143 | return self.registry().first_or_create(Model, **attrs) 144 | 145 | def create_or_first(self, Model: t.Any, **attrs) -> t.Any: 146 | return self.registry().create_or_first(Model, **attrs) 147 | -------------------------------------------------------------------------------- /docs/docs/how-to.md: -------------------------------------------------------------------------------- 1 | # How to ...? 2 | 3 | In this section you can find how to do some common database tasks using SQLAlchemy and SQLA-wrapper. Is not a complete reference of what you can do with SQLAlchemy so you should read the official [SQLAlchemy tutorial](https://docs.sqlalchemy.org/en/20/tutorial/) to have a better understanding of it. 4 | 5 | All examples assume that an SQLAlchemy instance has been created and stored in a global variable named `db`. 6 | 7 | 8 | ## Declare models 9 | 10 | `db` provides a `db.Model` class to be used as a declarative base class for your models and follow the new [type-based way to declare the table columns](https://docs.sqlalchemy.org/en/20/tutorial/metadata.html#declaring-mapped-classes) 11 | 12 | ```python 13 | from sqlalchemy.orm import Mapped, mapped_column 14 | from myapp.models import db 15 | 16 | class User(db.Model): 17 | __tablename__ = "users" 18 | 19 | id: Mapped[int] = mapped_column(primary_key=True) 20 | name: Mapped[str] = mapped_column(String(128)) 21 | ``` 22 | 23 | 24 | ## Insert an object to the database 25 | 26 | Inserting data into the database is a three step process: 27 | 28 | 1. Create the Python object 29 | 1. Add it to the session 30 | 1. Commit the session 31 | 32 | ```python 33 | from myapp.models import User, db 34 | 35 | me = User(name="Me", login="hello") 36 | db.s.add(me) 37 | db.s.commit() 38 | ``` 39 | 40 | You can also use the [`db.s.create`](working-with-the-session/#api) helper method to merge the first two steps. 41 | 42 | ```python 43 | from myapp.models import User, db 44 | 45 | db.s.create(User, name="Me", login="hello") 46 | db.s.commit() 47 | ``` 48 | 49 | 50 | ## Get an object by its primary key 51 | 52 | The [`db.s.get()`](working-with-the-session/#api) method can be used to retrieve an object by its primary key: 53 | 54 | ```python 55 | from myapp.models import User, db 56 | 57 | user = db.s.get(User, 2) 58 | ``` 59 | 60 | 61 | ## Get the first object by its attributes 62 | 63 | The [`db.s.first()`](working-with-the-session/#api) helper method can be used to retrieve an object by its primary key: 64 | 65 | ```python 66 | from myapp.models import User, db 67 | 68 | user = db.s.first(User, login="hello") 69 | ``` 70 | 71 | 72 | ## Query the database 73 | 74 | First, make a query using `sqlalchemy.select( ... )`, and then execute the query with `db.s.execute( ... )`. 75 | 76 | ```python 77 | import sqlalchemy as sa 78 | from myapp.models import User, db 79 | 80 | users = db.s.execute( 81 | sa.select(User) 82 | .where(User.email.endswith('@example.com')) 83 | ).scalars() 84 | 85 | # You can now do `users.all()`, `users.first()`, 86 | # `users.unique()`, etc. 87 | ``` 88 | 89 | The [results](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.Result) from `db.s.execute()` are returned as a list of rows, where each row is a tuple, even if only one result per row was requested. The [`scalars()`](https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.ScalarResult) method conveniently extract the first result in each row. 90 | 91 | The `select()` function it is very powerful and can do **a lot** more: 92 | https://docs.sqlalchemy.org/en/20/tutorial/data_select.html#selecting-rows-with-core-or-orm 93 | 94 | 95 | 96 | ## Count the number of rows in a query 97 | 98 | Like with regular SQL, use the `count` function: 99 | 100 | ```python 101 | import sqlalchemy as sa 102 | from myapp.models import User, db 103 | 104 | num = db.s.execute( 105 | sa.select(db,func.count(User.id)) 106 | .where(User.email.endswith('@example.com')) 107 | ).scalar() 108 | ``` 109 | 110 | The `scalar()` method conveniently returns only the first object of the first row. 111 | 112 | 113 | 114 | ## Update an object 115 | 116 | To update a database object, first retrieve it, modify it, and finally commit the session. 117 | 118 | ```python 119 | from myapp.models import User, db 120 | 121 | user = db.s.first(User, login="hello") 122 | user.name = "me" 123 | db.s.commit() 124 | ``` 125 | 126 | 127 | ## Delete an object from the database 128 | 129 | Deleting objects from the database is very similar to adding new ones, instead of `db.s.add()` use `db.s.delete()`: 130 | 131 | ```python 132 | from myapp.models import User, db 133 | 134 | user = db.s.first(User, login="hello") 135 | db.s.delete(user) 136 | db.s.commit() 137 | ``` 138 | 139 | 140 | ## Run an arbitrary SQL statement 141 | 142 | Use `sqlalchemy.text` to build a query and then run it with `db.s.execute`. 143 | 144 | ```python 145 | import sqlalchemy as sa 146 | from myapp.models import db 147 | 148 | sql = sa.text("SELECT * FROM user WHERE user.id = :user_id") 149 | results = db.s.execute(sql, params={"user_id": 5}).all() 150 | ``` 151 | 152 | Parameters are specified by name, always using the format `:name`, no matter the database engine. 153 | 154 | Is important to use `text()` instead of plain strings so the parameters are escaped protecting you from SQL injection attacks. 155 | 156 | 157 | ## Work with background jobs/tasks 158 | 159 | 1. Call `db.engine.dispose()` when each new process is created. 160 | 2. Call `db.s.remove()` at the end of each job/task 161 | 162 | Background jobs libraries, like Celery or RQ, use multiprocessing or `fork()`, to have several "workers" to run these jobs. When that happens, the pool of connections to the database is copied to the child processes, which does causes errors. 163 | 164 | For that reason you should call `db.engine.dispose()` when each worker process is created, so that the engine creates brand new database connections local to that fork. 165 | 166 | You also must remember to call `db.s.remove()` at the end of each job, so a new session is used each time. 167 | 168 | ### RQ 169 | 170 | RQ actually uses a `fork()` for *each* job. The best way to make sure you make the required cleanup is to use a custom `Worker` class: 171 | 172 | ```python 173 | # foo/bar.py 174 | import rq 175 | from myapp.models import db 176 | 177 | class Worker(rq.Worker): 178 | def perform_job(self, job, queue): 179 | db.engine.dispose() 180 | rv = super().perform_job(job, queue) 181 | db.s.remove() 182 | return rv 183 | 184 | ``` 185 | 186 | You can then use that custom class by starting the workers with the `--worker-class` argument: 187 | 188 | ```bash 189 | rq worker --worker-class 'foo.bar.Worker' 190 | ``` 191 | 192 | ### Celery 193 | 194 | Use signals to register functions to run when the worker is ready and when each job/task finish. 195 | 196 | ```python 197 | from celery.signals import task_postrun, worker_process_init 198 | from myapp.models import db 199 | 200 | @worker_process_init 201 | def refresh_db_connections(*args, **kwargs): 202 | db.engine.dispose() 203 | 204 | @task_postrun 205 | def remove_db_scoped_session(*args, **kwargs): 206 | db.s.remove() 207 | 208 | ``` 209 | -------------------------------------------------------------------------------- /src/sqla_wrapper/sqlalchemy_wrapper.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | import sqlalchemy as sa 4 | from sqlalchemy import orm as sa_orm 5 | from sqlalchemy.event import listens_for as sa_listens_for 6 | 7 | from .base_model import BaseModel 8 | from .session import PatchedScopedSession, Session 9 | 10 | 11 | __all__ = ("SQLAlchemy", "TestTransaction") 12 | 13 | 14 | class SQLAlchemy: 15 | """Create a SQLAlchemy connection 16 | 17 | This class creates an engine, a base class for your models, and a scoped session. 18 | 19 | The string form of the URL is 20 | `dialect[+driver]://user:password@host/dbname[?key=value..]`, 21 | where dialect is a database name such as mysql, postgresql, etc., and driver the 22 | name of a DBAPI, such as psycopg2, pyodbc, etc. 23 | 24 | Instead of the connection URL you can also specify dialect (plus optional driver), 25 | user, password, host, port, and database name as separate arguments. 26 | 27 | Please review the 28 | [Database URLs](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls) 29 | section of the SQLAlchemy documentation, for general guidelines in composing 30 | URL strings. In particular, special characters, such as those often part of 31 | passwords, must be URL-encoded to be properly parsed. 32 | 33 | Example: 34 | 35 | ```python 36 | db = SQLAlchemy(database_uri) 37 | # or SQLAlchemy(dialect=, name= [, user=] [, password=] [, host=] [, port=]) 38 | 39 | class Base(db.Model): 40 | pass 41 | 42 | class User(Base): 43 | __tablename__ = "users" 44 | id: Mapped[int] = mapped_column(primary_key=True) 45 | name: Mapped[str] = mapped_column(sa.String(80), unique=True) 46 | deleted: Mapped[datetime] = mapped_column(sa.DateTime) 47 | ``` 48 | 49 | """ 50 | 51 | def __init__( 52 | self, 53 | url: "str | None" = None, 54 | *, 55 | dialect: str = "sqlite", 56 | name: "str | None" = None, 57 | user: "str | None" = None, 58 | password: "str | None" = None, 59 | host: "str | None" = None, 60 | port: "str | int | None" = None, 61 | engine_options: "t.Dict[str, t.Any] | None" = None, 62 | session_options: "t.Dict[str, t.Any] | None" = None, 63 | base_model_class: t.Any = BaseModel, 64 | base_model_metaclass: t.Any = sa_orm.DeclarativeMeta, 65 | ) -> None: 66 | self.url = url or self._make_url( 67 | dialect=dialect, 68 | host=host, 69 | name=name, 70 | user=user, 71 | password=password, 72 | port=port, 73 | ) 74 | engine_options = engine_options or {} 75 | engine_options.setdefault("future", True) 76 | self.engine = sa.create_engine(self.url, **engine_options) 77 | 78 | self.registry = sa_orm.registry() 79 | self.Model = self.registry.generate_base( 80 | cls=base_model_class, 81 | name="Model", 82 | metaclass=base_model_metaclass 83 | ) 84 | 85 | session_options = session_options or {} 86 | session_options.setdefault("class_", Session) 87 | session_options.setdefault("bind", self.engine) 88 | session_options.setdefault("future", True) 89 | self.session_class = session_options["class_"] 90 | self.Session = sa_orm.sessionmaker(**session_options) 91 | self.s = PatchedScopedSession(self.Session) 92 | 93 | def create_all(self, **kwargs) -> None: 94 | """Creates all the tables of the models registered so far. 95 | 96 | Only tables that do not already exist are created. Existing tables are 97 | not modified. 98 | """ 99 | kwargs.setdefault("bind", self.engine) 100 | self.registry.metadata.create_all(**kwargs) 101 | 102 | def drop_all(self, **kwargs) -> None: 103 | """Drop all the database tables. 104 | 105 | Note that this is a destructive operation; data stored in the 106 | database will be deleted when this method is called. 107 | """ 108 | kwargs.setdefault("bind", self.engine) 109 | self.registry.metadata.drop_all(**kwargs) 110 | 111 | def test_transaction(self, savepoint: bool = False) -> "TestTransaction": 112 | return TestTransaction(self, savepoint=savepoint) 113 | 114 | def _make_url( 115 | self, 116 | dialect: str, 117 | *, 118 | user: "str | None" = None, 119 | password: "str | None" = None, 120 | host: "str | None" = None, 121 | port: "str | int | None" = None, 122 | name: "str | None" = None, 123 | ) -> str: 124 | url_parts = [f"{dialect}://"] 125 | if user: 126 | url_parts.append(user) 127 | if password: 128 | url_parts.append(f":{password}") 129 | url_parts.append("@") 130 | 131 | if not host and not dialect.startswith("sqlite"): 132 | host = "127.0.0.1" 133 | 134 | if host: 135 | url_parts.append(f"{host}") 136 | if port: 137 | url_parts.append(f":{port}") 138 | 139 | if name: 140 | url_parts.append(f"/{name}") 141 | return "".join(url_parts) 142 | 143 | def __repr__(self) -> str: 144 | return f"" 145 | 146 | 147 | class TestTransaction: 148 | """Helper for building sessions that rollback everyting at the end. 149 | 150 | See ["Joining a Session into an External Transaction"](https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#session-external-transaction) 151 | in the SQLAlchemy documentation. 152 | """ 153 | def __init__(self, db: SQLAlchemy, savepoint: bool = False) -> None: 154 | self.connection = db.engine.connect() 155 | self.trans = self.connection.begin() 156 | self.session = db.Session(bind=self.connection) 157 | db.s.registry.set(self.session) 158 | 159 | if savepoint: # pragma: no branch 160 | # if the database supports SAVEPOINT (SQLite needs a 161 | # special config for this to work), starting a savepoint 162 | # will allow tests to also use rollback within tests 163 | self.nested = self.connection.begin_nested() 164 | 165 | @sa_listens_for(target=self.session, identifier="after_transaction_end") 166 | def end_savepoint(session, transaction): 167 | if not self.nested.is_active: 168 | self.nested = self.connection.begin_nested() 169 | 170 | def close(self) -> None: 171 | self.session.close() 172 | self.trans.rollback() 173 | self.connection.close() 174 | 175 | def __enter__(self) -> "TestTransaction": # pragma: no cover 176 | return self 177 | 178 | def __exit__(self, exc_type, exc_val, exc_tb) -> None: # pragma: no cover 179 | self.close() 180 | -------------------------------------------------------------------------------- /docs/docs/testing-with-a-real-database.md: -------------------------------------------------------------------------------- 1 | # Testing with a real database 2 | 3 | "Testing with a real database, was not that supposed to be a no-no?" - you might be thinking. Many tutorials have always reccomended mocking your database so your unit-tests are fast. 4 | 5 | More often than not, thiugh, that is bad advice. For databases like PostgreSQL or MySQLfor whatever short-term gains you might get, you lose in long-term maintainability. 6 | 7 | Mocking your database is not only a lot of overhead but, after a while, the mocks no longer reflect the actual code and what is happening on the real database server. 8 | 9 | We need each test to be isolated from others: you should never have to think about what other tests have put in the database. However, creating tables, loading fixtures, and dropping those tables for each test is too slow. The good news is, you don't need to do it for each test. You will not have to worry about database performance issues if you follow this setup. 10 | 11 | 12 | ## 1. Manually create an empty database for testing 13 | 14 | First, you need to manually create a new (and empty) database for your tests to use. This could mean manually running a command like `createdb myapp-tests` or adding another database service in your development `docker-compose.yml` file. 15 | 16 | You should use the same type of database as the one used by your application. Otherwise, your tests could be affected by implementation details of the database engines, like the default order of null values, and not being able to test what would happen in production. 17 | 18 | 19 | ## 2. Prepare the database before running the test suite 20 | 21 | The test database exists but is empty at this point, so before we run any test, we need to create all the tables and insert the minimal fixture data necessary to run the application. 22 | 23 | We must configure our tests to do this every time we run the tests, but only once. There is no need to run any migrations because we can create all the tables from the models definition. You can however "stamp" it with the latest revision if some test expects the `alembic_version` table to exists. 24 | 25 | ### pytest 26 | 27 | With **pytest** this can be done with a session-scoped fixture: 28 | 29 | ```python 30 | # conftest.py 31 | import pytest 32 | 33 | from myapp.models import db, alembic, load_fixture_data 34 | 35 | @pytest.fixture(scope="session") 36 | def dbsetup(): 37 | assert "_test" in db.url # better to be safe than sorry 38 | db.create_all() 39 | alembic.stamp() # optional 40 | load_fixture_data() # optional 41 | yield 42 | db.drop_all() 43 | 44 | ``` 45 | 46 | ### unittest 47 | 48 | With **unitest**, we do it with the `setUpClass` and `tearDownClass` class methods in a base class. Other test cases must inherit from this class to make it work. 49 | 50 | ```python 51 | # base.py 52 | import unittest 53 | 54 | from myapp.models import db, alembic, load_fixture_data 55 | 56 | class DBTestCase(unittest.TestCase): 57 | @classmethod 58 | def setUpClass(cls): 59 | assert "_test" in db.url # better to be safe than sorry 60 | db.create_all() 61 | alembic.stamp() # optional 62 | load_fixture_data() # optional 63 | 64 | @classmethod 65 | def tearDownClass(cls): 66 | db.drop_all() 67 | 68 | ``` 69 | 70 | 71 | ## 3. Run each test inside a transaction and rollback at the end 72 | 73 | This part is the main trick, and **sqla-wrapper** includes a `db.test_transaction()` method that does it for you: 74 | 75 | ```python 76 | trans = db.test_transaction() 77 | ... 78 | trans.close() 79 | ``` 80 | 81 | By wrapping each test in a root transaction, we make sure that it will not alter the state of the database, even if we commit during the test since the transaction is rolled back on test teardown. 82 | 83 | There is only one caveat: if you want to allow tests to also use rollbacks within them, your database must support SAVEPOINT. 84 | 85 | !!! Note 86 | 87 | `test_transaction()` internally follow this recipe: 88 | 89 | ```python 90 | # 1. Start a transaction on a new connection 91 | connection = db.engine.connect() 92 | trans = connection.begin() 93 | 94 | # 2. Bind a new session to this connection 95 | session = db.Session(bind=connection) 96 | # and registers it as the new scoped session 97 | 98 | # 3. If your database supports it, start a savepoint to allow rollbacks within a test 99 | nested = connection.begin_nested() 100 | 101 | @event.listens_for(session, "after_transaction_end") 102 | def end_savepoint(session, transaction): 103 | if not nested.is_active: 104 | nested = connection.begin_nested() 105 | 106 | # 4. Run the test 107 | # ... 108 | 109 | # 5. Finally, rollback any changes and close the connection. 110 | session.close() 111 | trans.rollback() 112 | connection.close() 113 | ``` 114 | 115 | This recipe is what [SQLAlchemy documentation](https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#session-external-transaction) recommends and even test in their own CI to ensure that it remains working as expected. 116 | 117 | 118 | ### pytest 119 | 120 | With **pytest**, we can use a regular fixture and make it *depend* on the `dbsetup()` fixture we created before. Although this new fixture will run (automatically) before each test, the setup fixture will run only once, as we need to. 121 | 122 | ```python 123 | # conftest.py 124 | import pytest 125 | 126 | from myapp.models import db, alembic, load_fixture_data 127 | 128 | @pytest.fixture(scope="session") 129 | def dbsetup(): 130 | # ... 131 | 132 | @pytest.fixture() 133 | def dbs(dbsetup): 134 | trans = db.test_transaction() 135 | # Or, if you need to rollback in your tests 136 | # = db.test_transaction(savepoint=True) 137 | yield 138 | trans.close() 139 | 140 | ``` 141 | 142 | Then, we use that database session fixture for the tests that require interacting with the database. You can safely use the "global" scoped session, because it nows point to the test session. 143 | 144 | ```python 145 | # test_something.py 146 | from myapp.models import db 147 | 148 | def test_something(dbs): 149 | db.s.execute(some_stmt) 150 | db.s.commit() 151 | ... 152 | 153 | ``` 154 | 155 | ### unittest 156 | 157 | With **unittest** we use the `setUp()` and `tearDown()` methods 158 | 159 | ```python 160 | # base.py 161 | import unittest 162 | 163 | from myapp.models import db, alembic, load_fixture_data 164 | 165 | class DBTestCase(unittest.TestCase): 166 | @classmethod 167 | def setUpClass(cls): 168 | # ... 169 | 170 | @classmethod 171 | def tearDownClass(cls): 172 | # ... 173 | 174 | def setUp(self): 175 | self._trans = db.test_transaction() 176 | # Or, if you need to rollback in your tests 177 | # = db.test_transaction(savepoint=True) 178 | 179 | def tearDown(self): 180 | self._trans.close() 181 | 182 | # ... 183 | 184 | ``` 185 | 186 | Then, we inherit from that class for the tests that require interacting with the database. 187 | 188 | ```python 189 | # test_something.py 190 | from myapp.models import db 191 | from .base import DBTestCase 192 | 193 | class TestSomething(DBTestCase): 194 | def test_something(self): 195 | db.s.execute(some_stmt) 196 | db.s.commit() 197 | # ... 198 | 199 | ``` 200 | 201 | ## API 202 | 203 | ::: sqla_wrapper.TestTransaction 204 | options: 205 | heading_level: 3 206 | filter: 207 | -------------------------------------------------------------------------------- /docs/docs/assets/img/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/old/5.x/assets/img/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/docs/assets/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/old/5.x/assets/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/old/5.x/assets/stylesheets/palette.3f5d1f46.min.css: -------------------------------------------------------------------------------- 1 | [data-md-color-accent=red]{--md-accent-fg-color:#ff1947;--md-accent-fg-color--transparent:rgba(255,25,71,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=pink]{--md-accent-fg-color:#f50056;--md-accent-fg-color--transparent:rgba(245,0,86,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=purple]{--md-accent-fg-color:#df41fb;--md-accent-fg-color--transparent:rgba(223,65,251,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=deep-purple]{--md-accent-fg-color:#7c4dff;--md-accent-fg-color--transparent:rgba(124,77,255,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=indigo]{--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:rgba(82,108,254,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=blue]{--md-accent-fg-color:#4287ff;--md-accent-fg-color--transparent:rgba(66,135,255,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=light-blue]{--md-accent-fg-color:#0091eb;--md-accent-fg-color--transparent:rgba(0,145,235,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=cyan]{--md-accent-fg-color:#00bad6;--md-accent-fg-color--transparent:rgba(0,186,214,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=teal]{--md-accent-fg-color:#00bda4;--md-accent-fg-color--transparent:rgba(0,189,164,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=green]{--md-accent-fg-color:#00c753;--md-accent-fg-color--transparent:rgba(0,199,83,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=light-green]{--md-accent-fg-color:#63de17;--md-accent-fg-color--transparent:rgba(99,222,23,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-accent=lime]{--md-accent-fg-color:#b0eb00;--md-accent-fg-color--transparent:rgba(176,235,0,0.1);--md-accent-bg-color:rgba(0,0,0,0.87);--md-accent-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-accent=yellow]{--md-accent-fg-color:#ffd500;--md-accent-fg-color--transparent:rgba(255,213,0,0.1);--md-accent-bg-color:rgba(0,0,0,0.87);--md-accent-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-accent=amber]{--md-accent-fg-color:#fa0;--md-accent-fg-color--transparent:rgba(255,170,0,0.1);--md-accent-bg-color:rgba(0,0,0,0.87);--md-accent-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-accent=orange]{--md-accent-fg-color:#ff9100;--md-accent-fg-color--transparent:rgba(255,145,0,0.1);--md-accent-bg-color:rgba(0,0,0,0.87);--md-accent-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-accent=deep-orange]{--md-accent-fg-color:#ff6e42;--md-accent-fg-color--transparent:rgba(255,110,66,0.1);--md-accent-bg-color:#fff;--md-accent-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=red]{--md-primary-fg-color:#ef5552;--md-primary-fg-color--light:#e57171;--md-primary-fg-color--dark:#e53734;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=pink]{--md-primary-fg-color:#e92063;--md-primary-fg-color--light:#ec417a;--md-primary-fg-color--dark:#c3185d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=purple]{--md-primary-fg-color:#ab47bd;--md-primary-fg-color--light:#bb69c9;--md-primary-fg-color--dark:#8c24a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=deep-purple]{--md-primary-fg-color:#7e56c2;--md-primary-fg-color--light:#9574cd;--md-primary-fg-color--dark:#673ab6;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=indigo]{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=blue]{--md-primary-fg-color:#2094f3;--md-primary-fg-color--light:#42a5f5;--md-primary-fg-color--dark:#1975d2;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=light-blue]{--md-primary-fg-color:#02a6f2;--md-primary-fg-color--light:#28b5f6;--md-primary-fg-color--dark:#0287cf;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=cyan]{--md-primary-fg-color:#00bdd6;--md-primary-fg-color--light:#25c5da;--md-primary-fg-color--dark:#0097a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=teal]{--md-primary-fg-color:#009485;--md-primary-fg-color--light:#26a699;--md-primary-fg-color--dark:#007a6c;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=green]{--md-primary-fg-color:#4cae4f;--md-primary-fg-color--light:#68bb6c;--md-primary-fg-color--dark:#398e3d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=light-green]{--md-primary-fg-color:#8bc34b;--md-primary-fg-color--light:#9ccc66;--md-primary-fg-color--dark:#689f38;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=lime]{--md-primary-fg-color:#cbdc38;--md-primary-fg-color--light:#d3e156;--md-primary-fg-color--dark:#b0b52c;--md-primary-bg-color:rgba(0,0,0,0.87);--md-primary-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-primary=yellow]{--md-primary-fg-color:#ffec3d;--md-primary-fg-color--light:#ffee57;--md-primary-fg-color--dark:#fbc02d;--md-primary-bg-color:rgba(0,0,0,0.87);--md-primary-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-primary=amber]{--md-primary-fg-color:#ffc105;--md-primary-fg-color--light:#ffc929;--md-primary-fg-color--dark:#ffa200;--md-primary-bg-color:rgba(0,0,0,0.87);--md-primary-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-primary=orange]{--md-primary-fg-color:#ffa724;--md-primary-fg-color--light:#ffa724;--md-primary-fg-color--dark:#fa8900;--md-primary-bg-color:rgba(0,0,0,0.87);--md-primary-bg-color--light:rgba(0,0,0,0.54)}[data-md-color-primary=deep-orange]{--md-primary-fg-color:#ff6e42;--md-primary-fg-color--light:#ff8a66;--md-primary-fg-color--dark:#f4511f;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=brown]{--md-primary-fg-color:#795649;--md-primary-fg-color--light:#8d6e62;--md-primary-fg-color--dark:#5d4037;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=grey]{--md-primary-fg-color:#757575;--md-primary-fg-color--light:#9e9e9e;--md-primary-fg-color--dark:#616161;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=blue-grey]{--md-primary-fg-color:#546d78;--md-primary-fg-color--light:#607c8a;--md-primary-fg-color--dark:#455a63;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7)}[data-md-color-primary=white]{--md-primary-fg-color:#fff;--md-primary-fg-color--light:hsla(0,0%,100%,0.7);--md-primary-fg-color--dark:rgba(0,0,0,0.07);--md-primary-bg-color:rgba(0,0,0,0.87);--md-primary-bg-color--light:rgba(0,0,0,0.54);--md-typeset-a-color:#4051b5}@media screen and (min-width:60em){[data-md-color-primary=white] .md-search__form{background-color:rgba(0,0,0,.07)}[data-md-color-primary=white] .md-search__form:hover{background-color:rgba(0,0,0,.32)}[data-md-color-primary=white] .md-search__input+.md-search__icon{color:rgba(0,0,0,.87)}}@media screen and (min-width:76.25em){[data-md-color-primary=white] .md-tabs{border-bottom:.05rem solid rgba(0,0,0,.07)}}[data-md-color-primary=black]{--md-primary-fg-color:#000;--md-primary-fg-color--light:rgba(0,0,0,0.54);--md-primary-fg-color--dark:#000;--md-primary-bg-color:#fff;--md-primary-bg-color--light:hsla(0,0%,100%,0.7);--md-typeset-a-color:#4051b5}[data-md-color-primary=black] .md-header{background-color:#000}@media screen and (max-width:59.9375em){[data-md-color-primary=black] .md-nav__source{background-color:rgba(0,0,0,.87)}}@media screen and (min-width:60em){[data-md-color-primary=black] .md-search__form{background-color:hsla(0,0%,100%,.12)}[data-md-color-primary=black] .md-search__form:hover{background-color:hsla(0,0%,100%,.3)}}@media screen and (max-width:76.1875em){html [data-md-color-primary=black] .md-nav--primary .md-nav__title[for=__drawer]{background-color:#000}}@media screen and (min-width:76.25em){[data-md-color-primary=black] .md-tabs{background-color:#000}}@media screen{[data-md-color-scheme=slate]{--md-hue:232;--md-default-fg-color:hsla(var(--md-hue),75%,95%,1);--md-default-fg-color--light:hsla(var(--md-hue),75%,90%,0.62);--md-default-fg-color--lighter:hsla(var(--md-hue),75%,90%,0.32);--md-default-fg-color--lightest:hsla(var(--md-hue),75%,90%,0.12);--md-default-bg-color:hsla(var(--md-hue),15%,21%,1);--md-default-bg-color--light:hsla(var(--md-hue),15%,21%,0.54);--md-default-bg-color--lighter:hsla(var(--md-hue),15%,21%,0.26);--md-default-bg-color--lightest:hsla(var(--md-hue),15%,21%,0.07);--md-code-fg-color:hsla(var(--md-hue),18%,86%,1);--md-code-bg-color:hsla(var(--md-hue),15%,15%,1);--md-code-hl-color:rgba(66,135,255,0.15);--md-code-hl-number-color:#e6695b;--md-code-hl-special-color:#f06090;--md-code-hl-function-color:#c973d9;--md-code-hl-constant-color:#9383e2;--md-code-hl-keyword-color:#6791e0;--md-code-hl-string-color:#2fb170;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-mark-color:rgba(66,135,255,0.3);--md-typeset-kbd-color:hsla(var(--md-hue),15%,94%,0.12);--md-typeset-kbd-accent-color:hsla(var(--md-hue),15%,94%,0.2);--md-typeset-kbd-border-color:hsla(var(--md-hue),15%,14%,1);--md-typeset-table-color:hsla(var(--md-hue),75%,95%,0.12);--md-admonition-bg-color:hsla(var(--md-hue),0%,100%,0.025);--md-footer-bg-color:hsla(var(--md-hue),15%,12%,0.87);--md-footer-bg-color--dark:hsla(var(--md-hue),15%,10%,1)}[data-md-color-scheme=slate][data-md-color-primary=black],[data-md-color-scheme=slate][data-md-color-primary=white]{--md-typeset-a-color:#5d6cc0}} 2 | /*# sourceMappingURL=palette.3f5d1f46.min.css.map */ -------------------------------------------------------------------------------- /tests/test_alembic_wrapper.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sqlalchemy as sa 3 | from sqlalchemy.orm import Mapped, mapped_column 4 | from sqla_wrapper import Alembic 5 | 6 | 7 | def _create_test_model1(memdb): 8 | class TestModel1(memdb.Model): 9 | __tablename__ = "test_model_1" 10 | id: Mapped[int] = mapped_column(primary_key=True) 11 | name: Mapped[str] = mapped_column(sa.String(50), nullable=False) 12 | 13 | return TestModel1 14 | 15 | 16 | def _create_test_model2(memdb): 17 | class TestModel2(memdb.Model): 18 | __tablename__ = "test_model_2" 19 | id: Mapped[int] = mapped_column(primary_key=True) 20 | name: Mapped[str] = mapped_column(sa.String(50), nullable=False) 21 | 22 | return TestModel2 23 | 24 | 25 | def test_autoinit(memdb, dst): 26 | path = dst / "migrations" 27 | Alembic(memdb, path=path) 28 | assert path.is_dir() 29 | assert (path / "script.py.mako").is_file() 30 | 31 | 32 | def test_autoinit_exists(memdb, dst): 33 | path = dst / "migrations" 34 | path.mkdir() 35 | tmpl_path = path / "script.py.mako" 36 | tmpl_path.touch() 37 | 38 | Alembic(memdb, path=path) 39 | assert path.is_dir() 40 | assert tmpl_path.is_file() 41 | 42 | 43 | def test_revision(memdb, dst): 44 | _create_test_model1(memdb) 45 | alembic = Alembic( 46 | memdb, path=dst, file_template="%%(rev)s_%%(slug)s" 47 | ) 48 | alembic.rev_id = lambda: "1234" 49 | alembic.revision("test") 50 | 51 | revpath = dst / "1234_test.py" 52 | assert revpath.is_file() 53 | rev_src = revpath.read_text() 54 | assert "revision = '1234'" in rev_src 55 | assert "op.create_table" in rev_src 56 | 57 | 58 | def test_empty_revision(memdb, dst): 59 | _create_test_model1(memdb) 60 | alembic = Alembic( 61 | memdb, path=dst, file_template="%%(rev)s_%%(slug)s" 62 | ) 63 | alembic.rev_id = lambda: "1234" 64 | alembic.revision("test", empty=True) 65 | 66 | revpath = dst / "1234_test.py" 67 | rev_src = revpath.read_text() 68 | assert "op.create_table" not in rev_src 69 | 70 | 71 | def test_upgrade(memdb, dst): 72 | _create_test_model1(memdb) 73 | alembic = Alembic(memdb, path=dst) 74 | rev1 = alembic.revision("test1") 75 | 76 | assert alembic.get_current() is None 77 | alembic.upgrade() 78 | assert alembic.get_current() == rev1 79 | 80 | 81 | def test_upgrade_sql(memdb, dst, capsys): 82 | _create_test_model1(memdb) 83 | alembic = Alembic(memdb, path=dst) 84 | alembic.revision("test1") 85 | alembic.upgrade(":head", sql=True) 86 | 87 | stdout, _ = capsys.readouterr() 88 | assert "CREATE TABLE test_model_1" in stdout 89 | 90 | 91 | def test_upgrade_range_no_sql(memdb, dst): 92 | _create_test_model1(memdb) 93 | alembic = Alembic(memdb, path=dst) 94 | alembic.revision("test1") 95 | 96 | with pytest.raises(ValueError): 97 | alembic.upgrade(":head", sql=False) 98 | 99 | 100 | def test_upgrade_sql_no_range(memdb, dst): 101 | _create_test_model1(memdb) 102 | alembic = Alembic(memdb, path=dst) 103 | alembic.revision("test1") 104 | 105 | with pytest.raises(ValueError): 106 | alembic.upgrade("head", sql=True) 107 | 108 | 109 | def test_downgrade(memdb, dst): 110 | _create_test_model1(memdb) 111 | alembic = Alembic(memdb, path=dst) 112 | rev1 = alembic.revision("test1") 113 | alembic.upgrade() 114 | _create_test_model2(memdb) 115 | rev2 = alembic.revision("test2") 116 | alembic.upgrade() 117 | 118 | assert alembic.get_current() == rev2 119 | alembic.downgrade() 120 | assert alembic.get_current() == rev1 121 | alembic.upgrade() 122 | alembic.downgrade(-2) 123 | assert alembic.get_current() is None 124 | alembic.upgrade() 125 | assert alembic.get_current() == rev2 126 | alembic.downgrade(1) 127 | assert alembic.get_current() == rev1 128 | 129 | 130 | def test_downgrade_sql(memdb, dst, capsys): 131 | _create_test_model1(memdb) 132 | alembic = Alembic(memdb, path=dst) 133 | rev1 = alembic.revision("test1") 134 | alembic.upgrade() 135 | alembic.downgrade(f"{rev1.revision}:-1", sql=True) 136 | 137 | stdout, _ = capsys.readouterr() 138 | assert "DROP TABLE test_model_1" in stdout 139 | 140 | 141 | def test_downgrade_range_no_sql(memdb, dst): 142 | _create_test_model1(memdb) 143 | alembic = Alembic(memdb, path=dst) 144 | rev1 = alembic.revision("test1") 145 | alembic.upgrade() 146 | 147 | with pytest.raises(ValueError): 148 | alembic.downgrade(f"{rev1.revision}:-1", sql=False) 149 | 150 | 151 | def test_downgrade_sql_no_range(memdb, dst): 152 | _create_test_model1(memdb) 153 | alembic = Alembic(memdb, path=dst) 154 | alembic.revision("test1") 155 | alembic.upgrade() 156 | 157 | with pytest.raises(ValueError): 158 | alembic.downgrade("-1", sql=True) 159 | 160 | 161 | def test_get_history(memdb, dst): 162 | _create_test_model1(memdb) 163 | alembic = Alembic(memdb, path=dst) 164 | rev1 = alembic.revision("test1") 165 | alembic.upgrade() 166 | _create_test_model2(memdb) 167 | rev2 = alembic.revision("test2") 168 | 169 | print("rev1, rev2", rev1, rev2) 170 | assert alembic.get_history() == [rev1, rev2] 171 | assert alembic.get_history(end="current") == [rev1] 172 | alembic.upgrade() 173 | assert alembic.get_history(start="current") == [rev2] 174 | 175 | 176 | def test_print_history(memdb, dst, capsys): 177 | _create_test_model1(memdb) 178 | alembic = Alembic(memdb, path=dst) 179 | rev1 = alembic.revision("test1") 180 | alembic.upgrade() 181 | _create_test_model2(memdb) 182 | rev2 = alembic.revision("test2") 183 | 184 | alembic.history() 185 | stdout, _ = capsys.readouterr() 186 | assert f" -> {rev1.revision}, test1\n" in stdout 187 | assert f"{rev1.revision} -> {rev2.revision} (head), test2" in stdout 188 | 189 | 190 | def test_print_history_verbose(memdb, dst, capsys): 191 | _create_test_model1(memdb) 192 | alembic = Alembic(memdb, path=dst) 193 | rev1 = alembic.revision("test1") 194 | alembic.upgrade() 195 | _create_test_model2(memdb) 196 | rev2 = alembic.revision("test2") 197 | 198 | alembic.history(verbose=True) 199 | stdout, _ = capsys.readouterr() 200 | assert f"Rev: {rev1.revision}\nParent: \n" in stdout 201 | assert f"Rev: {rev2.revision} (head)\nParent: {rev1.revision}\n" in stdout 202 | 203 | 204 | def test_stamp(memdb, dst): 205 | _create_test_model1(memdb) 206 | alembic = Alembic(memdb, path=dst) 207 | rev1 = alembic.revision("test1") 208 | alembic.stamp() 209 | assert alembic.get_current() == rev1 210 | 211 | 212 | def test_stamp_sql(memdb, dst, capsys): 213 | _create_test_model1(memdb) 214 | alembic = Alembic(memdb, path=dst) 215 | rev1 = alembic.revision("test1") 216 | alembic.stamp(sql=True) 217 | 218 | stdout, _ = capsys.readouterr() 219 | print(stdout) 220 | stmt = f"INSERT INTO alembic_version (version_num) VALUES ('{rev1.revision}')" 221 | assert stmt in stdout 222 | assert "CREATE TABLE test_model_1" not in stdout 223 | 224 | 225 | def test_print_current(memdb, dst, capsys): 226 | _create_test_model1(memdb) 227 | alembic = Alembic(memdb, path=dst) 228 | rev1 = alembic.revision("test1") 229 | alembic.upgrade() 230 | _create_test_model2(memdb) 231 | alembic.revision("test2") 232 | 233 | alembic.current() 234 | stdout, _ = capsys.readouterr() 235 | assert f" -> {rev1.revision}, test1\n" in stdout 236 | 237 | 238 | def test_print_current_verbose(memdb, dst, capsys): 239 | _create_test_model1(memdb) 240 | alembic = Alembic(memdb, path=dst) 241 | rev1 = alembic.revision("test1") 242 | alembic.upgrade() 243 | _create_test_model2(memdb) 244 | alembic.revision("test2") 245 | 246 | alembic.current(verbose=True) 247 | stdout, _ = capsys.readouterr() 248 | assert f"Rev: {rev1.revision}\nParent: \n" in stdout 249 | 250 | 251 | def test_no_current(memdb, dst, capsys): 252 | alembic = Alembic(memdb, path=dst) 253 | alembic.current() 254 | stdout, _ = capsys.readouterr() 255 | assert not stdout 256 | 257 | 258 | def test_get_head(memdb, dst): 259 | _create_test_model1(memdb) 260 | alembic = Alembic(memdb, path=dst) 261 | alembic.revision("test1") 262 | alembic.upgrade() 263 | _create_test_model2(memdb) 264 | rev2 = alembic.revision("test2") 265 | 266 | assert alembic.get_head() == rev2 267 | 268 | 269 | def test_print_head(memdb, dst, capsys): 270 | _create_test_model1(memdb) 271 | alembic = Alembic(memdb, path=dst) 272 | rev1 = alembic.revision("test1") 273 | alembic.upgrade() 274 | _create_test_model2(memdb) 275 | rev2 = alembic.revision("test2") 276 | 277 | alembic.head() 278 | stdout, _ = capsys.readouterr() 279 | assert f"{rev1.revision} -> {rev2.revision} (head), test2" in stdout 280 | 281 | 282 | def test_print_head_verbose(memdb, dst, capsys): 283 | _create_test_model1(memdb) 284 | alembic = Alembic(memdb, path=dst) 285 | rev1 = alembic.revision("test1") 286 | alembic.upgrade() 287 | _create_test_model2(memdb) 288 | rev2 = alembic.revision("test2") 289 | 290 | alembic.head(verbose=True) 291 | stdout, _ = capsys.readouterr() 292 | assert f"Rev: {rev2.revision} (head)\nParent: {rev1.revision}\n" in stdout 293 | 294 | 295 | def test_no_head(memdb, dst, capsys): 296 | alembic = Alembic(memdb, path=dst) 297 | alembic.head() 298 | stdout, _ = capsys.readouterr() 299 | assert not stdout 300 | 301 | 302 | def test_create_all(memdb, dst): 303 | path = dst / "migrations" 304 | path.mkdir() 305 | Model = _create_test_model1(memdb) 306 | alembic = Alembic(memdb, path) 307 | rev1 = alembic.revision("test") 308 | alembic.create_all() 309 | 310 | with memdb.Session() as session: 311 | session.execute(sa.select(Model)).all() 312 | assert alembic.get_current() == rev1 313 | 314 | 315 | def test_get_proper_cli(memdb, dst): 316 | alembic = Alembic(memdb, path=dst) 317 | alembic.get_proper_cli() 318 | 319 | 320 | def test_get_click_cli(memdb, dst, capsys): 321 | alembic = Alembic(memdb, path=dst) 322 | cli = alembic.get_click_cli() 323 | 324 | cli(args=["--help"], prog_name="cli", standalone_mode=False) 325 | stdout, _ = capsys.readouterr() 326 | assert " current " in stdout 327 | assert " downgrade " in stdout 328 | assert " head " in stdout 329 | assert " history " in stdout 330 | assert " init " in stdout 331 | assert " revision " in stdout 332 | assert " stamp " in stdout 333 | assert " upgrade " in stdout 334 | 335 | 336 | def test_get_flask_cli(memdb, dst, capsys): 337 | alembic = Alembic(memdb, path=dst) 338 | cli = alembic.get_flask_cli() 339 | 340 | cli(args=["--help"], prog_name="cli", standalone_mode=False) 341 | stdout, _ = capsys.readouterr() 342 | assert " current " in stdout 343 | assert " downgrade " in stdout 344 | assert " head " in stdout 345 | assert " history " in stdout 346 | assert " init " in stdout 347 | assert " revision " in stdout 348 | assert " stamp " in stdout 349 | assert " upgrade " in stdout 350 | -------------------------------------------------------------------------------- /docs/docs/assets/css/highlight.css: -------------------------------------------------------------------------------- 1 | [data-md-color-scheme="default"] .highlight .hll { background-color: #ffffcc } 2 | [data-md-color-scheme="default"] .highlight .c { color: #808080 } /* Comment */ 3 | [data-md-color-scheme="default"] .highlight .err { color: #F00000; background-color: #F0A0A0 } /* Error */ 4 | [data-md-color-scheme="default"] .highlight .k { color: #008000; font-weight: bold } /* Keyword */ 5 | [data-md-color-scheme="default"] .highlight .o { color: #303030 } /* Operator */ 6 | [data-md-color-scheme="default"] .highlight .cm { color: #808080 } /* Comment.Multiline */ 7 | [data-md-color-scheme="default"] .highlight .cp { color: #507090 } /* Comment.Preproc */ 8 | [data-md-color-scheme="default"] .highlight .c1 { color: #808080 } /* Comment.Single */ 9 | [data-md-color-scheme="default"] .highlight .cs { color: #cc0000; font-weight: bold } /* Comment.Special */ 10 | [data-md-color-scheme="default"] .highlight .gd { color: #A00000 } /* Generic.Deleted */ 11 | [data-md-color-scheme="default"] .highlight .ge { font-style: italic } /* Generic.Emph */ 12 | [data-md-color-scheme="default"] .highlight .gr { color: #FF0000 } /* Generic.Error */ 13 | [data-md-color-scheme="default"] .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 14 | [data-md-color-scheme="default"] .highlight .gi { color: #00A000 } /* Generic.Inserted */ 15 | [data-md-color-scheme="default"] .highlight .go { color: #808080 } /* Generic.Output */ 16 | [data-md-color-scheme="default"] .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 17 | [data-md-color-scheme="default"] .highlight .gs { font-weight: bold } /* Generic.Strong */ 18 | [data-md-color-scheme="default"] .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 19 | [data-md-color-scheme="default"] .highlight .gt { color: #0040D0 } /* Generic.Traceback */ 20 | [data-md-color-scheme="default"] .highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ 21 | [data-md-color-scheme="default"] .highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ 22 | [data-md-color-scheme="default"] .highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ 23 | [data-md-color-scheme="default"] .highlight .kp { color: #003080; font-weight: bold } /* Keyword.Pseudo */ 24 | [data-md-color-scheme="default"] .highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ 25 | [data-md-color-scheme="default"] .highlight .kt { color: #303090; font-weight: bold } /* Keyword.Type */ 26 | [data-md-color-scheme="default"] .highlight .m { color: #6000E0; font-weight: bold } /* Literal.Number */ 27 | [data-md-color-scheme="default"] .highlight .s { background-color: #fff0f0 } /* Literal.String */ 28 | [data-md-color-scheme="default"] .highlight .na { color: #0000C0 } /* Name.Attribute */ 29 | [data-md-color-scheme="default"] .highlight .nb { color: #007020 } /* Name.Builtin */ 30 | [data-md-color-scheme="default"] .highlight .nc { color: #B00060; font-weight: bold } /* Name.Class */ 31 | [data-md-color-scheme="default"] .highlight .no { color: #003060; font-weight: bold } /* Name.Constant */ 32 | [data-md-color-scheme="default"] .highlight .nd { color: #505050; font-weight: bold } /* Name.Decorator */ 33 | [data-md-color-scheme="default"] .highlight .ni { color: #800000; font-weight: bold } /* Name.Entity */ 34 | [data-md-color-scheme="default"] .highlight .ne { color: #F00000; font-weight: bold } /* Name.Exception */ 35 | [data-md-color-scheme="default"] .highlight .nf { color: #0060B0; font-weight: bold } /* Name.Function */ 36 | [data-md-color-scheme="default"] .highlight .nl { color: #907000; font-weight: bold } /* Name.Label */ 37 | [data-md-color-scheme="default"] .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 38 | [data-md-color-scheme="default"] .highlight .nt { color: #007000 } /* Name.Tag */ 39 | [data-md-color-scheme="default"] .highlight .nv { color: #906030 } /* Name.Variable */ 40 | [data-md-color-scheme="default"] .highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */ 41 | [data-md-color-scheme="default"] .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 42 | [data-md-color-scheme="default"] .highlight .mf { color: #6000E0; font-weight: bold } /* Literal.Number.Float */ 43 | [data-md-color-scheme="default"] .highlight .mh { color: #005080; font-weight: bold } /* Literal.Number.Hex */ 44 | [data-md-color-scheme="default"] .highlight .mi { color: #0000D0; font-weight: bold } /* Literal.Number.Integer */ 45 | [data-md-color-scheme="default"] .highlight .mo { color: #4000E0; font-weight: bold } /* Literal.Number.Oct */ 46 | [data-md-color-scheme="default"] .highlight .sb { background-color: #fff0f0 } /* Literal.String.Backtick */ 47 | [data-md-color-scheme="default"] .highlight .sc { color: #0040D0 } /* Literal.String.Char */ 48 | [data-md-color-scheme="default"] .highlight .sd { color: #D04020 } /* Literal.String.Doc */ 49 | [data-md-color-scheme="default"] .highlight .s2 { background-color: #fff0f0 } /* Literal.String.Double */ 50 | [data-md-color-scheme="default"] .highlight .se { color: #606060; font-weight: bold; background-color: #fff0f0 } /* Literal.String.Escape */ 51 | [data-md-color-scheme="default"] .highlight .sh { background-color: #fff0f0 } /* Literal.String.Heredoc */ 52 | [data-md-color-scheme="default"] .highlight .si { background-color: #e0e0e0 } /* Literal.String.Interpol */ 53 | [data-md-color-scheme="default"] .highlight .sx { color: #D02000; background-color: #fff0f0 } /* Literal.String.Other */ 54 | [data-md-color-scheme="default"] .highlight .sr { color: #000000; background-color: #fff0ff } /* Literal.String.Regex */ 55 | [data-md-color-scheme="default"] .highlight .s1 { background-color: #fff0f0 } /* Literal.String.Single */ 56 | [data-md-color-scheme="default"] .highlight .ss { color: #A06000 } /* Literal.String.Symbol */ 57 | [data-md-color-scheme="default"] .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 58 | [data-md-color-scheme="default"] .highlight .vc { color: #306090 } /* Name.Variable.Class */ 59 | [data-md-color-scheme="default"] .highlight .vg { color: #d07000; font-weight: bold } /* Name.Variable.Global */ 60 | [data-md-color-scheme="default"] .highlight .vi { color: #3030B0 } /* Name.Variable.Instance */ 61 | [data-md-color-scheme="default"] .highlight .il { color: #0000D0; font-weight: bold } Literal.Number.Integer.Long 62 | 63 | 64 | [data-md-color-scheme="slate"] .highlight .hll { background-color: #49483e } 65 | [data-md-color-scheme="slate"] .highlight .c { color: #75715e } /* Comment */ 66 | [data-md-color-scheme="slate"] .highlight .err { color: #960050; background-color: #1e0010 } /* Error */ 67 | [data-md-color-scheme="slate"] .highlight .k { color: #66d9ef } /* Keyword */ 68 | [data-md-color-scheme="slate"] .highlight .l { color: #ae81ff } /* Literal */ 69 | [data-md-color-scheme="slate"] .highlight .n { color: #f8f8f2 } /* Name */ 70 | [data-md-color-scheme="slate"] .highlight .o { color: #f92672 } /* Operator */ 71 | [data-md-color-scheme="slate"] .highlight .p { color: #f8f8f2 } /* Punctuation */ 72 | [data-md-color-scheme="slate"] .highlight .ch { color: #75715e } /* Comment.Hashbang */ 73 | [data-md-color-scheme="slate"] .highlight .cm { color: #75715e } /* Comment.Multiline */ 74 | [data-md-color-scheme="slate"] .highlight .cp { color: #75715e } /* Comment.Preproc */ 75 | [data-md-color-scheme="slate"] .highlight .cpf { color: #75715e } /* Comment.PreprocFile */ 76 | [data-md-color-scheme="slate"] .highlight .c1 { color: #75715e } /* Comment.Single */ 77 | [data-md-color-scheme="slate"] .highlight .cs { color: #75715e } /* Comment.Special */ 78 | [data-md-color-scheme="slate"] .highlight .gd { color: #f92672 } /* Generic.Deleted */ 79 | [data-md-color-scheme="slate"] .highlight .ge { font-style: italic } /* Generic.Emph */ 80 | [data-md-color-scheme="slate"] .highlight .gi { color: #a6e22e } /* Generic.Inserted */ 81 | [data-md-color-scheme="slate"] .highlight .gs { font-weight: bold } /* Generic.Strong */ 82 | [data-md-color-scheme="slate"] .highlight .gu { color: #75715e } /* Generic.Subheading */ 83 | [data-md-color-scheme="slate"] .highlight .kc { color: #66d9ef } /* Keyword.Constant */ 84 | [data-md-color-scheme="slate"] .highlight .kd { color: #66d9ef } /* Keyword.Declaration */ 85 | [data-md-color-scheme="slate"] .highlight .kn { color: #f92672 } /* Keyword.Namespace */ 86 | [data-md-color-scheme="slate"] .highlight .kp { color: #66d9ef } /* Keyword.Pseudo */ 87 | [data-md-color-scheme="slate"] .highlight .kr { color: #66d9ef } /* Keyword.Reserved */ 88 | [data-md-color-scheme="slate"] .highlight .kt { color: #66d9ef } /* Keyword.Type */ 89 | [data-md-color-scheme="slate"] .highlight .ld { color: #e6db74 } /* Literal.Date */ 90 | [data-md-color-scheme="slate"] .highlight .m { color: #ae81ff } /* Literal.Number */ 91 | [data-md-color-scheme="slate"] .highlight .s { color: #e6db74 } /* Literal.String */ 92 | [data-md-color-scheme="slate"] .highlight .na { color: #a6e22e } /* Name.Attribute */ 93 | [data-md-color-scheme="slate"] .highlight .nb { color: #f8f8f2 } /* Name.Builtin */ 94 | [data-md-color-scheme="slate"] .highlight .nc { color: #a6e22e } /* Name.Class */ 95 | [data-md-color-scheme="slate"] .highlight .no { color: #66d9ef } /* Name.Constant */ 96 | [data-md-color-scheme="slate"] .highlight .nd { color: #a6e22e } /* Name.Decorator */ 97 | [data-md-color-scheme="slate"] .highlight .ni { color: #f8f8f2 } /* Name.Entity */ 98 | [data-md-color-scheme="slate"] .highlight .ne { color: #a6e22e } /* Name.Exception */ 99 | [data-md-color-scheme="slate"] .highlight .nf { color: #a6e22e } /* Name.Function */ 100 | [data-md-color-scheme="slate"] .highlight .nl { color: #f8f8f2 } /* Name.Label */ 101 | [data-md-color-scheme="slate"] .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ 102 | [data-md-color-scheme="slate"] .highlight .nx { color: #a6e22e } /* Name.Other */ 103 | [data-md-color-scheme="slate"] .highlight .py { color: #f8f8f2 } /* Name.Property */ 104 | [data-md-color-scheme="slate"] .highlight .nt { color: #f92672 } /* Name.Tag */ 105 | [data-md-color-scheme="slate"] .highlight .nv { color: #f8f8f2 } /* Name.Variable */ 106 | [data-md-color-scheme="slate"] .highlight .ow { color: #f92672 } /* Operator.Word */ 107 | [data-md-color-scheme="slate"] .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ 108 | [data-md-color-scheme="slate"] .highlight .mb { color: #ae81ff } /* Literal.Number.Bin */ 109 | [data-md-color-scheme="slate"] .highlight .mf { color: #ae81ff } /* Literal.Number.Float */ 110 | [data-md-color-scheme="slate"] .highlight .mh { color: #ae81ff } /* Literal.Number.Hex */ 111 | [data-md-color-scheme="slate"] .highlight .mi { color: #ae81ff } /* Literal.Number.Integer */ 112 | [data-md-color-scheme="slate"] .highlight .mo { color: #ae81ff } /* Literal.Number.Oct */ 113 | [data-md-color-scheme="slate"] .highlight .sa { color: #e6db74 } /* Literal.String.Affix */ 114 | [data-md-color-scheme="slate"] .highlight .sb { color: #e6db74 } /* Literal.String.Backtick */ 115 | [data-md-color-scheme="slate"] .highlight .sc { color: #e6db74 } /* Literal.String.Char */ 116 | [data-md-color-scheme="slate"] .highlight .dl { color: #e6db74 } /* Literal.String.Delimiter */ 117 | [data-md-color-scheme="slate"] .highlight .sd { color: #e6db74 } /* Literal.String.Doc */ 118 | [data-md-color-scheme="slate"] .highlight .s2 { color: #e6db74 } /* Literal.String.Double */ 119 | [data-md-color-scheme="slate"] .highlight .se { color: #ae81ff } /* Literal.String.Escape */ 120 | [data-md-color-scheme="slate"] .highlight .sh { color: #e6db74 } /* Literal.String.Heredoc */ 121 | [data-md-color-scheme="slate"] .highlight .si { color: #e6db74 } /* Literal.String.Interpol */ 122 | [data-md-color-scheme="slate"] .highlight .sx { color: #e6db74 } /* Literal.String.Other */ 123 | [data-md-color-scheme="slate"] .highlight .sr { color: #e6db74 } /* Literal.String.Regex */ 124 | [data-md-color-scheme="slate"] .highlight .s1 { color: #e6db74 } /* Literal.String.Single */ 125 | [data-md-color-scheme="slate"] .highlight .ss { color: #e6db74 } /* Literal.String.Symbol */ 126 | [data-md-color-scheme="slate"] .highlight .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ 127 | [data-md-color-scheme="slate"] .highlight .fm { color: #a6e22e } /* Name.Function.Magic */ 128 | [data-md-color-scheme="slate"] .highlight .vc { color: #f8f8f2 } /* Name.Variable.Class */ 129 | [data-md-color-scheme="slate"] .highlight .vg { color: #f8f8f2 } /* Name.Variable.Global */ 130 | [data-md-color-scheme="slate"] .highlight .vi { color: #f8f8f2 } /* Name.Variable.Instance */ 131 | [data-md-color-scheme="slate"] .highlight .vm { color: #f8f8f2 } /* Name.Variable.Magic */ 132 | [data-md-color-scheme="slate"] .highlight .il { color: #ae81ff } /* Literal.Number.Integer.Long */ 133 | -------------------------------------------------------------------------------- /docs/old/5.x/assets/css/highlight.css: -------------------------------------------------------------------------------- 1 | [data-md-color-scheme="default"] .highlight .hll { background-color: #ffffcc } 2 | [data-md-color-scheme="default"] .highlight .c { color: #808080 } /* Comment */ 3 | [data-md-color-scheme="default"] .highlight .err { color: #F00000; background-color: #F0A0A0 } /* Error */ 4 | [data-md-color-scheme="default"] .highlight .k { color: #008000; font-weight: bold } /* Keyword */ 5 | [data-md-color-scheme="default"] .highlight .o { color: #303030 } /* Operator */ 6 | [data-md-color-scheme="default"] .highlight .cm { color: #808080 } /* Comment.Multiline */ 7 | [data-md-color-scheme="default"] .highlight .cp { color: #507090 } /* Comment.Preproc */ 8 | [data-md-color-scheme="default"] .highlight .c1 { color: #808080 } /* Comment.Single */ 9 | [data-md-color-scheme="default"] .highlight .cs { color: #cc0000; font-weight: bold } /* Comment.Special */ 10 | [data-md-color-scheme="default"] .highlight .gd { color: #A00000 } /* Generic.Deleted */ 11 | [data-md-color-scheme="default"] .highlight .ge { font-style: italic } /* Generic.Emph */ 12 | [data-md-color-scheme="default"] .highlight .gr { color: #FF0000 } /* Generic.Error */ 13 | [data-md-color-scheme="default"] .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 14 | [data-md-color-scheme="default"] .highlight .gi { color: #00A000 } /* Generic.Inserted */ 15 | [data-md-color-scheme="default"] .highlight .go { color: #808080 } /* Generic.Output */ 16 | [data-md-color-scheme="default"] .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 17 | [data-md-color-scheme="default"] .highlight .gs { font-weight: bold } /* Generic.Strong */ 18 | [data-md-color-scheme="default"] .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 19 | [data-md-color-scheme="default"] .highlight .gt { color: #0040D0 } /* Generic.Traceback */ 20 | [data-md-color-scheme="default"] .highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ 21 | [data-md-color-scheme="default"] .highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ 22 | [data-md-color-scheme="default"] .highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ 23 | [data-md-color-scheme="default"] .highlight .kp { color: #003080; font-weight: bold } /* Keyword.Pseudo */ 24 | [data-md-color-scheme="default"] .highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ 25 | [data-md-color-scheme="default"] .highlight .kt { color: #303090; font-weight: bold } /* Keyword.Type */ 26 | [data-md-color-scheme="default"] .highlight .m { color: #6000E0; font-weight: bold } /* Literal.Number */ 27 | [data-md-color-scheme="default"] .highlight .s { background-color: #fff0f0 } /* Literal.String */ 28 | [data-md-color-scheme="default"] .highlight .na { color: #0000C0 } /* Name.Attribute */ 29 | [data-md-color-scheme="default"] .highlight .nb { color: #007020 } /* Name.Builtin */ 30 | [data-md-color-scheme="default"] .highlight .nc { color: #B00060; font-weight: bold } /* Name.Class */ 31 | [data-md-color-scheme="default"] .highlight .no { color: #003060; font-weight: bold } /* Name.Constant */ 32 | [data-md-color-scheme="default"] .highlight .nd { color: #505050; font-weight: bold } /* Name.Decorator */ 33 | [data-md-color-scheme="default"] .highlight .ni { color: #800000; font-weight: bold } /* Name.Entity */ 34 | [data-md-color-scheme="default"] .highlight .ne { color: #F00000; font-weight: bold } /* Name.Exception */ 35 | [data-md-color-scheme="default"] .highlight .nf { color: #0060B0; font-weight: bold } /* Name.Function */ 36 | [data-md-color-scheme="default"] .highlight .nl { color: #907000; font-weight: bold } /* Name.Label */ 37 | [data-md-color-scheme="default"] .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 38 | [data-md-color-scheme="default"] .highlight .nt { color: #007000 } /* Name.Tag */ 39 | [data-md-color-scheme="default"] .highlight .nv { color: #906030 } /* Name.Variable */ 40 | [data-md-color-scheme="default"] .highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */ 41 | [data-md-color-scheme="default"] .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 42 | [data-md-color-scheme="default"] .highlight .mf { color: #6000E0; font-weight: bold } /* Literal.Number.Float */ 43 | [data-md-color-scheme="default"] .highlight .mh { color: #005080; font-weight: bold } /* Literal.Number.Hex */ 44 | [data-md-color-scheme="default"] .highlight .mi { color: #0000D0; font-weight: bold } /* Literal.Number.Integer */ 45 | [data-md-color-scheme="default"] .highlight .mo { color: #4000E0; font-weight: bold } /* Literal.Number.Oct */ 46 | [data-md-color-scheme="default"] .highlight .sb { background-color: #fff0f0 } /* Literal.String.Backtick */ 47 | [data-md-color-scheme="default"] .highlight .sc { color: #0040D0 } /* Literal.String.Char */ 48 | [data-md-color-scheme="default"] .highlight .sd { color: #D04020 } /* Literal.String.Doc */ 49 | [data-md-color-scheme="default"] .highlight .s2 { background-color: #fff0f0 } /* Literal.String.Double */ 50 | [data-md-color-scheme="default"] .highlight .se { color: #606060; font-weight: bold; background-color: #fff0f0 } /* Literal.String.Escape */ 51 | [data-md-color-scheme="default"] .highlight .sh { background-color: #fff0f0 } /* Literal.String.Heredoc */ 52 | [data-md-color-scheme="default"] .highlight .si { background-color: #e0e0e0 } /* Literal.String.Interpol */ 53 | [data-md-color-scheme="default"] .highlight .sx { color: #D02000; background-color: #fff0f0 } /* Literal.String.Other */ 54 | [data-md-color-scheme="default"] .highlight .sr { color: #000000; background-color: #fff0ff } /* Literal.String.Regex */ 55 | [data-md-color-scheme="default"] .highlight .s1 { background-color: #fff0f0 } /* Literal.String.Single */ 56 | [data-md-color-scheme="default"] .highlight .ss { color: #A06000 } /* Literal.String.Symbol */ 57 | [data-md-color-scheme="default"] .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 58 | [data-md-color-scheme="default"] .highlight .vc { color: #306090 } /* Name.Variable.Class */ 59 | [data-md-color-scheme="default"] .highlight .vg { color: #d07000; font-weight: bold } /* Name.Variable.Global */ 60 | [data-md-color-scheme="default"] .highlight .vi { color: #3030B0 } /* Name.Variable.Instance */ 61 | [data-md-color-scheme="default"] .highlight .il { color: #0000D0; font-weight: bold } Literal.Number.Integer.Long 62 | 63 | 64 | [data-md-color-scheme="slate"] .highlight .hll { background-color: #49483e } 65 | [data-md-color-scheme="slate"] .highlight .c { color: #75715e } /* Comment */ 66 | [data-md-color-scheme="slate"] .highlight .err { color: #960050; background-color: #1e0010 } /* Error */ 67 | [data-md-color-scheme="slate"] .highlight .k { color: #66d9ef } /* Keyword */ 68 | [data-md-color-scheme="slate"] .highlight .l { color: #ae81ff } /* Literal */ 69 | [data-md-color-scheme="slate"] .highlight .n { color: #f8f8f2 } /* Name */ 70 | [data-md-color-scheme="slate"] .highlight .o { color: #f92672 } /* Operator */ 71 | [data-md-color-scheme="slate"] .highlight .p { color: #f8f8f2 } /* Punctuation */ 72 | [data-md-color-scheme="slate"] .highlight .ch { color: #75715e } /* Comment.Hashbang */ 73 | [data-md-color-scheme="slate"] .highlight .cm { color: #75715e } /* Comment.Multiline */ 74 | [data-md-color-scheme="slate"] .highlight .cp { color: #75715e } /* Comment.Preproc */ 75 | [data-md-color-scheme="slate"] .highlight .cpf { color: #75715e } /* Comment.PreprocFile */ 76 | [data-md-color-scheme="slate"] .highlight .c1 { color: #75715e } /* Comment.Single */ 77 | [data-md-color-scheme="slate"] .highlight .cs { color: #75715e } /* Comment.Special */ 78 | [data-md-color-scheme="slate"] .highlight .gd { color: #f92672 } /* Generic.Deleted */ 79 | [data-md-color-scheme="slate"] .highlight .ge { font-style: italic } /* Generic.Emph */ 80 | [data-md-color-scheme="slate"] .highlight .gi { color: #a6e22e } /* Generic.Inserted */ 81 | [data-md-color-scheme="slate"] .highlight .gs { font-weight: bold } /* Generic.Strong */ 82 | [data-md-color-scheme="slate"] .highlight .gu { color: #75715e } /* Generic.Subheading */ 83 | [data-md-color-scheme="slate"] .highlight .kc { color: #66d9ef } /* Keyword.Constant */ 84 | [data-md-color-scheme="slate"] .highlight .kd { color: #66d9ef } /* Keyword.Declaration */ 85 | [data-md-color-scheme="slate"] .highlight .kn { color: #f92672 } /* Keyword.Namespace */ 86 | [data-md-color-scheme="slate"] .highlight .kp { color: #66d9ef } /* Keyword.Pseudo */ 87 | [data-md-color-scheme="slate"] .highlight .kr { color: #66d9ef } /* Keyword.Reserved */ 88 | [data-md-color-scheme="slate"] .highlight .kt { color: #66d9ef } /* Keyword.Type */ 89 | [data-md-color-scheme="slate"] .highlight .ld { color: #e6db74 } /* Literal.Date */ 90 | [data-md-color-scheme="slate"] .highlight .m { color: #ae81ff } /* Literal.Number */ 91 | [data-md-color-scheme="slate"] .highlight .s { color: #e6db74 } /* Literal.String */ 92 | [data-md-color-scheme="slate"] .highlight .na { color: #a6e22e } /* Name.Attribute */ 93 | [data-md-color-scheme="slate"] .highlight .nb { color: #f8f8f2 } /* Name.Builtin */ 94 | [data-md-color-scheme="slate"] .highlight .nc { color: #a6e22e } /* Name.Class */ 95 | [data-md-color-scheme="slate"] .highlight .no { color: #66d9ef } /* Name.Constant */ 96 | [data-md-color-scheme="slate"] .highlight .nd { color: #a6e22e } /* Name.Decorator */ 97 | [data-md-color-scheme="slate"] .highlight .ni { color: #f8f8f2 } /* Name.Entity */ 98 | [data-md-color-scheme="slate"] .highlight .ne { color: #a6e22e } /* Name.Exception */ 99 | [data-md-color-scheme="slate"] .highlight .nf { color: #a6e22e } /* Name.Function */ 100 | [data-md-color-scheme="slate"] .highlight .nl { color: #f8f8f2 } /* Name.Label */ 101 | [data-md-color-scheme="slate"] .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ 102 | [data-md-color-scheme="slate"] .highlight .nx { color: #a6e22e } /* Name.Other */ 103 | [data-md-color-scheme="slate"] .highlight .py { color: #f8f8f2 } /* Name.Property */ 104 | [data-md-color-scheme="slate"] .highlight .nt { color: #f92672 } /* Name.Tag */ 105 | [data-md-color-scheme="slate"] .highlight .nv { color: #f8f8f2 } /* Name.Variable */ 106 | [data-md-color-scheme="slate"] .highlight .ow { color: #f92672 } /* Operator.Word */ 107 | [data-md-color-scheme="slate"] .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ 108 | [data-md-color-scheme="slate"] .highlight .mb { color: #ae81ff } /* Literal.Number.Bin */ 109 | [data-md-color-scheme="slate"] .highlight .mf { color: #ae81ff } /* Literal.Number.Float */ 110 | [data-md-color-scheme="slate"] .highlight .mh { color: #ae81ff } /* Literal.Number.Hex */ 111 | [data-md-color-scheme="slate"] .highlight .mi { color: #ae81ff } /* Literal.Number.Integer */ 112 | [data-md-color-scheme="slate"] .highlight .mo { color: #ae81ff } /* Literal.Number.Oct */ 113 | [data-md-color-scheme="slate"] .highlight .sa { color: #e6db74 } /* Literal.String.Affix */ 114 | [data-md-color-scheme="slate"] .highlight .sb { color: #e6db74 } /* Literal.String.Backtick */ 115 | [data-md-color-scheme="slate"] .highlight .sc { color: #e6db74 } /* Literal.String.Char */ 116 | [data-md-color-scheme="slate"] .highlight .dl { color: #e6db74 } /* Literal.String.Delimiter */ 117 | [data-md-color-scheme="slate"] .highlight .sd { color: #e6db74 } /* Literal.String.Doc */ 118 | [data-md-color-scheme="slate"] .highlight .s2 { color: #e6db74 } /* Literal.String.Double */ 119 | [data-md-color-scheme="slate"] .highlight .se { color: #ae81ff } /* Literal.String.Escape */ 120 | [data-md-color-scheme="slate"] .highlight .sh { color: #e6db74 } /* Literal.String.Heredoc */ 121 | [data-md-color-scheme="slate"] .highlight .si { color: #e6db74 } /* Literal.String.Interpol */ 122 | [data-md-color-scheme="slate"] .highlight .sx { color: #e6db74 } /* Literal.String.Other */ 123 | [data-md-color-scheme="slate"] .highlight .sr { color: #e6db74 } /* Literal.String.Regex */ 124 | [data-md-color-scheme="slate"] .highlight .s1 { color: #e6db74 } /* Literal.String.Single */ 125 | [data-md-color-scheme="slate"] .highlight .ss { color: #e6db74 } /* Literal.String.Symbol */ 126 | [data-md-color-scheme="slate"] .highlight .bp { color: #f8f8f2 } /* Name.Builtin.Pseudo */ 127 | [data-md-color-scheme="slate"] .highlight .fm { color: #a6e22e } /* Name.Function.Magic */ 128 | [data-md-color-scheme="slate"] .highlight .vc { color: #f8f8f2 } /* Name.Variable.Class */ 129 | [data-md-color-scheme="slate"] .highlight .vg { color: #f8f8f2 } /* Name.Variable.Global */ 130 | [data-md-color-scheme="slate"] .highlight .vi { color: #f8f8f2 } /* Name.Variable.Instance */ 131 | [data-md-color-scheme="slate"] .highlight .vm { color: #f8f8f2 } /* Name.Variable.Magic */ 132 | [data-md-color-scheme="slate"] .highlight .il { color: #ae81ff } /* Literal.Number.Integer.Long */ 133 | -------------------------------------------------------------------------------- /src/sqla_wrapper/alembic_wrapper.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import typing as t 3 | from pathlib import Path 4 | 5 | from alembic import autogenerate, util 6 | from alembic.config import Config 7 | from alembic.runtime.environment import EnvironmentContext 8 | from alembic.script import Script, ScriptDirectory 9 | 10 | from .cli import click_cli, proper_cli_cli 11 | from .sqlalchemy_wrapper import SQLAlchemy 12 | 13 | 14 | __all__ = ("Alembic",) 15 | 16 | StrPath = t.Union[str, Path] 17 | DEFAULT_FILE_TEMPLATE = "%%(year)d_%%(month).2d_%%(day).2d_%%(rev)s_%%(slug)s" 18 | TEMPLATE_FILE = "script.py.mako" 19 | 20 | 21 | class Alembic: 22 | """Provide an Alembic environment and migration API. 23 | 24 | For a more in-depth understanding of these methods and the extra options, you 25 | can read the 26 | [documentation for the Alembic config](https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file). 27 | 28 | Args: 29 | db: A `sqla_wrapper.SQLAlchemy` instance. 30 | path: Path to the migrations folder. 31 | **options: Other alembic options 32 | 33 | """ 34 | 35 | def __init__( 36 | self, 37 | db: SQLAlchemy, 38 | path: StrPath = "db/migrations", 39 | **options, 40 | ) -> None: 41 | self.db = db 42 | self.path = path = Path(path).absolute() 43 | options["script_location"] = str(path) 44 | self.config = self._get_config(options) 45 | self.init(path) 46 | self.script_directory = ScriptDirectory.from_config(self.config) 47 | 48 | def revision( 49 | self, message: str, *, empty: bool = False, parent: str = "head" 50 | ) -> "Script | None": 51 | """Create a new revision. 52 | Auto-generate operations by comparing models and database. 53 | 54 | Args: 55 | message: Revision message. 56 | empty: Generate just an empty migration file, not the operations. 57 | parent: Parent revision of this new revision. 58 | 59 | """ 60 | revision_context = autogenerate.RevisionContext( 61 | self.config, 62 | self.script_directory, 63 | { 64 | "message": message, 65 | "sql": False, 66 | "head": [parent], 67 | "splice": False, 68 | "branch_label": None, 69 | "version_path": self.script_directory.dir, 70 | "rev_id": self.rev_id(), 71 | "depends_on": None, 72 | }, 73 | ) 74 | 75 | def do_revision(revision, context): 76 | if empty: 77 | revision_context.run_no_autogenerate(revision, context) 78 | else: 79 | revision_context.run_autogenerate(revision, context) 80 | return [] 81 | 82 | self._run_online(do_revision) 83 | 84 | result = list(revision_context.generate_scripts()) 85 | return result[0] 86 | 87 | def upgrade(self, target: str = "head", *, sql: bool = False, **kwargs) -> None: 88 | """Run migrations to upgrade database. 89 | 90 | Args: 91 | target: Revision target or "from:to" range if `sql=True`. 92 | "head" by default. 93 | sql: Don't emit SQL to database, dump to standard output instead. 94 | kwargs: Optional arguments. 95 | 96 | If these are passed, they are sent directly 97 | to the `upgrade()` functions within each revision file. 98 | To use, modify the `script.py.mako`template file 99 | so that the `upgrade()` functions can accept arguments. 100 | 101 | """ 102 | starting_rev = None 103 | if ":" in target: 104 | if not sql: 105 | raise ValueError("range target not allowed") 106 | starting_rev, target = target.split(":") 107 | elif sql: 108 | raise ValueError("sql=True requires target=from:to") 109 | 110 | def do_upgrade(revision, context): 111 | return self.script_directory._upgrade_revs(target, revision) 112 | 113 | run = self._run_offline if sql else self._run_online 114 | run( 115 | do_upgrade, 116 | kwargs=kwargs, 117 | starting_rev=starting_rev, 118 | destination_rev=target, 119 | ) 120 | 121 | def downgrade(self, target: str = "-1", *, sql: bool = False, **kwargs) -> None: 122 | """Run migrations to downgrade database. 123 | 124 | Args: 125 | target: Revision target as an integer relative to the current 126 | state (e.g.: "-1"), or as a "from:to" range if `sql=True`. 127 | "-1" by default. 128 | sql: Don't emit SQL to database, dump to standard output instead. 129 | kwargs: Optional arguments. 130 | If these are passed, they are sent directly 131 | to the `downgrade()` functions within each revision file. 132 | To use, modify the `script.py.mako` template file 133 | so that the `downgrade()` functions can accept arguments. 134 | 135 | """ 136 | 137 | starting_rev = None 138 | if isinstance(target, str) and ":" in target: 139 | if not sql: 140 | raise ValueError("range target not allowed") 141 | starting_rev, target = target.split(":") 142 | elif sql: 143 | raise ValueError("sql=True requires target=from:to") 144 | else: 145 | itarget = int(target) 146 | target = str(-itarget if itarget > 0 else itarget) 147 | 148 | def do_downgrade(revision, context): 149 | return self.script_directory._downgrade_revs(target, revision) 150 | 151 | run = self._run_offline if sql else self._run_online 152 | run( 153 | do_downgrade, 154 | kwargs=kwargs, 155 | starting_rev=starting_rev, 156 | destination_rev=target, 157 | ) 158 | 159 | def get_history( 160 | self, *, start: "str | None" = None, end: "str | None" = None 161 | ) -> list[Script]: 162 | """Get the list of revisions in chronological order. 163 | You can optionally specify the range of revisions to return. 164 | 165 | Args: 166 | start: From this revision (including it.) 167 | end: To this revision (including it.) 168 | 169 | """ 170 | if start == "current": 171 | current = self.get_current() 172 | start = current.revision if current else None 173 | if end == "current": 174 | current = self.get_current() 175 | end = current.revision if current else "heads" 176 | 177 | return list( 178 | self.script_directory.walk_revisions(start or "base", end or "heads") 179 | )[::-1] 180 | 181 | def history( 182 | self, 183 | *, 184 | verbose: bool = False, 185 | start: "str | None" = "base", 186 | end: "str | None" = "heads", 187 | ) -> None: 188 | """Print the list of revisions in chronological order. 189 | You can optionally specify the range of revisions to return. 190 | 191 | Args: 192 | verbose: If `True`, shows also the path and the docstring 193 | of each revision file. 194 | start: Optional starting revision (including it.) 195 | end: Optional end revision (including it.) 196 | 197 | """ 198 | for rev in self.get_history(start=start, end=end): 199 | if verbose: 200 | print("-" * 20) 201 | print( 202 | rev.cmd_format( 203 | verbose=verbose, 204 | include_doc=True, 205 | include_parents=True, 206 | ) 207 | ) 208 | 209 | def stamp( 210 | self, target: str = "head", *, sql: bool = False, purge: bool = False 211 | ) -> None: 212 | """Set the given revision in the revision table. Don't run migrations. 213 | 214 | Args: 215 | target: The target revision; "head" by default. 216 | sql: Don't emit SQL to the database, dump to the standard output instead. 217 | purge: Delete all entries in the version table before stamping. 218 | 219 | """ 220 | 221 | def do_stamp(revision, context): 222 | return self.script_directory._stamp_revs(target, revision) 223 | 224 | run = self._run_offline if sql else self._run_online 225 | run( 226 | do_stamp, 227 | destination_rev=target, 228 | purge=purge, 229 | ) 230 | 231 | def _get_currents(self) -> "t.Tuple[Script | None, ...]": 232 | """Get the last revisions applied.""" 233 | env = EnvironmentContext(self.config, self.script_directory) 234 | with self.db.engine.connect() as connection: 235 | env.configure(connection=connection) 236 | migration_context = env.get_context() 237 | current_heads = migration_context.get_current_heads() 238 | 239 | return self.script_directory.get_revisions(current_heads) 240 | 241 | def get_current(self) -> "Script | None": 242 | """Get the last revision applied.""" 243 | revisions = self._get_currents() 244 | return revisions[0] if revisions else None 245 | 246 | def current(self, verbose: bool = False) -> None: 247 | """Print the latest revision(s) applied. 248 | 249 | Args: 250 | verbose: If `True`, shows also the path and the docstring 251 | of the revision file. 252 | 253 | """ 254 | rev = self.get_current() 255 | if rev: 256 | print( 257 | rev.cmd_format( 258 | verbose=verbose, 259 | include_doc=True, 260 | include_parents=True, 261 | ) 262 | ) 263 | 264 | def _get_heads(self) -> "t.Tuple[Script | None, ...]": 265 | """Get the list of the latest revisions.""" 266 | return self.script_directory.get_revisions("heads") 267 | 268 | def get_head(self) -> "Script | None": 269 | """Get the latest revision.""" 270 | heads = self._get_heads() 271 | return heads[0] if heads else None 272 | 273 | def head(self, verbose: bool = False) -> None: 274 | """Print the latest revision. 275 | 276 | Args: 277 | verbose: If `True`, shows also the path and the docstring 278 | of the revision file. 279 | 280 | """ 281 | rev = self.get_head() 282 | if rev: 283 | print( 284 | rev.cmd_format( 285 | verbose=verbose, 286 | include_doc=True, 287 | include_parents=True, 288 | ) 289 | ) 290 | 291 | def init(self, path: StrPath) -> None: 292 | """Creates a new migration folder 293 | with a `script.py.mako` template file. It doesn't fail if the 294 | folder or file already exists. 295 | 296 | Args: 297 | path: Target folder. 298 | 299 | """ 300 | path = Path(path) 301 | path.mkdir(exist_ok=True) 302 | src_path = ( 303 | Path(self.config.get_template_directory()) / "generic" / TEMPLATE_FILE 304 | ) 305 | dest_path = path / TEMPLATE_FILE 306 | if not dest_path.exists(): 307 | shutil.copy(src_path, path) 308 | 309 | def create_all(self) -> None: 310 | """Create all the tables from the current models 311 | and stamp the latest revision without running any migration. 312 | """ 313 | self.db.create_all() 314 | self.stamp() 315 | 316 | def rev_id(self) -> str: 317 | """Generate a unique id for a revision. 318 | 319 | By default this uses `alembic.util.rev_id`. Override this 320 | method to change it. 321 | """ 322 | return util.rev_id() 323 | 324 | def get_proper_cli(self) -> t.Any: 325 | return proper_cli_cli.get_proper_cli(self) 326 | 327 | def get_click_cli(self, name="db") -> t.Any: 328 | return click_cli.get_click_cli(self, name) 329 | 330 | def get_flask_cli(self, name="db") -> t.Any: 331 | return click_cli.get_flask_cli(self, name) 332 | 333 | # Private 334 | 335 | def _get_config(self, options: dict[str, str]) -> Config: 336 | options.setdefault("file_template", DEFAULT_FILE_TEMPLATE) 337 | options.setdefault("version_locations", options["script_location"]) 338 | 339 | config = Config() 340 | for key, value in options.items(): 341 | config.set_main_option(key, value) 342 | 343 | return config 344 | 345 | def _run_online( 346 | self, fn: t.Callable, *, kwargs: "dict | None" = None, **envargs 347 | ) -> None: 348 | """Emit the SQL to the database.""" 349 | env = EnvironmentContext(self.config, self.script_directory) 350 | with self.db.engine.connect() as connection: 351 | env.configure( 352 | connection=connection, 353 | fn=fn, 354 | target_metadata=self.db.registry.metadata, 355 | **envargs, 356 | ) 357 | kwargs = kwargs or {} 358 | with env.begin_transaction(): 359 | env.run_migrations(**kwargs) 360 | 361 | def _run_offline( 362 | self, fn: t.Callable, *, kwargs: "dict | None" = None, **envargs 363 | ) -> None: 364 | """Don't emit SQL to the database, dump to standard output instead.""" 365 | env = EnvironmentContext(self.config, self.script_directory) 366 | env.configure( 367 | url=self.db.url, 368 | fn=fn, 369 | target_metadata=self.db.registry.metadata, 370 | as_sql=True, 371 | **envargs, 372 | ) 373 | kwargs = kwargs or {} 374 | with env.begin_transaction(): 375 | env.run_migrations(**kwargs) 376 | -------------------------------------------------------------------------------- /docs/old/5.x/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | SQLA Wrapper 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 | 87 |
88 |
89 | 90 |
91 | 92 | 93 | 94 |
95 |
This document is for an older version of SQLA-Wrapper
96 | 206 | 207 |
208 | 209 |
210 | 211 | 212 | 213 | 214 | 215 | 216 |
217 |
218 | 219 | 220 | 221 |
222 |
223 |
224 | 225 | 226 | 227 | 423 |
424 |
425 |
426 | 427 | 428 | 429 |
430 |
431 | 432 |

404 - Not found

433 | 434 | 435 |
436 |
437 |
438 | 439 | 440 | 441 | Back to top 442 | 443 | 444 |
445 | 446 | 447 |
448 | 449 | 468 |
469 | 470 |
471 |
472 |
473 |
474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | -------------------------------------------------------------------------------- /docs/old/5.x/assets/javascripts/lunr/tinyseg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * export the module via AMD, CommonJS or as a browser global 3 | * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js 4 | */ 5 | ;(function (root, factory) { 6 | if (typeof define === 'function' && define.amd) { 7 | // AMD. Register as an anonymous module. 8 | define(factory) 9 | } else if (typeof exports === 'object') { 10 | /** 11 | * Node. Does not work with strict CommonJS, but 12 | * only CommonJS-like environments that support module.exports, 13 | * like Node. 14 | */ 15 | module.exports = factory() 16 | } else { 17 | // Browser globals (root is window) 18 | factory()(root.lunr); 19 | } 20 | }(this, function () { 21 | /** 22 | * Just return a value to define the module export. 23 | * This example returns an object, but the module 24 | * can return a function as the exported value. 25 | */ 26 | 27 | return function(lunr) { 28 | // TinySegmenter 0.1 -- Super compact Japanese tokenizer in Javascript 29 | // (c) 2008 Taku Kudo 30 | // TinySegmenter is freely distributable under the terms of a new BSD licence. 31 | // For details, see http://chasen.org/~taku/software/TinySegmenter/LICENCE.txt 32 | 33 | function TinySegmenter() { 34 | var patterns = { 35 | "[一二三四五六七八九十百千万億兆]":"M", 36 | "[一-龠々〆ヵヶ]":"H", 37 | "[ぁ-ん]":"I", 38 | "[ァ-ヴーア-ン゙ー]":"K", 39 | "[a-zA-Za-zA-Z]":"A", 40 | "[0-90-9]":"N" 41 | } 42 | this.chartype_ = []; 43 | for (var i in patterns) { 44 | var regexp = new RegExp(i); 45 | this.chartype_.push([regexp, patterns[i]]); 46 | } 47 | 48 | this.BIAS__ = -332 49 | this.BC1__ = {"HH":6,"II":2461,"KH":406,"OH":-1378}; 50 | this.BC2__ = {"AA":-3267,"AI":2744,"AN":-878,"HH":-4070,"HM":-1711,"HN":4012,"HO":3761,"IA":1327,"IH":-1184,"II":-1332,"IK":1721,"IO":5492,"KI":3831,"KK":-8741,"MH":-3132,"MK":3334,"OO":-2920}; 51 | this.BC3__ = {"HH":996,"HI":626,"HK":-721,"HN":-1307,"HO":-836,"IH":-301,"KK":2762,"MK":1079,"MM":4034,"OA":-1652,"OH":266}; 52 | this.BP1__ = {"BB":295,"OB":304,"OO":-125,"UB":352}; 53 | this.BP2__ = {"BO":60,"OO":-1762}; 54 | this.BQ1__ = {"BHH":1150,"BHM":1521,"BII":-1158,"BIM":886,"BMH":1208,"BNH":449,"BOH":-91,"BOO":-2597,"OHI":451,"OIH":-296,"OKA":1851,"OKH":-1020,"OKK":904,"OOO":2965}; 55 | this.BQ2__ = {"BHH":118,"BHI":-1159,"BHM":466,"BIH":-919,"BKK":-1720,"BKO":864,"OHH":-1139,"OHM":-181,"OIH":153,"UHI":-1146}; 56 | this.BQ3__ = {"BHH":-792,"BHI":2664,"BII":-299,"BKI":419,"BMH":937,"BMM":8335,"BNN":998,"BOH":775,"OHH":2174,"OHM":439,"OII":280,"OKH":1798,"OKI":-793,"OKO":-2242,"OMH":-2402,"OOO":11699}; 57 | this.BQ4__ = {"BHH":-3895,"BIH":3761,"BII":-4654,"BIK":1348,"BKK":-1806,"BMI":-3385,"BOO":-12396,"OAH":926,"OHH":266,"OHK":-2036,"ONN":-973}; 58 | this.BW1__ = {",と":660,",同":727,"B1あ":1404,"B1同":542,"、と":660,"、同":727,"」と":1682,"あっ":1505,"いう":1743,"いっ":-2055,"いる":672,"うし":-4817,"うん":665,"から":3472,"がら":600,"こう":-790,"こと":2083,"こん":-1262,"さら":-4143,"さん":4573,"した":2641,"して":1104,"すで":-3399,"そこ":1977,"それ":-871,"たち":1122,"ため":601,"った":3463,"つい":-802,"てい":805,"てき":1249,"でき":1127,"です":3445,"では":844,"とい":-4915,"とみ":1922,"どこ":3887,"ない":5713,"なっ":3015,"など":7379,"なん":-1113,"にし":2468,"には":1498,"にも":1671,"に対":-912,"の一":-501,"の中":741,"ませ":2448,"まで":1711,"まま":2600,"まる":-2155,"やむ":-1947,"よっ":-2565,"れた":2369,"れで":-913,"をし":1860,"を見":731,"亡く":-1886,"京都":2558,"取り":-2784,"大き":-2604,"大阪":1497,"平方":-2314,"引き":-1336,"日本":-195,"本当":-2423,"毎日":-2113,"目指":-724,"B1あ":1404,"B1同":542,"」と":1682}; 59 | this.BW2__ = {"..":-11822,"11":-669,"――":-5730,"−−":-13175,"いう":-1609,"うか":2490,"かし":-1350,"かも":-602,"から":-7194,"かれ":4612,"がい":853,"がら":-3198,"きた":1941,"くな":-1597,"こと":-8392,"この":-4193,"させ":4533,"され":13168,"さん":-3977,"しい":-1819,"しか":-545,"した":5078,"して":972,"しな":939,"その":-3744,"たい":-1253,"たた":-662,"ただ":-3857,"たち":-786,"たと":1224,"たは":-939,"った":4589,"って":1647,"っと":-2094,"てい":6144,"てき":3640,"てく":2551,"ては":-3110,"ても":-3065,"でい":2666,"でき":-1528,"でし":-3828,"です":-4761,"でも":-4203,"とい":1890,"とこ":-1746,"とと":-2279,"との":720,"とみ":5168,"とも":-3941,"ない":-2488,"なが":-1313,"など":-6509,"なの":2614,"なん":3099,"にお":-1615,"にし":2748,"にな":2454,"によ":-7236,"に対":-14943,"に従":-4688,"に関":-11388,"のか":2093,"ので":-7059,"のに":-6041,"のの":-6125,"はい":1073,"はが":-1033,"はず":-2532,"ばれ":1813,"まし":-1316,"まで":-6621,"まれ":5409,"めて":-3153,"もい":2230,"もの":-10713,"らか":-944,"らし":-1611,"らに":-1897,"りし":651,"りま":1620,"れた":4270,"れて":849,"れば":4114,"ろう":6067,"われ":7901,"を通":-11877,"んだ":728,"んな":-4115,"一人":602,"一方":-1375,"一日":970,"一部":-1051,"上が":-4479,"会社":-1116,"出て":2163,"分の":-7758,"同党":970,"同日":-913,"大阪":-2471,"委員":-1250,"少な":-1050,"年度":-8669,"年間":-1626,"府県":-2363,"手権":-1982,"新聞":-4066,"日新":-722,"日本":-7068,"日米":3372,"曜日":-601,"朝鮮":-2355,"本人":-2697,"東京":-1543,"然と":-1384,"社会":-1276,"立て":-990,"第に":-1612,"米国":-4268,"11":-669}; 60 | this.BW3__ = {"あた":-2194,"あり":719,"ある":3846,"い.":-1185,"い。":-1185,"いい":5308,"いえ":2079,"いく":3029,"いた":2056,"いっ":1883,"いる":5600,"いわ":1527,"うち":1117,"うと":4798,"えと":1454,"か.":2857,"か。":2857,"かけ":-743,"かっ":-4098,"かに":-669,"から":6520,"かり":-2670,"が,":1816,"が、":1816,"がき":-4855,"がけ":-1127,"がっ":-913,"がら":-4977,"がり":-2064,"きた":1645,"けど":1374,"こと":7397,"この":1542,"ころ":-2757,"さい":-714,"さを":976,"し,":1557,"し、":1557,"しい":-3714,"した":3562,"して":1449,"しな":2608,"しま":1200,"す.":-1310,"す。":-1310,"する":6521,"ず,":3426,"ず、":3426,"ずに":841,"そう":428,"た.":8875,"た。":8875,"たい":-594,"たの":812,"たり":-1183,"たる":-853,"だ.":4098,"だ。":4098,"だっ":1004,"った":-4748,"って":300,"てい":6240,"てお":855,"ても":302,"です":1437,"でに":-1482,"では":2295,"とう":-1387,"とし":2266,"との":541,"とも":-3543,"どう":4664,"ない":1796,"なく":-903,"など":2135,"に,":-1021,"に、":-1021,"にし":1771,"にな":1906,"には":2644,"の,":-724,"の、":-724,"の子":-1000,"は,":1337,"は、":1337,"べき":2181,"まし":1113,"ます":6943,"まっ":-1549,"まで":6154,"まれ":-793,"らし":1479,"られ":6820,"るる":3818,"れ,":854,"れ、":854,"れた":1850,"れて":1375,"れば":-3246,"れる":1091,"われ":-605,"んだ":606,"んで":798,"カ月":990,"会議":860,"入り":1232,"大会":2217,"始め":1681,"市":965,"新聞":-5055,"日,":974,"日、":974,"社会":2024,"カ月":990}; 61 | this.TC1__ = {"AAA":1093,"HHH":1029,"HHM":580,"HII":998,"HOH":-390,"HOM":-331,"IHI":1169,"IOH":-142,"IOI":-1015,"IOM":467,"MMH":187,"OOI":-1832}; 62 | this.TC2__ = {"HHO":2088,"HII":-1023,"HMM":-1154,"IHI":-1965,"KKH":703,"OII":-2649}; 63 | this.TC3__ = {"AAA":-294,"HHH":346,"HHI":-341,"HII":-1088,"HIK":731,"HOH":-1486,"IHH":128,"IHI":-3041,"IHO":-1935,"IIH":-825,"IIM":-1035,"IOI":-542,"KHH":-1216,"KKA":491,"KKH":-1217,"KOK":-1009,"MHH":-2694,"MHM":-457,"MHO":123,"MMH":-471,"NNH":-1689,"NNO":662,"OHO":-3393}; 64 | this.TC4__ = {"HHH":-203,"HHI":1344,"HHK":365,"HHM":-122,"HHN":182,"HHO":669,"HIH":804,"HII":679,"HOH":446,"IHH":695,"IHO":-2324,"IIH":321,"III":1497,"IIO":656,"IOO":54,"KAK":4845,"KKA":3386,"KKK":3065,"MHH":-405,"MHI":201,"MMH":-241,"MMM":661,"MOM":841}; 65 | this.TQ1__ = {"BHHH":-227,"BHHI":316,"BHIH":-132,"BIHH":60,"BIII":1595,"BNHH":-744,"BOHH":225,"BOOO":-908,"OAKK":482,"OHHH":281,"OHIH":249,"OIHI":200,"OIIH":-68}; 66 | this.TQ2__ = {"BIHH":-1401,"BIII":-1033,"BKAK":-543,"BOOO":-5591}; 67 | this.TQ3__ = {"BHHH":478,"BHHM":-1073,"BHIH":222,"BHII":-504,"BIIH":-116,"BIII":-105,"BMHI":-863,"BMHM":-464,"BOMH":620,"OHHH":346,"OHHI":1729,"OHII":997,"OHMH":481,"OIHH":623,"OIIH":1344,"OKAK":2792,"OKHH":587,"OKKA":679,"OOHH":110,"OOII":-685}; 68 | this.TQ4__ = {"BHHH":-721,"BHHM":-3604,"BHII":-966,"BIIH":-607,"BIII":-2181,"OAAA":-2763,"OAKK":180,"OHHH":-294,"OHHI":2446,"OHHO":480,"OHIH":-1573,"OIHH":1935,"OIHI":-493,"OIIH":626,"OIII":-4007,"OKAK":-8156}; 69 | this.TW1__ = {"につい":-4681,"東京都":2026}; 70 | this.TW2__ = {"ある程":-2049,"いった":-1256,"ころが":-2434,"しょう":3873,"その後":-4430,"だって":-1049,"ていた":1833,"として":-4657,"ともに":-4517,"もので":1882,"一気に":-792,"初めて":-1512,"同時に":-8097,"大きな":-1255,"対して":-2721,"社会党":-3216}; 71 | this.TW3__ = {"いただ":-1734,"してい":1314,"として":-4314,"につい":-5483,"にとっ":-5989,"に当た":-6247,"ので,":-727,"ので、":-727,"のもの":-600,"れから":-3752,"十二月":-2287}; 72 | this.TW4__ = {"いう.":8576,"いう。":8576,"からな":-2348,"してい":2958,"たが,":1516,"たが、":1516,"ている":1538,"という":1349,"ました":5543,"ません":1097,"ようと":-4258,"よると":5865}; 73 | this.UC1__ = {"A":484,"K":93,"M":645,"O":-505}; 74 | this.UC2__ = {"A":819,"H":1059,"I":409,"M":3987,"N":5775,"O":646}; 75 | this.UC3__ = {"A":-1370,"I":2311}; 76 | this.UC4__ = {"A":-2643,"H":1809,"I":-1032,"K":-3450,"M":3565,"N":3876,"O":6646}; 77 | this.UC5__ = {"H":313,"I":-1238,"K":-799,"M":539,"O":-831}; 78 | this.UC6__ = {"H":-506,"I":-253,"K":87,"M":247,"O":-387}; 79 | this.UP1__ = {"O":-214}; 80 | this.UP2__ = {"B":69,"O":935}; 81 | this.UP3__ = {"B":189}; 82 | this.UQ1__ = {"BH":21,"BI":-12,"BK":-99,"BN":142,"BO":-56,"OH":-95,"OI":477,"OK":410,"OO":-2422}; 83 | this.UQ2__ = {"BH":216,"BI":113,"OK":1759}; 84 | this.UQ3__ = {"BA":-479,"BH":42,"BI":1913,"BK":-7198,"BM":3160,"BN":6427,"BO":14761,"OI":-827,"ON":-3212}; 85 | this.UW1__ = {",":156,"、":156,"「":-463,"あ":-941,"う":-127,"が":-553,"き":121,"こ":505,"で":-201,"と":-547,"ど":-123,"に":-789,"の":-185,"は":-847,"も":-466,"や":-470,"よ":182,"ら":-292,"り":208,"れ":169,"を":-446,"ん":-137,"・":-135,"主":-402,"京":-268,"区":-912,"午":871,"国":-460,"大":561,"委":729,"市":-411,"日":-141,"理":361,"生":-408,"県":-386,"都":-718,"「":-463,"・":-135}; 86 | this.UW2__ = {",":-829,"、":-829,"〇":892,"「":-645,"」":3145,"あ":-538,"い":505,"う":134,"お":-502,"か":1454,"が":-856,"く":-412,"こ":1141,"さ":878,"ざ":540,"し":1529,"す":-675,"せ":300,"そ":-1011,"た":188,"だ":1837,"つ":-949,"て":-291,"で":-268,"と":-981,"ど":1273,"な":1063,"に":-1764,"の":130,"は":-409,"ひ":-1273,"べ":1261,"ま":600,"も":-1263,"や":-402,"よ":1639,"り":-579,"る":-694,"れ":571,"を":-2516,"ん":2095,"ア":-587,"カ":306,"キ":568,"ッ":831,"三":-758,"不":-2150,"世":-302,"中":-968,"主":-861,"事":492,"人":-123,"会":978,"保":362,"入":548,"初":-3025,"副":-1566,"北":-3414,"区":-422,"大":-1769,"天":-865,"太":-483,"子":-1519,"学":760,"実":1023,"小":-2009,"市":-813,"年":-1060,"強":1067,"手":-1519,"揺":-1033,"政":1522,"文":-1355,"新":-1682,"日":-1815,"明":-1462,"最":-630,"朝":-1843,"本":-1650,"東":-931,"果":-665,"次":-2378,"民":-180,"気":-1740,"理":752,"発":529,"目":-1584,"相":-242,"県":-1165,"立":-763,"第":810,"米":509,"自":-1353,"行":838,"西":-744,"見":-3874,"調":1010,"議":1198,"込":3041,"開":1758,"間":-1257,"「":-645,"」":3145,"ッ":831,"ア":-587,"カ":306,"キ":568}; 87 | this.UW3__ = {",":4889,"1":-800,"−":-1723,"、":4889,"々":-2311,"〇":5827,"」":2670,"〓":-3573,"あ":-2696,"い":1006,"う":2342,"え":1983,"お":-4864,"か":-1163,"が":3271,"く":1004,"け":388,"げ":401,"こ":-3552,"ご":-3116,"さ":-1058,"し":-395,"す":584,"せ":3685,"そ":-5228,"た":842,"ち":-521,"っ":-1444,"つ":-1081,"て":6167,"で":2318,"と":1691,"ど":-899,"な":-2788,"に":2745,"の":4056,"は":4555,"ひ":-2171,"ふ":-1798,"へ":1199,"ほ":-5516,"ま":-4384,"み":-120,"め":1205,"も":2323,"や":-788,"よ":-202,"ら":727,"り":649,"る":5905,"れ":2773,"わ":-1207,"を":6620,"ん":-518,"ア":551,"グ":1319,"ス":874,"ッ":-1350,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278,"・":-3794,"一":-1619,"下":-1759,"世":-2087,"両":3815,"中":653,"主":-758,"予":-1193,"二":974,"人":2742,"今":792,"他":1889,"以":-1368,"低":811,"何":4265,"作":-361,"保":-2439,"元":4858,"党":3593,"全":1574,"公":-3030,"六":755,"共":-1880,"円":5807,"再":3095,"分":457,"初":2475,"別":1129,"前":2286,"副":4437,"力":365,"動":-949,"務":-1872,"化":1327,"北":-1038,"区":4646,"千":-2309,"午":-783,"協":-1006,"口":483,"右":1233,"各":3588,"合":-241,"同":3906,"和":-837,"員":4513,"国":642,"型":1389,"場":1219,"外":-241,"妻":2016,"学":-1356,"安":-423,"実":-1008,"家":1078,"小":-513,"少":-3102,"州":1155,"市":3197,"平":-1804,"年":2416,"広":-1030,"府":1605,"度":1452,"建":-2352,"当":-3885,"得":1905,"思":-1291,"性":1822,"戸":-488,"指":-3973,"政":-2013,"教":-1479,"数":3222,"文":-1489,"新":1764,"日":2099,"旧":5792,"昨":-661,"時":-1248,"曜":-951,"最":-937,"月":4125,"期":360,"李":3094,"村":364,"東":-805,"核":5156,"森":2438,"業":484,"氏":2613,"民":-1694,"決":-1073,"法":1868,"海":-495,"無":979,"物":461,"特":-3850,"生":-273,"用":914,"町":1215,"的":7313,"直":-1835,"省":792,"県":6293,"知":-1528,"私":4231,"税":401,"立":-960,"第":1201,"米":7767,"系":3066,"約":3663,"級":1384,"統":-4229,"総":1163,"線":1255,"者":6457,"能":725,"自":-2869,"英":785,"見":1044,"調":-562,"財":-733,"費":1777,"車":1835,"軍":1375,"込":-1504,"通":-1136,"選":-681,"郎":1026,"郡":4404,"部":1200,"金":2163,"長":421,"開":-1432,"間":1302,"関":-1282,"雨":2009,"電":-1045,"非":2066,"駅":1620,"1":-800,"」":2670,"・":-3794,"ッ":-1350,"ア":551,"グ":1319,"ス":874,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278}; 88 | this.UW4__ = {",":3930,".":3508,"―":-4841,"、":3930,"。":3508,"〇":4999,"「":1895,"」":3798,"〓":-5156,"あ":4752,"い":-3435,"う":-640,"え":-2514,"お":2405,"か":530,"が":6006,"き":-4482,"ぎ":-3821,"く":-3788,"け":-4376,"げ":-4734,"こ":2255,"ご":1979,"さ":2864,"し":-843,"じ":-2506,"す":-731,"ず":1251,"せ":181,"そ":4091,"た":5034,"だ":5408,"ち":-3654,"っ":-5882,"つ":-1659,"て":3994,"で":7410,"と":4547,"な":5433,"に":6499,"ぬ":1853,"ね":1413,"の":7396,"は":8578,"ば":1940,"ひ":4249,"び":-4134,"ふ":1345,"へ":6665,"べ":-744,"ほ":1464,"ま":1051,"み":-2082,"む":-882,"め":-5046,"も":4169,"ゃ":-2666,"や":2795,"ょ":-1544,"よ":3351,"ら":-2922,"り":-9726,"る":-14896,"れ":-2613,"ろ":-4570,"わ":-1783,"を":13150,"ん":-2352,"カ":2145,"コ":1789,"セ":1287,"ッ":-724,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637,"・":-4371,"ー":-11870,"一":-2069,"中":2210,"予":782,"事":-190,"井":-1768,"人":1036,"以":544,"会":950,"体":-1286,"作":530,"側":4292,"先":601,"党":-2006,"共":-1212,"内":584,"円":788,"初":1347,"前":1623,"副":3879,"力":-302,"動":-740,"務":-2715,"化":776,"区":4517,"協":1013,"参":1555,"合":-1834,"和":-681,"員":-910,"器":-851,"回":1500,"国":-619,"園":-1200,"地":866,"場":-1410,"塁":-2094,"士":-1413,"多":1067,"大":571,"子":-4802,"学":-1397,"定":-1057,"寺":-809,"小":1910,"屋":-1328,"山":-1500,"島":-2056,"川":-2667,"市":2771,"年":374,"庁":-4556,"後":456,"性":553,"感":916,"所":-1566,"支":856,"改":787,"政":2182,"教":704,"文":522,"方":-856,"日":1798,"時":1829,"最":845,"月":-9066,"木":-485,"来":-442,"校":-360,"業":-1043,"氏":5388,"民":-2716,"気":-910,"沢":-939,"済":-543,"物":-735,"率":672,"球":-1267,"生":-1286,"産":-1101,"田":-2900,"町":1826,"的":2586,"目":922,"省":-3485,"県":2997,"空":-867,"立":-2112,"第":788,"米":2937,"系":786,"約":2171,"経":1146,"統":-1169,"総":940,"線":-994,"署":749,"者":2145,"能":-730,"般":-852,"行":-792,"規":792,"警":-1184,"議":-244,"谷":-1000,"賞":730,"車":-1481,"軍":1158,"輪":-1433,"込":-3370,"近":929,"道":-1291,"選":2596,"郎":-4866,"都":1192,"野":-1100,"銀":-2213,"長":357,"間":-2344,"院":-2297,"際":-2604,"電":-878,"領":-1659,"題":-792,"館":-1984,"首":1749,"高":2120,"「":1895,"」":3798,"・":-4371,"ッ":-724,"ー":-11870,"カ":2145,"コ":1789,"セ":1287,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637}; 89 | this.UW5__ = {",":465,".":-299,"1":-514,"E2":-32768,"]":-2762,"、":465,"。":-299,"「":363,"あ":1655,"い":331,"う":-503,"え":1199,"お":527,"か":647,"が":-421,"き":1624,"ぎ":1971,"く":312,"げ":-983,"さ":-1537,"し":-1371,"す":-852,"だ":-1186,"ち":1093,"っ":52,"つ":921,"て":-18,"で":-850,"と":-127,"ど":1682,"な":-787,"に":-1224,"の":-635,"は":-578,"べ":1001,"み":502,"め":865,"ゃ":3350,"ょ":854,"り":-208,"る":429,"れ":504,"わ":419,"を":-1264,"ん":327,"イ":241,"ル":451,"ン":-343,"中":-871,"京":722,"会":-1153,"党":-654,"務":3519,"区":-901,"告":848,"員":2104,"大":-1296,"学":-548,"定":1785,"嵐":-1304,"市":-2991,"席":921,"年":1763,"思":872,"所":-814,"挙":1618,"新":-1682,"日":218,"月":-4353,"査":932,"格":1356,"機":-1508,"氏":-1347,"田":240,"町":-3912,"的":-3149,"相":1319,"省":-1052,"県":-4003,"研":-997,"社":-278,"空":-813,"統":1955,"者":-2233,"表":663,"語":-1073,"議":1219,"選":-1018,"郎":-368,"長":786,"間":1191,"題":2368,"館":-689,"1":-514,"E2":-32768,"「":363,"イ":241,"ル":451,"ン":-343}; 90 | this.UW6__ = {",":227,".":808,"1":-270,"E1":306,"、":227,"。":808,"あ":-307,"う":189,"か":241,"が":-73,"く":-121,"こ":-200,"じ":1782,"す":383,"た":-428,"っ":573,"て":-1014,"で":101,"と":-105,"な":-253,"に":-149,"の":-417,"は":-236,"も":-206,"り":187,"る":-135,"を":195,"ル":-673,"ン":-496,"一":-277,"中":201,"件":-800,"会":624,"前":302,"区":1792,"員":-1212,"委":798,"学":-960,"市":887,"広":-695,"後":535,"業":-697,"相":753,"社":-507,"福":974,"空":-822,"者":1811,"連":463,"郎":1082,"1":-270,"E1":306,"ル":-673,"ン":-496}; 91 | 92 | return this; 93 | } 94 | TinySegmenter.prototype.ctype_ = function(str) { 95 | for (var i in this.chartype_) { 96 | if (str.match(this.chartype_[i][0])) { 97 | return this.chartype_[i][1]; 98 | } 99 | } 100 | return "O"; 101 | } 102 | 103 | TinySegmenter.prototype.ts_ = function(v) { 104 | if (v) { return v; } 105 | return 0; 106 | } 107 | 108 | TinySegmenter.prototype.segment = function(input) { 109 | if (input == null || input == undefined || input == "") { 110 | return []; 111 | } 112 | var result = []; 113 | var seg = ["B3","B2","B1"]; 114 | var ctype = ["O","O","O"]; 115 | var o = input.split(""); 116 | for (i = 0; i < o.length; ++i) { 117 | seg.push(o[i]); 118 | ctype.push(this.ctype_(o[i])) 119 | } 120 | seg.push("E1"); 121 | seg.push("E2"); 122 | seg.push("E3"); 123 | ctype.push("O"); 124 | ctype.push("O"); 125 | ctype.push("O"); 126 | var word = seg[3]; 127 | var p1 = "U"; 128 | var p2 = "U"; 129 | var p3 = "U"; 130 | for (var i = 4; i < seg.length - 3; ++i) { 131 | var score = this.BIAS__; 132 | var w1 = seg[i-3]; 133 | var w2 = seg[i-2]; 134 | var w3 = seg[i-1]; 135 | var w4 = seg[i]; 136 | var w5 = seg[i+1]; 137 | var w6 = seg[i+2]; 138 | var c1 = ctype[i-3]; 139 | var c2 = ctype[i-2]; 140 | var c3 = ctype[i-1]; 141 | var c4 = ctype[i]; 142 | var c5 = ctype[i+1]; 143 | var c6 = ctype[i+2]; 144 | score += this.ts_(this.UP1__[p1]); 145 | score += this.ts_(this.UP2__[p2]); 146 | score += this.ts_(this.UP3__[p3]); 147 | score += this.ts_(this.BP1__[p1 + p2]); 148 | score += this.ts_(this.BP2__[p2 + p3]); 149 | score += this.ts_(this.UW1__[w1]); 150 | score += this.ts_(this.UW2__[w2]); 151 | score += this.ts_(this.UW3__[w3]); 152 | score += this.ts_(this.UW4__[w4]); 153 | score += this.ts_(this.UW5__[w5]); 154 | score += this.ts_(this.UW6__[w6]); 155 | score += this.ts_(this.BW1__[w2 + w3]); 156 | score += this.ts_(this.BW2__[w3 + w4]); 157 | score += this.ts_(this.BW3__[w4 + w5]); 158 | score += this.ts_(this.TW1__[w1 + w2 + w3]); 159 | score += this.ts_(this.TW2__[w2 + w3 + w4]); 160 | score += this.ts_(this.TW3__[w3 + w4 + w5]); 161 | score += this.ts_(this.TW4__[w4 + w5 + w6]); 162 | score += this.ts_(this.UC1__[c1]); 163 | score += this.ts_(this.UC2__[c2]); 164 | score += this.ts_(this.UC3__[c3]); 165 | score += this.ts_(this.UC4__[c4]); 166 | score += this.ts_(this.UC5__[c5]); 167 | score += this.ts_(this.UC6__[c6]); 168 | score += this.ts_(this.BC1__[c2 + c3]); 169 | score += this.ts_(this.BC2__[c3 + c4]); 170 | score += this.ts_(this.BC3__[c4 + c5]); 171 | score += this.ts_(this.TC1__[c1 + c2 + c3]); 172 | score += this.ts_(this.TC2__[c2 + c3 + c4]); 173 | score += this.ts_(this.TC3__[c3 + c4 + c5]); 174 | score += this.ts_(this.TC4__[c4 + c5 + c6]); 175 | // score += this.ts_(this.TC5__[c4 + c5 + c6]); 176 | score += this.ts_(this.UQ1__[p1 + c1]); 177 | score += this.ts_(this.UQ2__[p2 + c2]); 178 | score += this.ts_(this.UQ3__[p3 + c3]); 179 | score += this.ts_(this.BQ1__[p2 + c2 + c3]); 180 | score += this.ts_(this.BQ2__[p2 + c3 + c4]); 181 | score += this.ts_(this.BQ3__[p3 + c2 + c3]); 182 | score += this.ts_(this.BQ4__[p3 + c3 + c4]); 183 | score += this.ts_(this.TQ1__[p2 + c1 + c2 + c3]); 184 | score += this.ts_(this.TQ2__[p2 + c2 + c3 + c4]); 185 | score += this.ts_(this.TQ3__[p3 + c1 + c2 + c3]); 186 | score += this.ts_(this.TQ4__[p3 + c2 + c3 + c4]); 187 | var p = "O"; 188 | if (score > 0) { 189 | result.push(word); 190 | word = ""; 191 | p = "B"; 192 | } 193 | p1 = p2; 194 | p2 = p3; 195 | p3 = p; 196 | word += seg[i]; 197 | } 198 | result.push(word); 199 | 200 | return result; 201 | } 202 | 203 | lunr.TinySegmenter = TinySegmenter; 204 | }; 205 | 206 | })); --------------------------------------------------------------------------------