├── src └── sqlalchemy_model_factory │ ├── py.typed │ ├── __init__.py │ ├── registry.py │ ├── pytest.py │ ├── declarative.py │ ├── utils.py │ └── base.py ├── docs ├── requirements.txt ├── source │ ├── quickstart.rst │ ├── index.rst │ ├── api.rst │ ├── options.rst │ ├── conf.py │ ├── testing.rst │ ├── declarative.rst │ └── factories.rst └── Makefile ├── readthedocs.yml ├── tests ├── __init__.py ├── test_pytest.py ├── test_utils.py ├── test_base.py ├── test_declarative.py └── test_model_manager.py ├── Makefile ├── setup.cfg ├── CHANGELOG.md ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── pyproject.toml ├── .gitignore ├── README.md ├── LICENSE └── poetry.lock /src/sqlalchemy_model_factory/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | m2r2 2 | sphinx 3 | sphinx_rtd_theme 4 | sphinx_autodoc_typehints 5 | mistune<2.0.0 6 | . 7 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | .. toctree:: 5 | 6 | .. mdinclude:: ../../README.md 7 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | build: 2 | image: latest 3 | 4 | python: 5 | version: 3.6 6 | pip_install: true 7 | 8 | requirements: docs/requirements.txt 9 | sphinx: 10 | configuration: docs/conf.py 11 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm.session import sessionmaker 3 | 4 | 5 | def get_session(Base, *, session=None): 6 | if session is None: 7 | engine = create_engine("sqlite:///") 8 | Session = sessionmaker(engine) 9 | session = Session() 10 | 11 | Base.metadata.create_all(session.connection()) 12 | return session 13 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to SQLAlchemy Model Factory's documentation! 2 | ==================================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | Quickstart 9 | Testing (Pytest) 10 | Factories 11 | Declarative 12 | Options 13 | API 14 | 15 | .. include:: quickstart 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. _model_factory: 5 | .. automodule:: sqlalchemy_model_factory 6 | :members: ModelFactory 7 | 8 | 9 | Declarative 10 | ----------- 11 | 12 | .. automodule:: sqlalchemy_model_factory.declarative 13 | :members: declarative, DeclarativeMF 14 | 15 | 16 | Factory Utilities 17 | ----------------- 18 | 19 | .. automodule:: sqlalchemy_model_factory.utils 20 | :members: 21 | 22 | 23 | Pytest Plugin 24 | ------------- 25 | 26 | .. automodule:: sqlalchemy_model_factory.pytest 27 | :members: 28 | -------------------------------------------------------------------------------- /src/sqlalchemy_model_factory/__init__.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy_model_factory.base import ModelFactory 2 | from sqlalchemy_model_factory.declarative import declarative, factory 3 | from sqlalchemy_model_factory.registry import register_at, Registry, registry 4 | from sqlalchemy_model_factory.utils import autoincrement, fluent, for_model 5 | 6 | __all__ = [ 7 | "autoincrement", 8 | "declarative", 9 | "factory", 10 | "fluent", 11 | "for_model", 12 | "ModelFactory", 13 | "register_at", 14 | "Registry", 15 | "registry", 16 | ] 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lock install build test lint format publish 2 | .DEFAULT_GOAL := help 3 | 4 | lock: 5 | poetry lock 6 | 7 | install: 8 | poetry install 9 | 10 | build: 11 | poetry build 12 | 13 | test: 14 | coverage run -m py.test src tests -vv 15 | coverage combine --append 16 | coverage report 17 | coverage xml 18 | 19 | lint: 20 | flake8 src tests 21 | isort --check-only src tests 22 | pydocstyle src tests 23 | mypy src tests 24 | black --check src tests 25 | 26 | format: 27 | isort --quiet src tests 28 | black src tests 29 | 30 | publish: build 31 | poetry publish -u __token__ -p '${PYPI_PASSWORD}' --no-interaction 32 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = D203,E203,W503 3 | exclude = .git,__pycache__,docs,build,dist 4 | max-line-length = 200 5 | 6 | [pydocstyle] 7 | ignore = D1,D200,D202,D203,D204,D213,D406,D407,D413 8 | match_dir = ^[^\.{]((?!igrations).)* 9 | 10 | [mypy] 11 | strict_optional = True 12 | ignore_missing_imports = False 13 | warn_unused_ignores = True 14 | incremental = True 15 | plugins = sqlmypy 16 | 17 | [tool:pytest] 18 | doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL ELLIPSIS 19 | addopts = --ff --doctest-modules 20 | 21 | [coverage:run] 22 | source = src 23 | branch = True 24 | parallel = True 25 | omit = src/sqlalchemy_model_factory/pytest.py 26 | 27 | [coverage:report] 28 | show_missing = True 29 | skip_covered = True 30 | -------------------------------------------------------------------------------- /tests/test_pytest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy import Column, types 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy_model_factory.registry import Registry 5 | from tests import get_session 6 | 7 | Base = declarative_base() 8 | 9 | 10 | class Foo(Base): 11 | __tablename__ = "foo" 12 | 13 | id = Column(types.Integer(), autoincrement=True, primary_key=True) 14 | 15 | 16 | registry = Registry() 17 | 18 | 19 | @registry.register_at("foo") 20 | def new_foo(): 21 | return Foo() 22 | 23 | 24 | @pytest.fixture 25 | def mf_registry(): 26 | return registry 27 | 28 | 29 | @pytest.fixture 30 | def mf_session(): 31 | return get_session(Base) 32 | 33 | 34 | def test_mf_fixture(mf): 35 | foo = mf.foo.new() 36 | assert foo.id == 1 37 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | livehtml: 23 | sphinx-autobuild -b html $(SOURCEDIR) $(SPHINXOPTS) $(BUILDDIR)/html 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased](https://github.com/DanCardin/sqlalchemy-model-factory/compare/v0.3.1...HEAD) (2021-12-04) 4 | 5 | ### Features 6 | 7 | * Unify AccessGuard and Namespace. 6fe7de7 8 | 9 | 10 | ### [v0.3.1](https://github.com/DanCardin/sqlalchemy-model-factory/compare/v0.3.0...v0.3.1) (2020-09-18) 11 | 12 | #### Fixes 13 | 14 | * non-persistent merges model references returned from factories. 6d20a64 15 | 16 | 17 | ## [v0.3.0](https://github.com/DanCardin/sqlalchemy-model-factory/compare/v0.2.0...v0.3.0) (2020-09-17) 18 | 19 | ### Features 20 | 21 | * Add factory options. 4edd96c 22 | 23 | ### Fixes 24 | 25 | * Switch from unmaintained m2r to m2r2. 6642e3e 26 | * Namespace AttributeError reporting bug. f204add 27 | 28 | 29 | ## v0.2.0 (2020-05-19) 30 | 31 | ### Features 32 | 33 | * Add ability to nest namespaces. b4aad76 34 | 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Github Release/Publish PyPi 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | gh-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v1 14 | - name: Release 15 | uses: softprops/action-gh-release@v1 16 | with: 17 | generate_release_notes: true 18 | 19 | publish-pypi: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-python@v4 24 | with: 25 | python-version: "3.9" 26 | - name: Run image 27 | uses: abatilo/actions-poetry@v2.0.0 28 | with: 29 | poetry-version: 1.2.0 30 | 31 | - name: Publish 32 | env: 33 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 34 | run: | 35 | poetry config pypi-token.pypi $PYPI_TOKEN 36 | poetry publish --build 37 | -------------------------------------------------------------------------------- /docs/source/options.rst: -------------------------------------------------------------------------------- 1 | Options 2 | ======= 3 | 4 | Factory-level Options 5 | --------------------- 6 | 7 | Options can be supplied to :code:`register_at` at the factory level to alter 8 | the default behavior when calling a factory. 9 | 10 | Factory-level options include: 11 | 12 | * commit: :code:`True`/:code:`False` (default :code:`True`) 13 | 14 | Whether the given factory should commit the models it produces. 15 | 16 | * merge: :code:`True`/:code:`False` (default :code:`False`) 17 | 18 | Whether the given factory should :code:`Session.add` the models 19 | it produces, or :code:`Session.merge` them. 20 | 21 | This option can be useful for obtaining a reference to some model you 22 | **know** is already in the database, but you dont currently have a handle on. 23 | 24 | For example: 25 | 26 | .. code-block:: python 27 | 28 | @register_at("widget", name="default", merge=True) 29 | def default_widget(): 30 | return Widget() 31 | 32 | 33 | Call-level Options 34 | ------------------ 35 | 36 | All options available at the factory-level can also be provided at the call-site 37 | when calling the factories, although their arguments are postfixed with a 38 | trailing :code:`_` to avoid colliding with normal factory arguments. 39 | 40 | .. code-block:: python 41 | 42 | def test_widget(mf): 43 | widget = mf.widget.default(merge_=True, commit_=True) 44 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "sqlalchemy-model-factory" 3 | version = "0.4.6" 4 | description = "A library to assist in generating models from a central location." 5 | authors = ["Dan Cardin "] 6 | license = "Apache-2.0" 7 | keywords = [ "sqlalchemy", "model", "factory", "pytest" ] 8 | repository = "https://github.com/dancardin/sqlalchemy-model-factory" 9 | packages = [ 10 | { include = "sqlalchemy_model_factory", from = "src" }, 11 | ] 12 | readme = 'README.md' 13 | include = [ 14 | "*.md", 15 | "py.typed", 16 | ] 17 | 18 | [tool.poetry.dependencies] 19 | python = ">=3.6.2,<4" 20 | 21 | sqlalchemy = "*" 22 | typing_extensions = ">=3.10" 23 | pytest = {version = ">=1.0", optional = true} 24 | 25 | [tool.poetry.dev-dependencies] 26 | black = "22.3.0" 27 | coverage = [ 28 | {version = ">=7", python = ">=3.7"}, 29 | {version = ">=6", python = "<3.7"}, 30 | ] 31 | flake8 = "*" 32 | isort = ">=5" 33 | mypy = "*" 34 | pydocstyle = ">=4.0.0" 35 | pytest = "*" 36 | sqlalchemy-stubs = "*" 37 | 38 | [tool.poetry.extras] 39 | pytest = ['pytest'] 40 | 41 | [tool.poetry.plugins.pytest11] 42 | model_manager = "sqlalchemy_model_factory.pytest" 43 | 44 | [tool.isort] 45 | default_section = 'FIRSTPARTY' 46 | known_first_party = 'tests' 47 | length_sort = false 48 | order_by_type = false 49 | profile = 'black' 50 | 51 | [build-system] 52 | requires = ["poetry_core>=1.0.0"] 53 | build-backend = "poetry.core.masonry.api" 54 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy_model_factory.utils import fluent 3 | 4 | 5 | class Test_fluent: 6 | def test_no_args(self): 7 | def foo(): 8 | return 5 9 | 10 | result = fluent(foo).bind() 11 | assert result == 5 12 | 13 | def test_bind_error(self): 14 | def foo(bind): 15 | pass 16 | 17 | with pytest.raises(ValueError): 18 | fluent(foo) 19 | 20 | def test_duplicate_options_unavailable(self): 21 | def foo(bar, baz, bay): 22 | pass 23 | 24 | with pytest.raises(AttributeError): 25 | fluent(foo).bar(1).bar(4) 26 | 27 | def test_call_before_result(self): 28 | def foo(bar): 29 | return bar 30 | 31 | result = fluent(foo).bar(4).bind(call_before=lambda _, __: ((5,), {})) 32 | assert result == 5 33 | 34 | def test_call_before_no_result(self, capsys): 35 | def foo(bar): 36 | return bar 37 | 38 | result = fluent(foo).bar(4).bind(call_before=print) 39 | assert result == 4 40 | assert capsys.readouterr().out == "[4] {}\n" 41 | 42 | def test_call_after_result(self): 43 | def foo(bar): 44 | return bar 45 | 46 | result = fluent(foo).bar(4).bind(call_after=lambda a: a + 1) 47 | assert result == 5 48 | 49 | def test_call_after_no_result(self, capsys): 50 | def foo(bar): 51 | return bar 52 | 53 | result = fluent(foo).bar(4).bind(call_after=print) 54 | assert result == 4 55 | assert capsys.readouterr().out == "4\n" 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /src/sqlalchemy_model_factory/registry.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Generic, Optional, TypeVar 2 | 3 | 4 | class Registry: 5 | def __init__(self): 6 | self._registered_methods = {} 7 | 8 | def namespaces(self): 9 | return list(self._registered_methods) 10 | 11 | def methods(self, *namespace_path): 12 | return self._registered_methods[namespace_path] 13 | 14 | def clear(self): 15 | self._registered_methods = {} 16 | 17 | def register_at( 18 | self, 19 | *namespace_path, 20 | name="new", 21 | merge: Optional[bool] = None, 22 | commit: Optional[bool] = None, 23 | ): 24 | def wrapper(fn): 25 | registry_namespace = self._registered_methods.setdefault(namespace_path, {}) 26 | if name in registry_namespace: 27 | raise ValueError( 28 | "Name '{}' is already registered in namespace {}".format( 29 | name, namespace_path 30 | ) 31 | ) 32 | 33 | method = fn 34 | if not isinstance(fn, Method): 35 | method = Method(fn, merge=merge, commit=commit) 36 | 37 | registry_namespace[name] = method 38 | return fn 39 | 40 | return wrapper 41 | 42 | 43 | R = TypeVar("R") 44 | 45 | 46 | class Method(Generic[R]): 47 | def __init__( 48 | self, 49 | fn: Callable[..., R], 50 | commit: Optional[bool] = None, 51 | merge: Optional[bool] = None, 52 | ): 53 | self.fn = fn 54 | self.commit = commit 55 | self.merge = merge 56 | 57 | def __repr__(self): 58 | result = f"{self.__class__.__name__}({self.fn}" 59 | if self.commit is not None: 60 | result += f", commit={self.commit}" 61 | 62 | if self.merge is not None: 63 | result += f", merge={self.merge}" 64 | result += ")" 65 | return result 66 | 67 | def __call__(self, *args, **kwargs) -> R: 68 | return self.fn(*args, **kwargs) 69 | 70 | 71 | registry = Registry() 72 | register_at = registry.register_at 73 | -------------------------------------------------------------------------------- /src/sqlalchemy_model_factory/pytest.py: -------------------------------------------------------------------------------- 1 | """A pytest plugin as a simplified way to use the ModelFactory. 2 | 3 | General usage requires the user to define either a `mf_engine` or a `mf_session` fixture. 4 | Once defined, they can have their tests depend on the exposed `mf` fixture, which should 5 | give them access to any factory functions on which they've called `register_at`. 6 | """ 7 | from sqlalchemy import create_engine 8 | from sqlalchemy.orm.session import sessionmaker 9 | from sqlalchemy_model_factory.base import ModelFactory 10 | from sqlalchemy_model_factory.registry import registry, Registry 11 | 12 | try: 13 | import pytest 14 | except ImportError: 15 | 16 | class pytest: # type:ignore 17 | """Guard against pytest not being installed. 18 | 19 | The below function will simply act as a normal function if pytest is not installed. 20 | """ 21 | 22 | def fixture(fn): 23 | return fn 24 | 25 | 26 | def create_registry_fixture(factory_or_registry): 27 | if isinstance(factory_or_registry, Registry): 28 | registry = factory_or_registry 29 | else: 30 | registry = factory_or_registry.registry 31 | 32 | def fixture(): 33 | return registry 34 | 35 | return pytest.fixture(fixture) 36 | 37 | 38 | @pytest.fixture 39 | def mf_registry(): 40 | """Define a default fixture for the general case where the default registry is used.""" 41 | return registry 42 | 43 | 44 | @pytest.fixture 45 | def mf_engine(): 46 | """Define a default fixture in for the database engine.""" 47 | return create_engine("sqlite:///") 48 | 49 | 50 | @pytest.fixture 51 | def mf_session(mf_engine): 52 | """Define a default fixture in for the session, in case the user defines only `mf_engine`.""" 53 | Session = sessionmaker(mf_engine) 54 | session = Session() 55 | try: 56 | yield session 57 | finally: 58 | session.close() 59 | 60 | 61 | @pytest.fixture 62 | def mf_config(): 63 | """Define a default fixture in for the model factory configuration.""" 64 | return {} 65 | 66 | 67 | @pytest.fixture 68 | def mf(mf_registry, mf_session, mf_config): 69 | """Define a fixture for use of the ModelFactory in tests.""" 70 | with ModelFactory(mf_registry, mf_session, options=mf_config) as model_manager: 71 | yield model_manager 72 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | # Test our minimum version bound, the highest version available, 21 | # and something in the middle (i.e. what gets run locally). 22 | python-version: ["3.7", "3.9", "3.11"] 23 | sqlalchemy-version: ["1.3.0", "1.4.0"] 24 | 25 | name: Python ${{ matrix.python-version }} Tests 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Set up Python 29 | uses: actions/setup-python@v4 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | architecture: x64 33 | 34 | - name: Install poetry 35 | uses: abatilo/actions-poetry@v2.0.0 36 | with: 37 | poetry-version: 1.2.0 38 | 39 | - name: Set up cache 40 | uses: actions/cache@v3 41 | with: 42 | path: ~/.cache/pypoetry/virtualenvs 43 | key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} 44 | restore-keys: | 45 | ${{ runner.os }}-poetry- 46 | 47 | - name: Install dependencies 48 | run: poetry install 49 | 50 | - name: Install specific sqlalchemy version 51 | run: | 52 | poetry run pip install 'sqlalchemy~=${{ matrix.sqlalchemy-version }}' 53 | 54 | - if: ${{ matrix.python-version == '3.9' }} 55 | run: poetry run make lint 56 | 57 | - run: poetry run make test 58 | 59 | - name: Store test result artifacts 60 | uses: actions/upload-artifact@v3 61 | with: 62 | path: coverage.xml 63 | 64 | - name: Coveralls 65 | env: 66 | COVERALLS_FLAG_NAME: run-${{ inputs.working-directory }} 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | COVERALLS_PARALLEL: true 69 | run: | 70 | pip install tomli coveralls 71 | coveralls --service=github 72 | 73 | finish: 74 | needs: 75 | - test 76 | runs-on: ubuntu-latest 77 | steps: 78 | - name: Coveralls Finished 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | run: | 82 | pip install tomli coveralls 83 | coveralls --service=github --finish 84 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("..")) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "SQLAlchemy Model Factory" 22 | copyright = "2019, Dan Cardin" 23 | author = "Dan Cardin" 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = "0.1.0" 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "m2r2", 36 | "sphinx.ext.autosectionlabel", 37 | "sphinx.ext.intersphinx", 38 | "sphinx.ext.autodoc", 39 | "sphinx.ext.napoleon", 40 | "sphinx_autodoc_typehints", 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ["_templates"] 45 | 46 | # List of patterns, relative to source directory, that match files and 47 | # directories to ignore when looking for source files. 48 | # This pattern also affects html_static_path and html_extra_path. 49 | exclude_patterns = [] 50 | 51 | 52 | # -- Options for HTML output ------------------------------------------------- 53 | 54 | # The theme to use for HTML and HTML Help pages. See the documentation for 55 | # a list of builtin themes. 56 | # 57 | html_theme = "sphinx_rtd_theme" 58 | 59 | # Add any paths that contain custom static files (such as style sheets) here, 60 | # relative to this directory. They are copied after the builtin static files, 61 | # so a file named "default.css" will overwrite the builtin "default.css". 62 | html_static_path = ["_static"] 63 | 64 | autoclass_content = "both" 65 | 66 | intersphinx_mapping = { 67 | "python": ("https://docs.python.org/3", None), 68 | "sqlalchemy": ("https://docs.sqlalchemy.org/en/13/", None), 69 | } 70 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy import Column, types 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy_model_factory.base import ModelFactory, Namespace 5 | from sqlalchemy_model_factory.registry import Method, Registry 6 | from tests import get_session 7 | 8 | 9 | class TestNamespace: 10 | def test_empty_namespace_from_registry(self): 11 | Namespace.from_registry(Registry()) 12 | 13 | def test_empty_namespace(self): 14 | n = Namespace(None) 15 | with pytest.raises(AttributeError) as e: 16 | n.foo() 17 | 18 | assert "no attribute 'foo'" in str(e.value) 19 | assert "methods include: N/A" in str(e.value) 20 | 21 | def test_expose_available_methods_in_error(self): 22 | n = Namespace(None, foo=None, bar=None) 23 | with pytest.raises(AttributeError) as e: 24 | n.wat() 25 | 26 | assert "no attribute 'wat'" in str(e.value) 27 | assert "methods include: foo, bar" in str(e.value) 28 | 29 | def test_repr(self): 30 | n = Namespace(None, foo=None, bar=Method(4), baz=Namespace(4)) 31 | result = repr(n) 32 | assert result == "Namespace(foo=None, bar=Method(4), baz=Namespace(__call__=4))" 33 | 34 | def test_repr_merge_commit(self): 35 | n = Namespace( 36 | None, foo=None, bar=Method(4, merge=True, commit=True), baz=Namespace(4) 37 | ) 38 | result = repr(n) 39 | assert ( 40 | result 41 | == "Namespace(foo=None, bar=Method(4, commit=True, merge=True), baz=Namespace(__call__=4))" 42 | ) 43 | 44 | 45 | class TestModelFactory: 46 | Base = declarative_base() 47 | 48 | class Foo(Base): 49 | __tablename__ = "foo" 50 | 51 | id = Column(types.Integer(), autoincrement=True, primary_key=True) 52 | 53 | def test_it_allows_method_and_namespace_to_share_a_name(self): 54 | 55 | session = get_session(self.Base) 56 | 57 | registry = Registry() 58 | 59 | @registry.register_at("foo", "bar", name="baz") 60 | def baz(): 61 | return self.Foo(id=4) 62 | 63 | @registry.register_at("foo", name="bar") 64 | def bar(): 65 | return self.Foo(id=10) 66 | 67 | with ModelFactory(registry, session) as mf: 68 | bar_result = mf.foo.bar() 69 | assert bar_result.id == 10 70 | 71 | baz_result = mf.foo.bar.baz() 72 | assert baz_result.id == 4 73 | 74 | foos = session.query(self.Foo).all() 75 | assert len(foos) == 2 76 | 77 | def test_cannot_call_namespace_with_no_method(self): 78 | session = get_session(self.Base) 79 | 80 | registry = Registry() 81 | 82 | @registry.register_at("foo", "bar", name="baz") 83 | def baz(): 84 | return self.Foo(id=4) 85 | 86 | with ModelFactory(registry, session) as mf: 87 | with pytest.raises(RuntimeError): 88 | mf.foo() 89 | -------------------------------------------------------------------------------- /docs/source/testing.rst: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | A (or maybe **the**) primary usecase for this package is is in writing test tests 5 | concisely, ergonmically, and readably. 6 | 7 | To that end, we integrate with the testing framework in order to provide good UX in your 8 | tests. 9 | 10 | Pytest 11 | ------ 12 | 13 | We provide default implementations of a couple of pytest fixtures: :code:`mf_engine`, 14 | :code:`mf_session`, and :code:`mf_config`. However this assumes you're okay running your code as though it's 15 | executed in SQLite, and with default session parameters. 16 | 17 | If your system will work under those conditions, great! Simply go on and use the `mf` fixture 18 | which gives you a handle on a :code:`ModelFactory` 19 | 20 | .. code-block:: python 21 | 22 | from sqlalchemy_model_factory import registry 23 | 24 | @registry.register_at('foo') 25 | def new_foo(): 26 | return Foo() 27 | 28 | def test_foo(mf): 29 | foo = mf.foo.new() 30 | assert isinstance(foo, Foo) 31 | 32 | 33 | If, however, you make use of feature not available in SQLite, you may need a handle on a real 34 | database engine. Supposing you've got a postgres database available at :code:`db:5432`, you can 35 | put the following into your :code:`tests/conftest.py`. 36 | 37 | .. code-block:: python 38 | 39 | import pytest 40 | from sqlalchemy import create_engine 41 | 42 | @pytest.fixture 43 | def mf_engine(): 44 | return create_engine('psycopg2+postgresql://db:5432') 45 | 46 | # now the `mf` fixture should work 47 | 48 | Furthermore, if your application works in a context where you assume your :code:`session` has 49 | particular options set, you can similarly plug in your own session. 50 | 51 | .. code-block:: python 52 | 53 | import pytest 54 | from sqlalchemy.orm.session import sessionmaker 55 | 56 | @pytest.fixture 57 | def mf_session(mf_engine): 58 | Session = sessionmaker() # Set your options 59 | return Session(bind=engine) 60 | 61 | # now the `mf` fixture should work 62 | 63 | 64 | Finally, there are a set of hooks through which you can configure the behavior of the :code:`ModelFactory` 65 | itself through the :code:`mf_config` fixture. If defined, this fixture should return a :code:`dict`, 66 | the contents of which would be the config options available. 67 | 68 | Below is defined, the equivalent of a maximally defined :code:`mf_config` fixture with all the 69 | values set to their defaults. **Note** That as a user, you only need to include options which 70 | you want overridden from their defaults. 71 | 72 | .. code-block:: python 73 | 74 | @pytest.fixture 75 | def mf_config(): 76 | return { 77 | # Whether the calling of all factory functions should commit, or just flush. 78 | "commit": True, 79 | 80 | # Whether the actions performed by the model-factory should attempt to revert. Certain 81 | # test circumstances (like complex relationships, or direct sql `execute` calls might 82 | # mean cleanup will fail an otherwise valid test. 83 | "cleanup": True, 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/dancardin/sqlalchemy-model-factory/workflows/build/badge.svg)](https://github.com/dancardin/sqlalchemy-model-factory/actions) [![codecov](https://codecov.io/gh/DanCardin/sqlalchemy-model-factory/branch/main/graph/badge.svg)](https://codecov.io/gh/DanCardin/sqlalchemy-model-factory) [![Documentation Status](https://readthedocs.org/projects/sqlalchemy-model-factory/badge/?version=latest)](https://sqlalchemy-model-factory.readthedocs.io/en/latest/?badge=latest) 2 | 3 | sqlalchemy-model-factory aims to make it easy to write factory functions for sqlalchemy 4 | models, particularly for use in testing. 5 | 6 | It should make it easy to define as many factories as you might want, with as little 7 | boilerplate as possible, while remaining as unopinionated as possible about the behavior 8 | going in your factories. 9 | 10 | ## Installation 11 | 12 | ```python 13 | pip install sqlalchemy-model-factory 14 | ``` 15 | 16 | ## Usage 17 | 18 | Suppose you've defined a `Widget` model, and for example you want to test some API code 19 | that queries for `Widget` instances. Couple of factory functions might look like so: 20 | 21 | ```python 22 | # tests/test_example_which_uses_pytest 23 | from sqlalchemy_model_factory import autoincrement, register_at 24 | from . import models 25 | 26 | @register_at('widget') 27 | def new_widget(name, weight, color, size, **etc): 28 | """My goal is to allow you to specify *all* the options a widget might require. 29 | """ 30 | return Widget(name, weight, color, size, **etc) 31 | 32 | @register_at('widget', name='default') 33 | @autoincrement 34 | def new_default_widget(autoincrement=1): 35 | """My goal is to give you a widget with as little input as possible. 36 | """ 37 | # I'm gonna call the other factory function...because i can! 38 | return new_widget( 39 | f'default_name{autoincrement}', 40 | weight=autoincrement, 41 | color='rgb({0}, {0}, {0})'.format(autoincrement), 42 | size=autoincrement, 43 | ) 44 | ``` 45 | 46 | What this does, is register those functions to the registry of factory functions, within 47 | the "widget" namespace, at the `name` (defaults to `new`) location in the namespace. 48 | 49 | So when I go to write a test, all I need to do is accept the `mf` fixture (and lets say 50 | a `session` db connection fixture to make assertions against) and I can call all the 51 | factories that have been registered. 52 | 53 | ```python 54 | def test_example_model(mf, session): 55 | widget1 = mf.widget.new('name', 1, 'rgb(0, 0, 0)', 1) 56 | widget2 = mf.widget.default() 57 | widget3 = mf.widget.default() 58 | widget4 = mf.widget.default() 59 | 60 | widgets = session.query(Widget).all() 61 | assert len(widgets) == 4 62 | assert widgets[0].name == 'name' 63 | assert widgets[1].id == widget2.id 64 | assert widgets[2].name == widget3.name 65 | assert widgets[3].color == 'rgb(3, 3, 3)' 66 | ``` 67 | 68 | In a simple toy example, where you don't gain much on the calls themselves the benefits 69 | are primarily: 70 | 71 | - The instances are automatically put into the database and cleaned up after the test. 72 | - You can make assertions without hardcoding the values, because you get back a handle on the object. 73 | 74 | But as the graph of models required to set up a particular scenario grows: 75 | 76 | - You can define factories as complex as you want 77 | - They can create related objects and assign them to relationships 78 | - They can be given sources of randomness or uniqueness to not violate constraints 79 | - They can compose with eachother (when called normally, they're the same as the original function). 80 | -------------------------------------------------------------------------------- /docs/source/declarative.rst: -------------------------------------------------------------------------------- 1 | Declarative API 2 | =============== 3 | There are a few benefits to declaratively specifying the factory function tree: 4 | 5 | - The vanilla ``@register_at`` decorator is dynamic and string based, which 6 | leaves no direct import or path to trace back to the implementing function 7 | from a ``ModelFactory`` instance. 8 | - There is, perhaps, an alternative declarative implementation which ensures 9 | the yielded ``ModelFactory`` literally **is** the heirarchy specified. 10 | 11 | - As is, if you're in a context in which you can type annotate the model 12 | factory, then this enables typical LSP features like "Go to Definition". 13 | 14 | In the most common cases, such as with ``pytest``, you'll be being handed 15 | an untyped ``mf`` (:ref:`model_factory`) fixture instance. Here, you can 16 | type annotate the argument as being your ``@declarative`` ly decorated 17 | class. 18 | 19 | 20 | .. code-block:: python 21 | 22 | # some_module.py 23 | class other: 24 | def new(): 25 | ... 26 | 27 | # factory.py 28 | from some_module import other 29 | 30 | @declarative 31 | class ModelFactory: 32 | other = other 33 | 34 | class namespace: 35 | def example(): 36 | ... 37 | 38 | # tests.py 39 | from sqlalchemy_model_factory.pytest import create_registry_fixture 40 | from factory import ModelFactory 41 | 42 | mf_registry = create_registry_fixture(ModelFactory) 43 | 44 | # `mf` being a sqlalchemy_model_factory-provided fixture. 45 | def test_factory(mf: ModelFactory): 46 | ... 47 | 48 | Alternatively, a registry can be provided to the decorator directly, 49 | ou have one pre-constructed. 50 | 51 | .. code-block:: python 52 | 53 | registry = Registry() 54 | 55 | @declarative(registry=registry) 56 | class ModelFactory: 57 | def fn(): 58 | ... 59 | 60 | .. note:: 61 | 62 | interior classes to the decorator, including both the root class, as 63 | well as any nested class or attributes which are raw types, will be 64 | instantiated. This is notable, primarily in the event that an `__init__` 65 | is defined on the class for whatever reason. Each class will be instantiated 66 | once without arguments. 67 | 68 | 69 | Conversion from ``@register_at`` 70 | -------------------------------- 71 | If you have an existing body of model-factory functions registered using the 72 | ``@register_at`` pattern, you can incrementally adopt (and therefore incrementally 73 | get viable type hinting support) the declarative api. 74 | 75 | 76 | If you are importing ``from sqlalchemy_model_factory import register_at``, today, 77 | you can import ``from sqlalchemy_model_factory import registry``, and send that 78 | into the ``@declarative`` decorator: 79 | 80 | .. code-block:: python 81 | 82 | from sqlalchemy_model_factory import registry 83 | 84 | @declarative(registry=registry) 85 | class ModelFactory: 86 | def example(): 87 | ... 88 | 89 | Alternatively, you can switch to manually constructing your own ``Registry``, 90 | though you will need to change your ``@register_at`` calls to use it! 91 | 92 | .. code-block:: python 93 | 94 | from sqlalchemy_model_factory import Registry, declarative 95 | 96 | registry = Registry() 97 | 98 | @register_at("path", name="new") 99 | def new_path(): 100 | ... 101 | 102 | @declarative(registry=registry) 103 | class Base: 104 | def example(): 105 | ... 106 | 107 | Then once you make use of the annotation, say in some test: 108 | 109 | .. code-block:: python 110 | 111 | def test_path(mf: Base): 112 | mf.example() 113 | 114 | you should get go-to-definition and hinting support for the declaratively specified 115 | methods **only**. 116 | 117 | .. note:: 118 | 119 | You might see mypy type errors like ``Type[...] has no attribute "..."`` 120 | for ``@register_at``. You can either ignore these, or else apply the 121 | ``compat`` as a superclass to your declarative: 122 | 123 | .. code-block:: python 124 | 125 | from sqlalchemy_model_factory import declarative 126 | 127 | @declarative.declarative 128 | class Factory(declarative.compat): 129 | ... 130 | -------------------------------------------------------------------------------- /tests/test_declarative.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy import Column, create_engine, types 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy.orm.session import sessionmaker 5 | from sqlalchemy_model_factory import base 6 | from sqlalchemy_model_factory.declarative import ( 7 | compat, 8 | declarative, 9 | DeclarativeMF, 10 | factory, 11 | ) 12 | from sqlalchemy_model_factory.pytest import create_registry_fixture 13 | from sqlalchemy_model_factory.registry import Registry 14 | from tests import get_session 15 | 16 | Base = declarative_base() 17 | 18 | 19 | class Foo(Base): 20 | __tablename__ = "foo" 21 | 22 | id = Column(types.Integer(), autoincrement=True, primary_key=True) 23 | 24 | 25 | class Bar(Base): 26 | __tablename__ = "bar" 27 | 28 | pk = Column(types.Integer(), autoincrement=True, primary_key=True) 29 | 30 | 31 | class bar: 32 | @staticmethod 33 | def new(id: int): 34 | return Bar(pk=id) 35 | 36 | 37 | @declarative 38 | class ModelFactory: 39 | some_attribute = 5 40 | 41 | class foo: 42 | class nest: 43 | @staticmethod 44 | def default(id: int): 45 | return Foo(id=id) 46 | 47 | bar = bar 48 | 49 | 50 | mf_registry = create_registry_fixture(ModelFactory) 51 | 52 | 53 | def test_declarative_base(mf: ModelFactory, mf_session): 54 | session = get_session(Base, session=mf_session) 55 | 56 | foo = mf.foo.nest.default(5) 57 | assert foo.id == 5 58 | 59 | bar = mf.bar.new(3) 60 | assert bar.pk == 3 61 | 62 | foo = session.query(Foo).one() 63 | assert foo.id == 5 64 | 65 | bar = session.query(Bar).one() 66 | assert bar.pk == 3 67 | 68 | 69 | def test_declarative_argument(): 70 | registry = Registry() 71 | 72 | @declarative(registry=registry) 73 | class ModelFactory: 74 | @staticmethod 75 | def default(id: int): 76 | return Foo(id=id) 77 | 78 | session = get_session(Base) 79 | 80 | with base.ModelFactory(registry, session) as mf: 81 | foo = mf.default(5) 82 | 83 | foo = session.query(Foo).one() 84 | assert foo.id == 5 85 | 86 | 87 | def test_metaclass(): 88 | registry = Registry() 89 | 90 | class ModelFactory(DeclarativeMF, registry=registry): 91 | @staticmethod 92 | def default(id: int): 93 | return Foo(id=id) 94 | 95 | session = get_session(Base) 96 | 97 | with base.ModelFactory(registry, session) as mf: 98 | foo = mf.default(5) 99 | 100 | foo = session.query(Foo).one() 101 | assert foo.id == 5 102 | 103 | 104 | def test_non_staticmethods(): 105 | @declarative 106 | class ModelFactory: 107 | def default(self, id: int): 108 | return Foo(id=id) 109 | 110 | session = get_session(Base) 111 | 112 | with base.ModelFactory(ModelFactory.registry, session) as mf: 113 | foo = mf.default(5) 114 | 115 | foo = session.query(Foo).one() 116 | assert foo.id == 5 117 | 118 | 119 | def test_callable_namespace(): 120 | @declarative 121 | class ModelFactory: 122 | class ex: 123 | def __call__(self, id: int): 124 | return Foo(id=id * -1) 125 | 126 | def default(self, id: int): 127 | return Foo(id=id) 128 | 129 | session = get_session(Base) 130 | 131 | with base.ModelFactory(ModelFactory.registry, session) as mf: 132 | mf.ex(5) 133 | foos = session.query(Foo.id).all() 134 | assert foos == [(-5,)] 135 | 136 | mf.ex.default(5) 137 | 138 | foos = session.query(Foo.id).all() 139 | assert foos == [(-5,), (5,)] 140 | 141 | 142 | # Mixed-dynamic and declarative setup. 143 | mixed_registry = Registry() 144 | 145 | 146 | @mixed_registry.register_at("ex", name="new") 147 | def new(): 148 | return Foo(id=6) 149 | 150 | 151 | @declarative(registry=mixed_registry) 152 | class MixedModelFactory(compat): 153 | class ex(compat): 154 | @staticmethod 155 | def default(id: int): 156 | return Foo(id=id) 157 | 158 | 159 | @pytest.fixture 160 | def mixed_mf_session(mf_engine): 161 | mf_engine = create_engine("sqlite:///") 162 | Base.metadata.create_all(mf_engine) 163 | Session = sessionmaker(mf_engine) 164 | return Session() 165 | 166 | 167 | @pytest.fixture 168 | def mixed_mf(mixed_mf_session): 169 | with base.ModelFactory(mixed_registry, mixed_mf_session) as model_manager: 170 | yield model_manager 171 | 172 | 173 | def test_mixed_dynamic_and_declarative(mixed_mf: MixedModelFactory, mixed_mf_session): 174 | session = mixed_mf_session 175 | 176 | mixed_mf.ex.new() 177 | foos = session.query(Foo.id).all() 178 | assert foos == [(6,)] 179 | 180 | mixed_mf.ex.default(5) 181 | 182 | foos = session.query(Foo.id).all() 183 | assert foos == [(5,), (6,)] 184 | 185 | 186 | factory_fn_registry = Registry() 187 | 188 | 189 | @declarative(registry=factory_fn_registry) 190 | class FactoryFnMF: 191 | @staticmethod 192 | @factory(merge=True) 193 | def default(*, id: int) -> Foo: 194 | return Foo(id=id) 195 | 196 | class foo: 197 | @staticmethod 198 | @factory() 199 | def bar(*, pk: int) -> Bar: 200 | return Bar(pk=pk) 201 | 202 | 203 | @pytest.fixture 204 | def factory_mf(mixed_mf_session): 205 | with base.ModelFactory(factory_fn_registry, mixed_mf_session) as model_manager: 206 | yield model_manager 207 | 208 | 209 | def test_Factory(factory_mf: FactoryFnMF, mixed_mf_session): 210 | foo = factory_mf.default(id=5) 211 | 212 | # Make a 2nd one, it should merge instead of erroring. 213 | foo = factory_mf.default(id=5) 214 | 215 | bar = factory_mf.foo.bar(pk=6) 216 | 217 | the_foo = mixed_mf_session.query(Foo).one() 218 | assert foo.id == 5 219 | assert foo is the_foo 220 | 221 | the_bar = mixed_mf_session.query(Bar).one() 222 | assert bar.pk == 6 223 | assert bar is the_bar 224 | -------------------------------------------------------------------------------- /src/sqlalchemy_model_factory/declarative.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from sqlalchemy_model_factory.registry import Method, R, Registry 4 | 5 | 6 | def declarative(_cls=None, *, registry=None): 7 | """Decorate a base object on which factory functions reside. 8 | 9 | The primary benefit of declaratively specifying the factory function tree 10 | is that it enables references to the factory to be type aware, enabling 11 | things like "Go to Definition". 12 | 13 | *Note* interior classes to the decorator, including both the root class, as 14 | well as any nested class or attributes which are raw types, will be 15 | instantiated. This is notable, primarily in the event that an `__init__` 16 | is defined on the class for whatever reason. Each class will be instantiated 17 | once without arguments. 18 | 19 | Examples: 20 | >>> @declarative 21 | ... class ModelFactory: 22 | ... class namespace: 23 | ... def fn(): 24 | ... ... 25 | 26 | >>> from sqlalchemy_model_factory.pytest import create_registry_fixture 27 | >>> mf_registry = create_registry_fixture(ModelFactory) 28 | 29 | >>> def test_factory(mf: ModelFactory): 30 | ... ... 31 | 32 | Alternatively, a registry can be provided to the decorator directly, 33 | if you have one pre-constructed. 34 | 35 | >>> registry = Registry() 36 | >>> 37 | >>> @declarative(registry=registry) 38 | ... class ModelFactory: 39 | ... def fn(): 40 | ... ... 41 | 42 | *Note* due to the dynamic nature of fixtures, you must annotate the fixture 43 | argument to have the type of the root, declarative class. 44 | """ 45 | registry = registry or Registry() 46 | 47 | def _root_declarative(cls): 48 | """Perform the declarative mapping to the root class. 49 | 50 | This is distinct from the recursive `_declarative` function in that this: 51 | 52 | - Creates a registry if one was not provided overall. 53 | - Assigns the registry as a root attribute to the factory itself. 54 | creates 55 | """ 56 | _declarative(cls) 57 | 58 | cls.registry = registry 59 | return cls 60 | 61 | def _declarative(cls, *, context=None): 62 | """Traverse the heirarchy of objects on the root factory, recursively.""" 63 | context = context or [] 64 | 65 | if not hasattr(cls, "__dict__"): 66 | return 67 | 68 | # Instantiate classes to enable non-`staticmethod`s to work properly. 69 | if isinstance(cls, type): 70 | cls = cls() 71 | 72 | for name in dir(cls): 73 | attr = getattr(cls, name) 74 | is_method = isinstance(attr, Method) 75 | 76 | # Ignore private attributes. These classes are intended to be used as 77 | # an api, it makes no sense to collect "private" attributes! 78 | if name.startswith("_") and name != "__call__": 79 | continue 80 | 81 | if is_method: 82 | registration = registry.register_at(*context, name=name) 83 | registration(attr) 84 | continue 85 | 86 | # Callables can now be registered to the function registry. 87 | if name == "__call__": 88 | *rcontext, rname = context 89 | registration = registry.register_at(*rcontext, name=rname) 90 | registration(attr) 91 | continue 92 | 93 | # Finally, recurse into the object to search for more! 94 | new_context = [*context, name] 95 | _declarative(attr, context=new_context) 96 | 97 | if _cls is not None: 98 | return _root_declarative(_cls) 99 | return _root_declarative 100 | 101 | 102 | def factory(merge=None, commit=None) -> Callable[[Callable[..., R]], Method[R]]: 103 | """Annotate declaratively specified factory functions. 104 | 105 | This is an optional addition in the common case. Normally, factory functions 106 | will be automatically wrapped in `Method` in order to get the same behavior. 107 | 108 | However, if you need to customize the model-factory behavior in order to supply 109 | merge/commit/etc kwargs that would normally be supplied with `register_at`. 110 | """ 111 | 112 | def decorator(fn: Callable[..., R]) -> Method[R]: 113 | return Method(fn, merge=merge, commit=commit) 114 | 115 | return decorator 116 | 117 | 118 | class compat_meta(type): 119 | def __getattr__(self, attr): 120 | return super().__getattr__(attr) 121 | 122 | 123 | class compat(metaclass=compat_meta): 124 | """Compatibility base class for factory classes. 125 | 126 | Essentially what we're doing here is providing a base-class which 127 | will disable mypy checks for attributes which might not exist like 128 | 'Type[factory] has no attribute "foo"'. By defining a getattr, we opt 129 | that class out of such checks (because it cannot be statically defined), 130 | without modifying the class's behavior. 131 | 132 | This requires a metaclass because nested class definitions mean the type 133 | of your sub-class attribute is actually the type itself rather than an 134 | instance. 135 | """ 136 | 137 | 138 | class DeclarativeMF: 139 | """Provide an alternative to the class decorator for declarative base factories. 140 | 141 | Today there's no meaningful difference between the decorator and the subclass 142 | method, except the interface. 143 | 144 | Examples: 145 | >>> class ModelFactory(DeclarativeMF): 146 | ... def default(id: int = None): 147 | ... return Foo(id=id) 148 | 149 | or 150 | 151 | >>> registry = Registry() 152 | >>> 153 | >>> class ModelFactory(DeclarativeMF, registry=registry): 154 | ... def default(id: int = None): 155 | ... return Foo(id=id) 156 | """ 157 | 158 | @classmethod 159 | def __init_subclass__(cls, registry=None): 160 | declarative(cls, registry=registry) 161 | -------------------------------------------------------------------------------- /docs/source/factories.rst: -------------------------------------------------------------------------------- 1 | Factories 2 | ========= 3 | 4 | Basic 5 | ----- 6 | 7 | So the most basic factories that you can write are just functions which return models. 8 | 9 | .. code-block:: python 10 | 11 | from sqlalchemy_model_factory import register_at 12 | 13 | @register_at('foo') 14 | def new_foo(): 15 | return Foo() 16 | 17 | def test_foo(mf): 18 | foo = mf.foo.new() 19 | assert isinstance(foo, Foo) 20 | 21 | 22 | Nested 23 | ~~~~~~ 24 | 25 | Note, you can also create nested models through relationships and whatnot; and that will all work 26 | normally. 27 | 28 | .. code-block:: python 29 | 30 | from sqlalchemy_model_factory import register_at 31 | 32 | class Foo(Base): 33 | ... 34 | 35 | bar = relationship('Bar') 36 | 37 | class Bar(Base): 38 | ... 39 | 40 | baz = relationship('Baz') 41 | 42 | @register_at('foo') 43 | def new_foo(): 44 | ... 45 | 46 | 47 | General Use Functions 48 | --------------------- 49 | 50 | In some cases, you'll have a a function already handy that returns the equivalent signature 51 | of a model (or you just **want** a function that returns the signature). 52 | 53 | In this case, your function will act as the originally defined function when called normally, 54 | however when invoked in the context of the :code:`ModelFactory`, it returns the specified model 55 | instance. 56 | 57 | .. code-block:: python 58 | 59 | from sqlalchemy_model_factory import for_model, register_at 60 | 61 | @register_at('foo') 62 | @for_model(Foo) 63 | def new_foo(): 64 | return {'id': 1, 'name': 'bar'} 65 | 66 | def test_foo(mf): 67 | foo = mf.foo.new() 68 | assert foo.id == 1 69 | assert foo.name == 'bar' 70 | 71 | raw_foo = new_foo() 72 | assert raw_foo == {'id': 1, 'name': 'bar'} 73 | 74 | 75 | Sources of Uniqueness 76 | --------------------- 77 | 78 | Suppose you've got a column defined with a constraint, like 79 | `name = Column(types.Integer(), unique=True)`. 80 | 81 | Suddenly you'll need to parametrize your factory to accept a :code:`name` param. However if you 82 | **actually** don't care about the specific name values, you have a few options. 83 | 84 | Autoincrement 85 | ~~~~~~~~~~~~~ 86 | 87 | Automatically incrementing number values is one option. Your factory will be automatically 88 | supplied with an :code:`autoincrement` parameter, known to not collide with previously 89 | generated values. 90 | 91 | .. code-block:: python 92 | 93 | from sqlalchemy_model_factory import autoincrement, register_at 94 | 95 | @register_at('foo') 96 | @autoincrement 97 | def new_foo(autoincrement=1): 98 | return Foo(name=f'name{autoincrement}') 99 | 100 | def test_foo(mf): 101 | assert mf.foo.new().name == 'name1' 102 | assert mf.foo.new().name == 'name2' 103 | assert mf.foo.new().name == 'name3' 104 | 105 | 106 | Fluency 107 | ------- 108 | 109 | You've been working along and writing factories and you finally find yourself in a situation like 110 | this. 111 | 112 | .. code-block:: python 113 | 114 | @register_at('foo') 115 | @autoincrement 116 | def new_foo(name='name', height=2, width=3, depth=3, category='foo', autoincrement=1): 117 | ... 118 | 119 | And in the event your test requires a number of identical parameters across multiple calls, you 120 | might end up with test code that looks like. 121 | 122 | .. code-block:: python 123 | 124 | def test_foo(mf): 125 | width_4 = mf.foo.new(height=3, category='bar', width=4) 126 | width_5 = mf.foo.new(height=3, category='bar', width=5) 127 | width_6 = mf.foo.new(height=3, category='bar', width=6) 128 | width_7 = mf.foo.new(height=3, category='bar', width=7) 129 | ... 130 | 131 | The above (as a dirt simple example, that might be easily solved in different ways) has got **most** 132 | of its information duplicated unnecessarily. 133 | 134 | The "fluent" decorator 135 | ~~~~~~~~~~~~~~~~~~~~~~ 136 | 137 | A simple solution to this general problem category is the :code:`fluent` decorator. Which adapts a 138 | given callable to be able to be called in a fluent style. 139 | 140 | .. code-block:: python 141 | 142 | def test_foo(mf): 143 | bar_type_foo = mf.foo.new(3).category('bar') 144 | 145 | width_4 = bar_type_foo.width(4).bind() 146 | width_5 = bar_type_foo.width(5).bind() 147 | width_6 = bar_type_foo.width(6).bind() 148 | width_7 = bar_type_foo.width(7).bind() 149 | 150 | Now in this particular case, you could have just done a for-loop over the original set of calls, 151 | or maybe :code:`functools.partial` could have sufficed, but the fluent pattern is more generally 152 | useful than just in cases like this. 153 | 154 | Also from the callee's perspective, there's not necessarily any requirement that all the args have 155 | their parameter names supplied, so you might end up reading :code:`foo.new('a', 3, 4, 5, 'ro')`, 156 | which is arguably far less readable. 157 | 158 | To note, the :code:`bind` call at the end of each expression above is necessary to let the fluent 159 | calls know that its done being called (because as you might notice, we didn't call all the available 160 | methods we could have called). But this also serves as a convenient point at which to add custom 161 | behaviors. (for example you *could* supply `.bind(call_after=print)` to have it print out the final 162 | result of the function; see the api portion of the docs for the full set of options.) 163 | 164 | Class-style factories 165 | ~~~~~~~~~~~~~~~~~~~~~ 166 | 167 | From the perspective of the model factory, all factory "functions" are just callables, so you can 168 | always manually mimic something like the above :code:`fluent` decorator in a class so that you can 169 | implement your own custom behavior for each option. 170 | 171 | .. code-block:: python 172 | 173 | @register_at('foo') 174 | class NewFoo: 175 | def __init__(self, **kwargs): 176 | self.kwargs 177 | 178 | def name(self, name): 179 | self.__class__(**self.kwargs, name=name) 180 | 181 | def width(self, width): 182 | self.__class__(**self.kwargs, width=width) 183 | 184 | def bind(self): 185 | return Foo(**self.kwargs) 186 | 187 | 188 | Albeit, with the above, very naive implementation, your test code would end up looking like 189 | 190 | .. code-block:: python 191 | 192 | def test_foo(mf): 193 | bar_foo = mf.foo.new().name('bar') 194 | width_4 = bar_fo.width(4).bind() 195 | width_5 = bar_fo.width(5).bind() 196 | -------------------------------------------------------------------------------- /src/sqlalchemy_model_factory/utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import inspect 3 | from types import MappingProxyType 4 | from typing import Any, Callable, Dict, List, Optional, Tuple 5 | 6 | 7 | def autoincrement(fn: Optional[Callable] = None, *, start: int = 1): # pragma: no cover 8 | """Decorate registered callables to provide them with a source of uniqueness. 9 | 10 | Args: 11 | fn: The callable 12 | start: The starting number of the sequence to generate 13 | 14 | Examples: 15 | >>> @autoincrement 16 | ... def new(autoincrement=1): 17 | ... return autoincrement 18 | >>> new() 19 | 1 20 | >>> new() 21 | 2 22 | 23 | >>> @autoincrement(start=4) 24 | ... def new(autoincrement=1): 25 | ... return autoincrement 26 | >>> new() 27 | 4 28 | >>> new() 29 | 5 30 | """ 31 | 32 | def wrapper(fn): 33 | wrapper.initial = start 34 | 35 | @functools.wraps(fn) 36 | def decorator(*args, **kwargs): 37 | result = fn(*args, autoincrement=wrapper.initial, **kwargs) 38 | wrapper.initial += 1 39 | return result 40 | 41 | return decorator 42 | 43 | if fn: 44 | return wrapper(fn) 45 | return wrapper 46 | 47 | 48 | def for_model(typ): 49 | """Decorate a factory that returns a `Mapping` type in order to coerce it into the `typ`. 50 | 51 | This decorator is only invoked in the context of model factory usage. The intent is that 52 | a factory function could be more generally useful, such as to create API inputs, that 53 | also happen to correspond to the creation of a model when invoked during a test. 54 | 55 | Examples: 56 | >>> class Model: 57 | ... def __init__(self, **kwargs): 58 | ... self.kw = kwargs 59 | ... 60 | ... def __repr__(self): 61 | ... return f"Model(a={self.kw['a']}, b={self.kw['b']}, c={self.kw['c']})" 62 | 63 | >>> @for_model(Model) 64 | ... def new_model(a, b, c): 65 | ... return {'a': a, 'b': b, 'c': c} 66 | 67 | >>> new_model(1, 2, 3) 68 | {'a': 1, 'b': 2, 'c': 3} 69 | >>> new_model.for_model(1, 2, 3) 70 | Model(a=1, b=2, c=3) 71 | """ 72 | 73 | def wrapper(fn): 74 | def for_model(*args, **kwargs): 75 | result = fn(*args, **kwargs) 76 | return typ(**result) 77 | 78 | fn.for_model = for_model 79 | return fn 80 | 81 | return wrapper 82 | 83 | 84 | class fluent: 85 | """Decorate a function with `fluent` to enable it to be called in a "fluent" style. 86 | 87 | Examples: 88 | >>> @fluent 89 | ... def foo(a, b=None, *args, c=3, **kwargs): 90 | ... print(f'(a={a}, b={b}, c={c}, args={args}, kwargs={kwargs})') 91 | 92 | >>> foo.kwargs(much=True, surprise='wow').a(4).bind() 93 | (a=4, b=None, c=3, args=(), kwargs={'much': True, 'surprise': 'wow'}) 94 | 95 | >>> foo.args(True, 'wow').a(5).bind() 96 | (a=5, b=None, c=3, args=(True, 'wow'), kwargs={}) 97 | 98 | >>> partial = foo.a(1) 99 | >>> partial.b(5).bind() 100 | (a=1, b=5, c=3, args=(), kwargs={}) 101 | 102 | >>> partial.b(6).bind() 103 | (a=1, b=6, c=3, args=(), kwargs={}) 104 | """ 105 | 106 | def __init__(self, fn, signature=None, pending_args=None): 107 | self.fn = fn 108 | 109 | self.signature = signature or inspect.signature(fn) 110 | self.pending_args = pending_args or {} 111 | 112 | for parameter in self.signature.parameters.values(): 113 | if parameter.name == self.bind.__name__: 114 | raise ValueError( 115 | f"`fluent` reserves the name {self.bind.__name__}, please choose a different parameter name" 116 | ) 117 | 118 | if parameter.name in self.pending_args: 119 | continue 120 | 121 | setattr(self, parameter.name, self.__apply(parameter)) 122 | 123 | def __apply(self, parameter): 124 | @functools.wraps(self.fn) 125 | def wrapper(*args, **kwargs): 126 | signature = inspect.Signature(parameters=[parameter]) 127 | bound_args = signature.bind(*args, **kwargs) 128 | bound_args.apply_defaults() 129 | return self.__class__( 130 | self.fn, 131 | self.signature, 132 | {**self.pending_args, parameter.name: bound_args.arguments}, 133 | ) 134 | 135 | return wrapper 136 | 137 | def bind( 138 | self, 139 | *, 140 | call_before: Optional[Callable] = None, 141 | call_after: Optional[Callable] = None, 142 | ): 143 | """Finalize the call chain for a fluently called factory. 144 | 145 | Args: 146 | call_before: When provided, calls the given callable, supplying the args and kwargs 147 | being sent into the factory function before actually calling it. If the `call_before` 148 | function returns anything, the 2-tuple of (args, kwargs) will be replaced with the 149 | ones passed into the `call_before` function. 150 | call_after: When provided, calls the given callable, supplying the result of the factory 151 | function call after having called it. If the `call_after` function returns anything, 152 | the result of `call_after` will be replaced with the result of the factory function. 153 | """ 154 | unsupplied_args = set(self.signature.parameters) - set(self.pending_args) 155 | 156 | for arg in unsupplied_args: 157 | fn = getattr(self, arg) 158 | self = fn() 159 | 160 | args: List[Any] = [] 161 | kwargs: Dict[Any, Any] = {} 162 | 163 | for parameter in self.signature.parameters.values(): 164 | kind_map: Dict[Any, Tuple[Callable, bool]] = { 165 | parameter.POSITIONAL_ONLY: (args.append, True), 166 | parameter.POSITIONAL_OR_KEYWORD: (args.append, True), 167 | parameter.VAR_POSITIONAL: (args.extend, True), 168 | parameter.VAR_KEYWORD: (kwargs.update, True), 169 | parameter.KEYWORD_ONLY: (kwargs.update, False), 170 | } 171 | 172 | pending_arg = self.pending_args[parameter.name] 173 | 174 | update_fn, key_on_param = kind_map[parameter.kind] 175 | if key_on_param: 176 | update_fn(pending_arg[parameter.name]) 177 | else: 178 | update_fn(pending_arg) 179 | 180 | if call_before: 181 | call_before_result = call_before(args, MappingProxyType(kwargs)) 182 | if call_before_result: 183 | args, kwargs = call_before_result 184 | 185 | result = self.fn(*args, **kwargs) 186 | 187 | if call_after: 188 | call_after_result = call_after(result) 189 | if call_after_result: 190 | return call_after_result 191 | 192 | return result 193 | -------------------------------------------------------------------------------- /tests/test_model_manager.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy import Column, ForeignKey, types 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from sqlalchemy.orm import relationship 5 | from sqlalchemy_model_factory.base import ModelFactory 6 | from sqlalchemy_model_factory.registry import registry 7 | from sqlalchemy_model_factory.utils import for_model 8 | from tests import get_session 9 | 10 | Base = declarative_base() 11 | 12 | 13 | class Foo(Base): 14 | __tablename__ = "foo" 15 | 16 | id = Column(types.Integer(), autoincrement=True, primary_key=True) 17 | bar_id = Column(types.Integer(), ForeignKey("bar.id"), nullable=False) 18 | 19 | bar = relationship("Bar", uselist=False) 20 | 21 | 22 | class Bar(Base): 23 | __tablename__ = "bar" 24 | 25 | id = Column(types.Integer(), autoincrement=True, primary_key=True) 26 | 27 | bar2s = relationship( 28 | "Bar2", cascade="all, delete-orphan", passive_deletes=True, back_populates="bar" 29 | ) 30 | 31 | 32 | class Bar2(Base): 33 | __tablename__ = "bar2" 34 | 35 | id = Column(types.Integer(), autoincrement=True, primary_key=True) 36 | bar_id = Column( 37 | types.Integer(), ForeignKey("bar.id", ondelete="CASCADE"), nullable=False 38 | ) 39 | 40 | bar = relationship("Bar", back_populates="bar2s") 41 | 42 | 43 | class Baz(Base): 44 | __tablename__ = "baz" 45 | 46 | id = Column(types.Integer(), autoincrement=True, primary_key=True) 47 | bar_id = Column( 48 | types.Integer(), ForeignKey("bar.id", ondelete="CASCADE"), nullable=False 49 | ) 50 | 51 | bar = relationship("Bar") 52 | 53 | 54 | class TestRegistry: 55 | def setup(self): 56 | registry.clear() 57 | 58 | def test_default_function_name(self): 59 | registry.register_at("thing")(1) 60 | assert registry.namespaces() == [("thing",)] 61 | assert registry.methods("thing")["new"].fn == 1 62 | 63 | def test_explicit_function_name(self): 64 | registry.register_at("thing", name="foo")(1) 65 | assert registry.namespaces() == [("thing",)] 66 | assert registry.methods("thing")["foo"].fn == 1 67 | 68 | def test_registration_duplicate(self): 69 | registry.register_at("thing", name="foo")(1) 70 | with pytest.raises(ValueError): 71 | registry.register_at("thing", name="foo")(1) 72 | 73 | 74 | class TestModelFactory: 75 | def setup(self): 76 | registry.clear() 77 | 78 | def test_exit_removal(self): 79 | session = get_session(Base) 80 | 81 | @registry.register_at("thing") 82 | def new_thing(): 83 | return [Bar(), Bar()] 84 | 85 | @registry.register_at("thing", name="foo") 86 | def new_thing_foo(arg1): 87 | return Foo(bar=Bar()) 88 | 89 | with ModelFactory(registry, session) as mm: 90 | mm.thing.new() 91 | mm.thing.foo(1) 92 | 93 | assert len(session.query(Foo).all()) == 1 94 | assert len(session.query(Bar).all()) == 3 95 | 96 | assert len(session.query(Foo).all()) == 0 97 | assert len(session.query(Bar).all()) == 0 98 | 99 | def test_mid_session_delete(self): 100 | session = get_session(Base) 101 | 102 | @registry.register_at("thing") 103 | def new_thing(): 104 | return [Bar(), Bar()] 105 | 106 | @registry.register_at("thing", name="foo") 107 | def new_thing_foo(arg1): 108 | return Foo(bar=Bar()) 109 | 110 | with ModelFactory(registry, session) as mm: 111 | bar1, bar2 = mm.thing.new() 112 | foo = mm.thing.foo(1) 113 | 114 | assert len(session.query(Foo).all()) == 1 115 | assert len(session.query(Bar).all()) == 3 116 | 117 | session.delete(bar1) 118 | assert len(session.query(Bar).all()) == 2 119 | 120 | session.delete(foo) 121 | assert len(session.query(Foo).all()) == 0 122 | assert len(session.query(Bar).all()) == 2 123 | 124 | assert len(session.query(Foo).all()) == 0 125 | assert len(session.query(Bar).all()) == 0 126 | 127 | def test_cascade_delete(self): 128 | session = get_session(Base) 129 | 130 | @registry.register_at("bar") 131 | def new_bar(): 132 | return Bar() 133 | 134 | @registry.register_at("baz") 135 | def new_baz(bar): 136 | return Baz(bar=bar) 137 | 138 | with ModelFactory(registry, session) as mm: 139 | bar = mm.bar.new() 140 | mm.baz.new(bar) 141 | 142 | assert len(session.query(Bar).all()) == 0 143 | assert len(session.query(Baz).all()) == 0 144 | 145 | def test_cascade_delete_mapping_table(self): 146 | session = get_session(Base) 147 | 148 | @registry.register_at("bar") 149 | def new_bar(): 150 | return Bar() 151 | 152 | @registry.register_at("bar2") 153 | def new_bar2(bar): 154 | return Bar2(bar=bar) 155 | 156 | with ModelFactory(registry, session) as mm: 157 | bar = mm.bar.new() 158 | mm.bar2.new(bar) 159 | session.delete(bar) 160 | 161 | assert len(session.query(Bar).all()) == 0 162 | assert len(session.query(Bar2).all()) == 0 163 | 164 | def test_for_model(self): 165 | session = get_session(Base) 166 | 167 | @registry.register_at("thing") 168 | @for_model(Foo) 169 | def new_thing(): 170 | bar = Bar(id=1) 171 | return {"bar_id": bar.id} 172 | 173 | with ModelFactory(registry, session) as mm: 174 | foo = mm.thing.new() 175 | assert isinstance(foo, Foo) 176 | 177 | 178 | class TestNamespaceNesting: 179 | def setup(self): 180 | registry.clear() 181 | 182 | def test_namespace_nesting(self): 183 | session = get_session(Base) 184 | 185 | @registry.register_at("name", "space", "nesting", name="new") 186 | def new_bar(): 187 | return Bar(id=1) 188 | 189 | with ModelFactory(registry, session) as mm: 190 | bar = mm.name.space.nesting.new() 191 | assert isinstance(bar, Bar) 192 | 193 | def test_namespace_nesting_at_different_levels(self): 194 | session = get_session(Base) 195 | 196 | @registry.register_at("name", "space", "nesting", name="new") 197 | def new_bar(): 198 | return Bar(id=1) 199 | 200 | @registry.register_at("name", "space", name="new") 201 | def new_foo(bar): 202 | return Foo(bar=bar) 203 | 204 | with ModelFactory(registry, session) as mm: 205 | bar = mm.name.space.nesting.new() 206 | assert isinstance(bar, Bar) 207 | 208 | foo = mm.name.space.new(bar) 209 | assert isinstance(foo, Foo) 210 | 211 | def test_logical_error_on_namespace_callable(self): 212 | session = get_session(Base) 213 | 214 | @registry.register_at("name", "space", "nesting", name="new") 215 | def new_bar(): 216 | return Bar(id=1) 217 | 218 | with ModelFactory(registry, session) as mm: 219 | with pytest.raises(AttributeError) as e: 220 | mm.name.space.new() 221 | 222 | assert "Available methods include:" in str(e.value) 223 | assert "Available nested namespaces include:" in str(e.value) 224 | 225 | 226 | def test_merge(): 227 | session = get_session(Base) 228 | 229 | @registry.register_at("bar", merge=True) 230 | def new_bar(): 231 | return Bar(id=1) 232 | 233 | with ModelFactory(registry, session) as mm: 234 | session.add(Bar(id=1)) 235 | session.commit() 236 | 237 | bar = mm.bar.new() 238 | 239 | session.add(Bar2(bar=bar)) 240 | session.commit() 241 | assert bar.id == 1 242 | -------------------------------------------------------------------------------- /src/sqlalchemy_model_factory/base.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Set 2 | 3 | from sqlalchemy_model_factory.registry import Method, Registry 4 | 5 | _ITERABLES = (list, tuple, set) 6 | 7 | 8 | class Options: 9 | def __init__(self, commit=True, cleanup=True): 10 | self.commit = commit 11 | self.cleanup = cleanup 12 | 13 | 14 | class ModelFactory: 15 | def __init__(self, registry: Registry, session, options=None): 16 | self.registry = registry 17 | self.new_models: Set = set() 18 | self.session = session 19 | 20 | self.options = Options(**options or {}) 21 | 22 | def __enter__(self): 23 | return Namespace.from_registry(self.registry, manager=self) 24 | 25 | def __exit__(self, *_): 26 | self.remove_managed_data() 27 | return False 28 | 29 | def remove_managed_data(self): 30 | if not self.options.cleanup: 31 | return 32 | 33 | # Events inside the context manager could have left pending state. 34 | self.session.rollback() 35 | 36 | if getattr(self.session, "autocommit", None): 37 | self.session.begin() 38 | 39 | while self.session.identity_map: 40 | model = next(iter(self.session.identity_map.values())) 41 | self.session.delete(model) 42 | self.session.flush() 43 | 44 | self.new_models.clear() 45 | 46 | if self.options.commit: 47 | self.session.commit() 48 | 49 | def add_result(self, result, commit=True, merge=False): 50 | # The state of the session is unknown at this point. Ensure it's empty. 51 | self.session.rollback() 52 | 53 | # When the session is autocommit, it is expected that you start the transaction manually. 54 | if getattr(self.session, "autocommit", None): 55 | self.session.begin() 56 | 57 | if merge: 58 | if isinstance(result, _ITERABLES): 59 | items = [] 60 | for item in result: 61 | items.append(self.session.merge(item)) 62 | result = items 63 | else: 64 | result = self.session.merge(result) 65 | else: 66 | if isinstance(result, _ITERABLES): 67 | for item in result: 68 | self.session.add(item) 69 | else: 70 | self.session.add(result) 71 | 72 | self.new_models = self.new_models.union(self.session.new) 73 | 74 | self.session.flush() 75 | if commit: 76 | # Again, we cannot predict what's happening elsewhere, so we should try to keep models 77 | # appear to return as they would if freshly queried from the database. 78 | if self.options.commit: 79 | self.session.commit() 80 | else: 81 | self.session.flush() 82 | 83 | if isinstance(result, _ITERABLES): 84 | for item in result: 85 | self.session.refresh(item) 86 | else: 87 | self.session.refresh(result) 88 | 89 | return result 90 | 91 | 92 | class Namespace: 93 | """Represent a collection of registered namespaces or callable `Method`s. 94 | 95 | A `Namespace` is a recursive structure used to form the path traversal 96 | from the root namespace to any registered function. 97 | 98 | A method can be registered at any level, including previously registered path 99 | names, so long as that exact path + leaf is not yet registered. 100 | 101 | Examples: 102 | >>> from sqlalchemy_model_factory.registry import Method 103 | 104 | >>> method1 = Method(lambda: 1) 105 | >>> method2 = Method(lambda: 2) 106 | >>> namespace = Namespace(method1, foo=Namespace(method2)) 107 | 108 | >>> namespace() 109 | 1 110 | 111 | >>> namespace.foo() 112 | 2 113 | """ 114 | 115 | @classmethod 116 | def from_registry(cls, registry: Registry, manager=None): 117 | """Produce a `Namespace` tree structure from a flat `Registry` structure. 118 | 119 | Examples: 120 | >>> registry = Registry() 121 | >>> @registry.register_at("foo", name='bar') 122 | ... def foo_bar(): 123 | ... return 5 124 | 125 | >>> @registry.register_at(name='foo') 126 | ... def foo_bar(): 127 | ... return 6 128 | 129 | >>> namespace = Namespace.from_registry(registry) 130 | >>> namespace.foo() 131 | 6 132 | >>> namespace.foo.bar() 133 | 5 134 | """ 135 | tree: Dict[str, Any] = {} 136 | 137 | for namespace in registry.namespaces(): 138 | context = tree 139 | for path_item in namespace: 140 | context = context.setdefault(path_item, {}) 141 | 142 | methods = registry.methods(*namespace) 143 | for name, method in methods.items(): 144 | context.setdefault(name, {})["__call__"] = method 145 | 146 | return cls.from_tree(tree, manager=manager) 147 | 148 | @classmethod 149 | def from_tree(cls, tree, manager=None): 150 | attrs = {} 151 | for key, raw_value in tree.items(): 152 | if key == "__call__": 153 | value = raw_value 154 | else: 155 | value = cls.from_tree(raw_value, manager=manager) 156 | attrs[key] = value 157 | 158 | return cls(_manager=manager, **attrs) 159 | 160 | def __init__(self, __call__: Optional[Method] = None, *, _manager=None, **attrs): 161 | self.__manager = _manager 162 | self.__method = __call__ 163 | 164 | for attr, value in attrs.items(): 165 | setattr(self, attr, value) 166 | 167 | def __getattr__(self, attr): 168 | """Catch unset attribute names to provide a better error message.""" 169 | namespaces = [] 170 | methods = [] 171 | for name, item in self.__dict__.items(): 172 | if name.startswith(f"_{self.__class__.__name__}"): 173 | continue 174 | 175 | if isinstance(item, self.__class__): 176 | namespaces.append(name) 177 | else: 178 | methods.append(name) 179 | 180 | method_names = "N/A" 181 | if methods: 182 | method_names = ", ".join(methods) 183 | 184 | namespace_names = "N/A" 185 | if namespaces: 186 | namespace_names = ", ".join(namespaces) 187 | 188 | raise AttributeError( 189 | f"{self.__class__.__name__} has no attribute '{attr}'. Available methods include: {method_names}. Available nested namespaces include: {namespace_names}." 190 | ) 191 | 192 | def __call__(self, *args, commit_=None, merge_=None, **kwargs): 193 | """Provide an access guarding mechanism around callables. 194 | 195 | Allows for a hook into, for example, the calling of namespace functions 196 | for the purposes of keeping track of the results of the function calls, 197 | or otherwise manipulating the input arguments. 198 | """ 199 | if self.__method is None: 200 | raise RuntimeError( 201 | f"{self} has no registered factory function and cannot be called." 202 | ) 203 | 204 | callable = self.__method.fn 205 | if hasattr(callable, "for_model"): 206 | callable = callable.for_model 207 | 208 | result = callable(*args, **kwargs) 209 | 210 | if self.__manager: 211 | commit = ( 212 | commit_ 213 | if commit_ is not None 214 | else self.__method.commit 215 | if self.__method.commit is not None 216 | else True 217 | ) 218 | merge = ( 219 | merge_ 220 | if merge_ is not None 221 | else self.__method.merge 222 | if self.__method.merge is not None 223 | else False 224 | ) 225 | result = self.__manager.add_result(result, commit=commit, merge=merge) 226 | return result 227 | 228 | def __repr__(self): 229 | cls_name = self.__class__.__name__ 230 | 231 | attrs = [] 232 | for key, value in self.__dict__.items(): 233 | if key.endswith("__manager"): 234 | continue 235 | 236 | if key == f"_{cls_name}__method": 237 | if value is None: 238 | continue 239 | 240 | attrs.append(f"__call__={value}") 241 | else: 242 | attrs.append(f"{key}={repr(value)}") 243 | 244 | attrs_str = ", ".join(attrs) 245 | return f"{cls_name}({attrs_str})" 246 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "atomicwrites" 3 | version = "1.4.1" 4 | description = "Atomic file writes." 5 | category = "main" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "22.1.0" 12 | description = "Classes Without Boilerplate" 13 | category = "main" 14 | optional = false 15 | python-versions = ">=3.5" 16 | 17 | [package.extras] 18 | dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] 19 | docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] 20 | tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] 21 | tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] 22 | 23 | [[package]] 24 | name = "black" 25 | version = "22.3.0" 26 | description = "The uncompromising code formatter." 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=3.6.2" 30 | 31 | [package.dependencies] 32 | click = ">=8.0.0" 33 | dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} 34 | mypy-extensions = ">=0.4.3" 35 | pathspec = ">=0.9.0" 36 | platformdirs = ">=2" 37 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 38 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} 39 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 40 | 41 | [package.extras] 42 | colorama = ["colorama (>=0.4.3)"] 43 | d = ["aiohttp (>=3.7.4)"] 44 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 45 | uvloop = ["uvloop (>=0.15.2)"] 46 | 47 | [[package]] 48 | name = "click" 49 | version = "8.0.4" 50 | description = "Composable command line interface toolkit" 51 | category = "dev" 52 | optional = false 53 | python-versions = ">=3.6" 54 | 55 | [package.dependencies] 56 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 57 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 58 | 59 | [[package]] 60 | name = "colorama" 61 | version = "0.4.5" 62 | description = "Cross-platform colored terminal text." 63 | category = "main" 64 | optional = false 65 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 66 | 67 | [[package]] 68 | name = "coverage" 69 | version = "6.2" 70 | description = "Code coverage measurement for Python" 71 | category = "dev" 72 | optional = false 73 | python-versions = ">=3.6" 74 | 75 | [package.extras] 76 | toml = ["tomli"] 77 | 78 | [[package]] 79 | name = "coverage" 80 | version = "7.0.0" 81 | description = "Code coverage measurement for Python" 82 | category = "dev" 83 | optional = false 84 | python-versions = ">=3.7" 85 | 86 | [package.extras] 87 | toml = ["tomli"] 88 | 89 | [[package]] 90 | name = "dataclasses" 91 | version = "0.8" 92 | description = "A backport of the dataclasses module for Python 3.6" 93 | category = "dev" 94 | optional = false 95 | python-versions = ">=3.6, <3.7" 96 | 97 | [[package]] 98 | name = "flake8" 99 | version = "5.0.4" 100 | description = "the modular source code checker: pep8 pyflakes and co" 101 | category = "dev" 102 | optional = false 103 | python-versions = ">=3.6.1" 104 | 105 | [package.dependencies] 106 | importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} 107 | mccabe = ">=0.7.0,<0.8.0" 108 | pycodestyle = ">=2.9.0,<2.10.0" 109 | pyflakes = ">=2.5.0,<2.6.0" 110 | 111 | [[package]] 112 | name = "greenlet" 113 | version = "2.0.1" 114 | description = "Lightweight in-process concurrent programming" 115 | category = "main" 116 | optional = false 117 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" 118 | 119 | [package.extras] 120 | docs = ["Sphinx", "docutils (<0.18)"] 121 | test = ["faulthandler", "objgraph", "psutil"] 122 | 123 | [[package]] 124 | name = "importlib-metadata" 125 | version = "4.2.0" 126 | description = "Read metadata from Python packages" 127 | category = "main" 128 | optional = false 129 | python-versions = ">=3.6" 130 | 131 | [package.dependencies] 132 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 133 | zipp = ">=0.5" 134 | 135 | [package.extras] 136 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 137 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] 138 | 139 | [[package]] 140 | name = "iniconfig" 141 | version = "1.1.1" 142 | description = "iniconfig: brain-dead simple config-ini parsing" 143 | category = "main" 144 | optional = false 145 | python-versions = "*" 146 | 147 | [[package]] 148 | name = "isort" 149 | version = "5.10.1" 150 | description = "A Python utility / library to sort Python imports." 151 | category = "dev" 152 | optional = false 153 | python-versions = ">=3.6.1,<4.0" 154 | 155 | [package.extras] 156 | colors = ["colorama (>=0.4.3,<0.5.0)"] 157 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 158 | plugins = ["setuptools"] 159 | requirements_deprecated_finder = ["pip-api", "pipreqs"] 160 | 161 | [[package]] 162 | name = "mccabe" 163 | version = "0.7.0" 164 | description = "McCabe checker, plugin for flake8" 165 | category = "dev" 166 | optional = false 167 | python-versions = ">=3.6" 168 | 169 | [[package]] 170 | name = "mypy" 171 | version = "0.971" 172 | description = "Optional static typing for Python" 173 | category = "dev" 174 | optional = false 175 | python-versions = ">=3.6" 176 | 177 | [package.dependencies] 178 | mypy-extensions = ">=0.4.3" 179 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 180 | typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} 181 | typing-extensions = ">=3.10" 182 | 183 | [package.extras] 184 | dmypy = ["psutil (>=4.0)"] 185 | python2 = ["typed-ast (>=1.4.0,<2)"] 186 | reports = ["lxml"] 187 | 188 | [[package]] 189 | name = "mypy-extensions" 190 | version = "0.4.3" 191 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 192 | category = "dev" 193 | optional = false 194 | python-versions = "*" 195 | 196 | [[package]] 197 | name = "packaging" 198 | version = "21.3" 199 | description = "Core utilities for Python packages" 200 | category = "main" 201 | optional = false 202 | python-versions = ">=3.6" 203 | 204 | [package.dependencies] 205 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 206 | 207 | [[package]] 208 | name = "pathspec" 209 | version = "0.9.0" 210 | description = "Utility library for gitignore style pattern matching of file paths." 211 | category = "dev" 212 | optional = false 213 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 214 | 215 | [[package]] 216 | name = "platformdirs" 217 | version = "2.4.0" 218 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 219 | category = "dev" 220 | optional = false 221 | python-versions = ">=3.6" 222 | 223 | [package.extras] 224 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 225 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 226 | 227 | [[package]] 228 | name = "pluggy" 229 | version = "1.0.0" 230 | description = "plugin and hook calling mechanisms for python" 231 | category = "main" 232 | optional = false 233 | python-versions = ">=3.6" 234 | 235 | [package.dependencies] 236 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 237 | 238 | [package.extras] 239 | dev = ["pre-commit", "tox"] 240 | testing = ["pytest", "pytest-benchmark"] 241 | 242 | [[package]] 243 | name = "py" 244 | version = "1.11.0" 245 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 246 | category = "main" 247 | optional = false 248 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 249 | 250 | [[package]] 251 | name = "pycodestyle" 252 | version = "2.9.1" 253 | description = "Python style guide checker" 254 | category = "dev" 255 | optional = false 256 | python-versions = ">=3.6" 257 | 258 | [[package]] 259 | name = "pydocstyle" 260 | version = "6.1.1" 261 | description = "Python docstring style checker" 262 | category = "dev" 263 | optional = false 264 | python-versions = ">=3.6" 265 | 266 | [package.dependencies] 267 | snowballstemmer = "*" 268 | 269 | [package.extras] 270 | toml = ["toml"] 271 | 272 | [[package]] 273 | name = "pyflakes" 274 | version = "2.5.0" 275 | description = "passive checker of Python programs" 276 | category = "dev" 277 | optional = false 278 | python-versions = ">=3.6" 279 | 280 | [[package]] 281 | name = "pyparsing" 282 | version = "3.0.7" 283 | description = "Python parsing module" 284 | category = "main" 285 | optional = false 286 | python-versions = ">=3.6" 287 | 288 | [package.extras] 289 | diagrams = ["jinja2", "railroad-diagrams"] 290 | 291 | [[package]] 292 | name = "pytest" 293 | version = "7.0.1" 294 | description = "pytest: simple powerful testing with Python" 295 | category = "main" 296 | optional = false 297 | python-versions = ">=3.6" 298 | 299 | [package.dependencies] 300 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 301 | attrs = ">=19.2.0" 302 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 303 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 304 | iniconfig = "*" 305 | packaging = "*" 306 | pluggy = ">=0.12,<2.0" 307 | py = ">=1.8.2" 308 | tomli = ">=1.0.0" 309 | 310 | [package.extras] 311 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 312 | 313 | [[package]] 314 | name = "snowballstemmer" 315 | version = "2.2.0" 316 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 317 | category = "dev" 318 | optional = false 319 | python-versions = "*" 320 | 321 | [[package]] 322 | name = "SQLAlchemy" 323 | version = "1.4.45" 324 | description = "Database Abstraction Library" 325 | category = "main" 326 | optional = false 327 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 328 | 329 | [package.dependencies] 330 | greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} 331 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 332 | 333 | [package.extras] 334 | aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] 335 | aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] 336 | asyncio = ["greenlet (!=0.4.17)"] 337 | asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] 338 | mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"] 339 | mssql = ["pyodbc"] 340 | mssql_pymssql = ["pymssql"] 341 | mssql_pyodbc = ["pyodbc"] 342 | mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] 343 | mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] 344 | mysql_connector = ["mysql-connector-python"] 345 | oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] 346 | postgresql = ["psycopg2 (>=2.7)"] 347 | postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] 348 | postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] 349 | postgresql_psycopg2binary = ["psycopg2-binary"] 350 | postgresql_psycopg2cffi = ["psycopg2cffi"] 351 | pymysql = ["pymysql", "pymysql (<1)"] 352 | sqlcipher = ["sqlcipher3_binary"] 353 | 354 | [[package]] 355 | name = "sqlalchemy-stubs" 356 | version = "0.4" 357 | description = "SQLAlchemy stubs and mypy plugin" 358 | category = "dev" 359 | optional = false 360 | python-versions = "*" 361 | 362 | [package.dependencies] 363 | mypy = ">=0.790" 364 | typing-extensions = ">=3.7.4" 365 | 366 | [[package]] 367 | name = "tomli" 368 | version = "1.2.3" 369 | description = "A lil' TOML parser" 370 | category = "main" 371 | optional = false 372 | python-versions = ">=3.6" 373 | 374 | [[package]] 375 | name = "typed-ast" 376 | version = "1.5.4" 377 | description = "a fork of Python 2 and 3 ast modules with type comment support" 378 | category = "dev" 379 | optional = false 380 | python-versions = ">=3.6" 381 | 382 | [[package]] 383 | name = "typing-extensions" 384 | version = "4.1.1" 385 | description = "Backported and Experimental Type Hints for Python 3.6+" 386 | category = "main" 387 | optional = false 388 | python-versions = ">=3.6" 389 | 390 | [[package]] 391 | name = "zipp" 392 | version = "3.6.0" 393 | description = "Backport of pathlib-compatible object wrapper for zip files" 394 | category = "main" 395 | optional = false 396 | python-versions = ">=3.6" 397 | 398 | [package.extras] 399 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 400 | testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] 401 | 402 | [extras] 403 | pytest = ["pytest"] 404 | 405 | [metadata] 406 | lock-version = "1.1" 407 | python-versions = ">=3.6.2,<4" 408 | content-hash = "e18d8581fcda651061215925aa3445b14e55336bd67438f28b9d6f3fa848848a" 409 | 410 | [metadata.files] 411 | atomicwrites = [ 412 | {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, 413 | ] 414 | attrs = [ 415 | {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, 416 | {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, 417 | ] 418 | black = [ 419 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, 420 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, 421 | {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, 422 | {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, 423 | {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, 424 | {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, 425 | {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, 426 | {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, 427 | {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, 428 | {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, 429 | {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, 430 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, 431 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, 432 | {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, 433 | {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, 434 | {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, 435 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, 436 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, 437 | {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, 438 | {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, 439 | {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, 440 | {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, 441 | {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, 442 | ] 443 | click = [ 444 | {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, 445 | {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, 446 | ] 447 | colorama = [ 448 | {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, 449 | {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, 450 | ] 451 | coverage = [ 452 | {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, 453 | {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, 454 | {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, 455 | {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, 456 | {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, 457 | {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, 458 | {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, 459 | {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, 460 | {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, 461 | {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, 462 | {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, 463 | {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, 464 | {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, 465 | {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, 466 | {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, 467 | {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, 468 | {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, 469 | {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, 470 | {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, 471 | {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, 472 | {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, 473 | {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, 474 | {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, 475 | {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, 476 | {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, 477 | {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, 478 | {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, 479 | {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, 480 | {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, 481 | {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, 482 | {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, 483 | {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, 484 | {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, 485 | {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, 486 | {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, 487 | {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, 488 | {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, 489 | {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, 490 | {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, 491 | {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, 492 | {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, 493 | {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, 494 | {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, 495 | {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, 496 | {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, 497 | {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, 498 | {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, 499 | {file = "coverage-7.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2569682d6ea9628da8d6ba38579a48b1e53081226ec7a6c82b5024b3ce5009f"}, 500 | {file = "coverage-7.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ec256a592b497f26054195f7d7148892aca8c4cdcc064a7cc66ef7a0455b811"}, 501 | {file = "coverage-7.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5885a4ceb6dde34271bb0adafa4a248a7f589c89821e9da3110c39f92f41e21b"}, 502 | {file = "coverage-7.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d43d406a4d73aa7f855fa44fa77ff47e739b565b2af3844600cdc016d01e46b9"}, 503 | {file = "coverage-7.0.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18df11efa615b79b9ecc13035a712957ff6283f7b244e57684e1c092869f541"}, 504 | {file = "coverage-7.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f6a4bf5bdee93f6817797beba7086292c2ebde6df0d5822e0c33f8b05415c339"}, 505 | {file = "coverage-7.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:33efe89cd0efef016db19d8d05aa46631f76793de90a61b6717acb202b36fe60"}, 506 | {file = "coverage-7.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96b5b1f1079e48f56bfccf103bcf44d48b9eb5163f1ea523fad580f15d3fe5e0"}, 507 | {file = "coverage-7.0.0-cp310-cp310-win32.whl", hash = "sha256:fb85b7a7a4b204bd59d6d0b0c8d87d9ffa820da225e691dfaffc3137dc05b5f6"}, 508 | {file = "coverage-7.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:793dcd9d42035746fc7637df4336f7581df19d33c5c5253cf988c99d8e93a8ba"}, 509 | {file = "coverage-7.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d564142a03d3bc8913499a458e931b52ddfe952f69b6cd4b24d810fd2959044a"}, 510 | {file = "coverage-7.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0a8b0e86bede874bf5da566b02194fbb12dd14ce3585cabd58452007f272ba81"}, 511 | {file = "coverage-7.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e645c73cbfc4577d93747d3f793115acf6f907a7eb9208fa807fdcf2da1964a4"}, 512 | {file = "coverage-7.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de06e7585abe88c6d38c1b73ce4c3cb4c1a79fbb0da0d0f8e8689ef5729ec60d"}, 513 | {file = "coverage-7.0.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a30b646fbdd5bc52f506e149fa4fbdef82432baf6b81774e61ec4e3b43b9cbde"}, 514 | {file = "coverage-7.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:db8141856dc9be0917413df7200f53accf1d84c8b156868e6af058a1ea8e903a"}, 515 | {file = "coverage-7.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:59e71912c7fc78d08a567ee65656123878f49ca1b5672e660ea70bf8dfbebf8f"}, 516 | {file = "coverage-7.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b8f7cd942dda3795fc9eadf303cc53a422ac057e3b70c2ad6d4276ec6a83a541"}, 517 | {file = "coverage-7.0.0-cp311-cp311-win32.whl", hash = "sha256:bf437a04b9790d3c9cd5b48e9ce9aa84229040e3ae7d6c670a55118906113c5a"}, 518 | {file = "coverage-7.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a7e1bb36b4e57a2d304322021b35d4e4a25fa0d501ba56e8e51efaebf4480556"}, 519 | {file = "coverage-7.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:215f40ef86f1958a1151fa7fad2b4f2f99534c4e10a34a1e065eba3f19ef8868"}, 520 | {file = "coverage-7.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae088eb1cbdad8206931b1bf3f11dee644e038a9300be84d3e705e29356e5b1d"}, 521 | {file = "coverage-7.0.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9071e197faa24837b967bc9aa0b9ef961f805a75f1ee3ea1f3367f55cd46c3c"}, 522 | {file = "coverage-7.0.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f1e6d9c70d45a960d3f3d781ea62b167fdf2e0e1f6bb282b96feea653adb923"}, 523 | {file = "coverage-7.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9fadd15f9fcfd7b16d9cccce9f5e6ec6f9b8df860633ad9aa62c2b14c259560f"}, 524 | {file = "coverage-7.0.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:10b6246cae61896ab4c7568e498e492cbb73a2dfa4c3af79141c43cf806f929a"}, 525 | {file = "coverage-7.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a8785791c2120af114ea7a06137f7778632e568a5aa2bbfc3b46c573b702af74"}, 526 | {file = "coverage-7.0.0-cp37-cp37m-win32.whl", hash = "sha256:30220518dd89c4878908d73f5f3d1269f86e9e045354436534587a18c7b9da85"}, 527 | {file = "coverage-7.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bc904aa96105d73357de03de76336b1e3db28e2b12067d36625fd9646ab043fd"}, 528 | {file = "coverage-7.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2331b7bd84a1be79bd17ca8e103ce38db8cbf7cb354dc56e651ba489cf849212"}, 529 | {file = "coverage-7.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e907db8bdd0ad1253a33c20fdc5f0f6209d271114a9c6f1fcdf96617343f7ca0"}, 530 | {file = "coverage-7.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0deee68e0dae1d6e3fe6943c76d7e66fbeb6519bd08e4e5366bcc28a8a9aca"}, 531 | {file = "coverage-7.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fff0f08bc5ffd0d78db821971472b4adc2ee876b86f743e46d634fb8e3c22f"}, 532 | {file = "coverage-7.0.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a290b7921c1c05787b953e5854d394e887df40696f21381cc33c4e2179bf50ac"}, 533 | {file = "coverage-7.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:100546219af59d2ad82d4575de03a303eb27b75ea36ffbd1677371924d50bcbc"}, 534 | {file = "coverage-7.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c1ba6e63b831112b9484ff5905370d89e43d4316bac76d403031f60d61597466"}, 535 | {file = "coverage-7.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c685fc17d6f4f1a3833e9dac27d0b931f7ccb52be6c30d269374203c7d0204a2"}, 536 | {file = "coverage-7.0.0-cp38-cp38-win32.whl", hash = "sha256:8938f3a10f45019b502020ba9567b97b6ecc8c76b664b421705c5406d4f92fe8"}, 537 | {file = "coverage-7.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:c4b63888bef2928d0eca12cbce0760cfb696acb4fe226eb55178b6a2a039328a"}, 538 | {file = "coverage-7.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cda63459eb20652b22e038729a8f5063862c189a3963cb042a764b753172f75e"}, 539 | {file = "coverage-7.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e06abac1a4aec1ff989131e43ca917fc7bd296f34bf0cfe86cbf74343b21566d"}, 540 | {file = "coverage-7.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b94ad926e933976627f040f96dd1d9b0ac91f8d27e868c30a28253b9b6ac2d"}, 541 | {file = "coverage-7.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6b4af31fb49a2ae8de1cd505fa66c403bfcc5066e845ac19d8904dcfc9d40da"}, 542 | {file = "coverage-7.0.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36b62f0220459e528ad5806cc7dede71aa716e067d2cb10cb4a09686b8791fba"}, 543 | {file = "coverage-7.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:43ec1935c6d6caab4f3bc126d20bd709c0002a175d62208ebe745be37a826a41"}, 544 | {file = "coverage-7.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8593c9baf1f0f273afa22f5b45508b76adc7b8e94e17e7d98fbe1e3cd5812af2"}, 545 | {file = "coverage-7.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fee283cd36c3f14422d9c1b51da24ddbb5e1eed89ad2480f6a9f115df38b5df8"}, 546 | {file = "coverage-7.0.0-cp39-cp39-win32.whl", hash = "sha256:97c0b001ff15b8e8882995fc07ac0a08c8baf8b13c1145f3f12e0587bbb0e335"}, 547 | {file = "coverage-7.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:8dbf83a4611c591b5de65069b6fd4dd3889200ed270cd2f7f5ac765d3842889f"}, 548 | {file = "coverage-7.0.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:bcaf18e46668057051a312c714a4548b81f7e8fb3454116ad97be7562d2a99e4"}, 549 | {file = "coverage-7.0.0.tar.gz", hash = "sha256:9a175da2a7320e18fc3ee1d147639a2b3a8f037e508c96aa2da160294eb50e17"}, 550 | ] 551 | dataclasses = [ 552 | {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, 553 | {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, 554 | ] 555 | flake8 = [ 556 | {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, 557 | {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, 558 | ] 559 | greenlet = [ 560 | {file = "greenlet-2.0.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:9ed358312e63bf683b9ef22c8e442ef6c5c02973f0c2a939ec1d7b50c974015c"}, 561 | {file = "greenlet-2.0.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4f09b0010e55bec3239278f642a8a506b91034f03a4fb28289a7d448a67f1515"}, 562 | {file = "greenlet-2.0.1-cp27-cp27m-win32.whl", hash = "sha256:1407fe45246632d0ffb7a3f4a520ba4e6051fc2cbd61ba1f806900c27f47706a"}, 563 | {file = "greenlet-2.0.1-cp27-cp27m-win_amd64.whl", hash = "sha256:3001d00eba6bbf084ae60ec7f4bb8ed375748f53aeaefaf2a37d9f0370558524"}, 564 | {file = "greenlet-2.0.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d566b82e92ff2e09dd6342df7e0eb4ff6275a3f08db284888dcd98134dbd4243"}, 565 | {file = "greenlet-2.0.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:0722c9be0797f544a3ed212569ca3fe3d9d1a1b13942d10dd6f0e8601e484d26"}, 566 | {file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d37990425b4687ade27810e3b1a1c37825d242ebc275066cfee8cb6b8829ccd"}, 567 | {file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be35822f35f99dcc48152c9839d0171a06186f2d71ef76dc57fa556cc9bf6b45"}, 568 | {file = "greenlet-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c140e7eb5ce47249668056edf3b7e9900c6a2e22fb0eaf0513f18a1b2c14e1da"}, 569 | {file = "greenlet-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d21681f09e297a5adaa73060737e3aa1279a13ecdcfcc6ef66c292cb25125b2d"}, 570 | {file = "greenlet-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fb412b7db83fe56847df9c47b6fe3f13911b06339c2aa02dcc09dce8bbf582cd"}, 571 | {file = "greenlet-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6a08799e9e88052221adca55741bf106ec7ea0710bca635c208b751f0d5b617"}, 572 | {file = "greenlet-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e112e03d37987d7b90c1e98ba5e1b59e1645226d78d73282f45b326f7bddcb9"}, 573 | {file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56961cfca7da2fdd178f95ca407fa330c64f33289e1804b592a77d5593d9bd94"}, 574 | {file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13ba6e8e326e2116c954074c994da14954982ba2795aebb881c07ac5d093a58a"}, 575 | {file = "greenlet-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bf633a50cc93ed17e494015897361010fc08700d92676c87931d3ea464123ce"}, 576 | {file = "greenlet-2.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9f2c221eecb7ead00b8e3ddb913c67f75cba078fd1d326053225a3f59d850d72"}, 577 | {file = "greenlet-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:13ebf93c343dd8bd010cd98e617cb4c1c1f352a0cf2524c82d3814154116aa82"}, 578 | {file = "greenlet-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:6f61d71bbc9b4a3de768371b210d906726535d6ca43506737682caa754b956cd"}, 579 | {file = "greenlet-2.0.1-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:2d0bac0385d2b43a7bd1d651621a4e0f1380abc63d6fb1012213a401cbd5bf8f"}, 580 | {file = "greenlet-2.0.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:f6327b6907b4cb72f650a5b7b1be23a2aab395017aa6f1adb13069d66360eb3f"}, 581 | {file = "greenlet-2.0.1-cp35-cp35m-win32.whl", hash = "sha256:81b0ea3715bf6a848d6f7149d25bf018fd24554a4be01fcbbe3fdc78e890b955"}, 582 | {file = "greenlet-2.0.1-cp35-cp35m-win_amd64.whl", hash = "sha256:38255a3f1e8942573b067510f9611fc9e38196077b0c8eb7a8c795e105f9ce77"}, 583 | {file = "greenlet-2.0.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:04957dc96669be041e0c260964cfef4c77287f07c40452e61abe19d647505581"}, 584 | {file = "greenlet-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:4aeaebcd91d9fee9aa768c1b39cb12214b30bf36d2b7370505a9f2165fedd8d9"}, 585 | {file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974a39bdb8c90a85982cdb78a103a32e0b1be986d411303064b28a80611f6e51"}, 586 | {file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dca09dedf1bd8684767bc736cc20c97c29bc0c04c413e3276e0962cd7aeb148"}, 587 | {file = "greenlet-2.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c0757db9bd08470ff8277791795e70d0bf035a011a528ee9a5ce9454b6cba2"}, 588 | {file = "greenlet-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5067920de254f1a2dee8d3d9d7e4e03718e8fd2d2d9db962c8c9fa781ae82a39"}, 589 | {file = "greenlet-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5a8e05057fab2a365c81abc696cb753da7549d20266e8511eb6c9d9f72fe3e92"}, 590 | {file = "greenlet-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:3d75b8d013086b08e801fbbb896f7d5c9e6ccd44f13a9241d2bf7c0df9eda928"}, 591 | {file = "greenlet-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:097e3dae69321e9100202fc62977f687454cd0ea147d0fd5a766e57450c569fd"}, 592 | {file = "greenlet-2.0.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:cb242fc2cda5a307a7698c93173d3627a2a90d00507bccf5bc228851e8304963"}, 593 | {file = "greenlet-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:72b00a8e7c25dcea5946692a2485b1a0c0661ed93ecfedfa9b6687bd89a24ef5"}, 594 | {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce"}, 595 | {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000"}, 596 | {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2"}, 597 | {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9"}, 598 | {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1"}, 599 | {file = "greenlet-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"}, 600 | {file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"}, 601 | {file = "greenlet-2.0.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:cd4ccc364cf75d1422e66e247e52a93da6a9b73cefa8cad696f3cbbb75af179d"}, 602 | {file = "greenlet-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c8b1c43e75c42a6cafcc71defa9e01ead39ae80bd733a2608b297412beede68"}, 603 | {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e"}, 604 | {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48"}, 605 | {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764"}, 606 | {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0"}, 607 | {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9"}, 608 | {file = "greenlet-2.0.1-cp38-cp38-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"}, 609 | {file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"}, 610 | {file = "greenlet-2.0.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b1992ba9d4780d9af9726bbcef6a1db12d9ab1ccc35e5773685a24b7fb2758eb"}, 611 | {file = "greenlet-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b5e83e4de81dcc9425598d9469a624826a0b1211380ac444c7c791d4a2137c19"}, 612 | {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5"}, 613 | {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7"}, 614 | {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d"}, 615 | {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726"}, 616 | {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e"}, 617 | {file = "greenlet-2.0.1-cp39-cp39-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"}, 618 | {file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"}, 619 | {file = "greenlet-2.0.1.tar.gz", hash = "sha256:42e602564460da0e8ee67cb6d7236363ee5e131aa15943b6670e44e5c2ed0f67"}, 620 | ] 621 | importlib-metadata = [ 622 | {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, 623 | {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, 624 | ] 625 | iniconfig = [ 626 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 627 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 628 | ] 629 | isort = [ 630 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 631 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 632 | ] 633 | mccabe = [ 634 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 635 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 636 | ] 637 | mypy = [ 638 | {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, 639 | {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, 640 | {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, 641 | {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, 642 | {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, 643 | {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, 644 | {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, 645 | {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, 646 | {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, 647 | {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, 648 | {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, 649 | {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, 650 | {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, 651 | {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, 652 | {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, 653 | {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, 654 | {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, 655 | {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, 656 | {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, 657 | {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, 658 | {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, 659 | {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, 660 | {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, 661 | ] 662 | mypy-extensions = [ 663 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 664 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 665 | ] 666 | packaging = [ 667 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 668 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 669 | ] 670 | pathspec = [ 671 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 672 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 673 | ] 674 | platformdirs = [ 675 | {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, 676 | {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, 677 | ] 678 | pluggy = [ 679 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 680 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 681 | ] 682 | py = [ 683 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 684 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 685 | ] 686 | pycodestyle = [ 687 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, 688 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, 689 | ] 690 | pydocstyle = [ 691 | {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, 692 | {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, 693 | ] 694 | pyflakes = [ 695 | {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, 696 | {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, 697 | ] 698 | pyparsing = [ 699 | {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, 700 | {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, 701 | ] 702 | pytest = [ 703 | {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, 704 | {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, 705 | ] 706 | snowballstemmer = [ 707 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 708 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 709 | ] 710 | SQLAlchemy = [ 711 | {file = "SQLAlchemy-1.4.45-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:f1d3fb02a4d0b07d1351a4a52f159e5e7b3045c903468b7e9349ebf0020ffdb9"}, 712 | {file = "SQLAlchemy-1.4.45-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9b7025d46aba946272f6b6b357a22f3787473ef27451f342df1a2a6de23743e3"}, 713 | {file = "SQLAlchemy-1.4.45-cp27-cp27m-win32.whl", hash = "sha256:26b8424b32eeefa4faad21decd7bdd4aade58640b39407bf43e7d0a7c1bc0453"}, 714 | {file = "SQLAlchemy-1.4.45-cp27-cp27m-win_amd64.whl", hash = "sha256:13578d1cda69bc5e76c59fec9180d6db7ceb71c1360a4d7861c37d87ea6ca0b1"}, 715 | {file = "SQLAlchemy-1.4.45-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6cd53b4c756a6f9c6518a3dc9c05a38840f9ae442c91fe1abde50d73651b6922"}, 716 | {file = "SQLAlchemy-1.4.45-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:ca152ffc7f0aa069c95fba46165030267ec5e4bb0107aba45e5e9e86fe4d9363"}, 717 | {file = "SQLAlchemy-1.4.45-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06055476d38ed7915eeed22b78580556d446d175c3574a01b9eb04d91f3a8b2e"}, 718 | {file = "SQLAlchemy-1.4.45-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:081e2a2d75466353c738ca2ee71c0cfb08229b4f9909b5fa085f75c48d021471"}, 719 | {file = "SQLAlchemy-1.4.45-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96821d806c0c90c68ce3f2ce6dd529c10e5d7587961f31dd5c30e3bfddc4545d"}, 720 | {file = "SQLAlchemy-1.4.45-cp310-cp310-win32.whl", hash = "sha256:c8051bff4ce48cbc98f11e95ac46bfd1e36272401070c010248a3230d099663f"}, 721 | {file = "SQLAlchemy-1.4.45-cp310-cp310-win_amd64.whl", hash = "sha256:16ad798fc121cad5ea019eb2297127b08c54e1aa95fe17b3fea9fdbc5c34fe62"}, 722 | {file = "SQLAlchemy-1.4.45-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:099efef0de9fbda4c2d7cb129e4e7f812007901942259d4e6c6e19bd69de1088"}, 723 | {file = "SQLAlchemy-1.4.45-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29a29d02c9e6f6b105580c5ed7afb722b97bc2e2fdb85e1d45d7ddd8440cfbca"}, 724 | {file = "SQLAlchemy-1.4.45-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc10423b59d6d032d6dff0bb42aa06dc6a8824eb6029d70c7d1b6981a2e7f4d8"}, 725 | {file = "SQLAlchemy-1.4.45-cp311-cp311-win32.whl", hash = "sha256:1a92685db3b0682776a5abcb5f9e9addb3d7d9a6d841a452a17ec2d8d457bea7"}, 726 | {file = "SQLAlchemy-1.4.45-cp311-cp311-win_amd64.whl", hash = "sha256:db3ccbce4a861bf4338b254f95916fc68dd8b7aa50eea838ecdaf3a52810e9c0"}, 727 | {file = "SQLAlchemy-1.4.45-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:a62ae2ea3b940ce9c9cbd675489c2047921ce0a79f971d3082978be91bd58117"}, 728 | {file = "SQLAlchemy-1.4.45-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a87f8595390764db333a1705591d0934973d132af607f4fa8b792b366eacbb3c"}, 729 | {file = "SQLAlchemy-1.4.45-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a21c1fb71c69c8ec65430160cd3eee44bbcea15b5a4e556f29d03f246f425ec"}, 730 | {file = "SQLAlchemy-1.4.45-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7944b04e6fcf8d733964dd9ee36b6a587251a1a4049af3a9b846f6e64eb349a"}, 731 | {file = "SQLAlchemy-1.4.45-cp36-cp36m-win32.whl", hash = "sha256:a3bcd5e2049ceb97e8c273e6a84ff4abcfa1dc47b6d8bbd36e07cce7176610d3"}, 732 | {file = "SQLAlchemy-1.4.45-cp36-cp36m-win_amd64.whl", hash = "sha256:5953e225be47d80410ae519f865b5c341f541d8e383fb6d11f67fb71a45bf890"}, 733 | {file = "SQLAlchemy-1.4.45-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:6a91b7883cb7855a27bc0637166eed622fdf1bb94a4d1630165e5dd88c7e64d3"}, 734 | {file = "SQLAlchemy-1.4.45-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d458fd0566bc9e10b8be857f089e96b5ca1b1ef033226f24512f9ffdf485a8c0"}, 735 | {file = "SQLAlchemy-1.4.45-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f4ad3b081c0dbb738886f8d425a5d983328670ee83b38192687d78fc82bd1e"}, 736 | {file = "SQLAlchemy-1.4.45-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd95a3e6ab46da2c5b0703e797a772f3fab44d085b3919a4f27339aa3b1f51d3"}, 737 | {file = "SQLAlchemy-1.4.45-cp37-cp37m-win32.whl", hash = "sha256:715f5859daa3bee6ecbad64501637fa4640ca6734e8cda6135e3898d5f8ccadd"}, 738 | {file = "SQLAlchemy-1.4.45-cp37-cp37m-win_amd64.whl", hash = "sha256:2d1539fbc82d2206380a86d6d7d0453764fdca5d042d78161bbfb8dd047c80ec"}, 739 | {file = "SQLAlchemy-1.4.45-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:01aa76f324c9bbc0dcb2bc3d9e2a9d7ede4808afa1c38d40d5e2007e3163b206"}, 740 | {file = "SQLAlchemy-1.4.45-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:416fe7d228937bd37990b5a429fd00ad0e49eabcea3455af7beed7955f192edd"}, 741 | {file = "SQLAlchemy-1.4.45-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7e32ce2584564d9e068bb7e0ccd1810cbb0a824c0687f8016fe67e97c345a637"}, 742 | {file = "SQLAlchemy-1.4.45-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:561605cfc26273825ed2fb8484428faf36e853c13e4c90c61c58988aeccb34ed"}, 743 | {file = "SQLAlchemy-1.4.45-cp38-cp38-win32.whl", hash = "sha256:55ddb5585129c5d964a537c9e32a8a68a8c6293b747f3fa164e1c034e1657a98"}, 744 | {file = "SQLAlchemy-1.4.45-cp38-cp38-win_amd64.whl", hash = "sha256:445914dcadc0b623bd9851260ee54915ecf4e3041a62d57709b18a0eed19f33b"}, 745 | {file = "SQLAlchemy-1.4.45-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:2db887dbf05bcc3151de1c4b506b14764c6240a42e844b4269132a7584de1e5f"}, 746 | {file = "SQLAlchemy-1.4.45-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52b90c9487e4449ad954624d01dea34c90cd8c104bce46b322c83654f37a23c5"}, 747 | {file = "SQLAlchemy-1.4.45-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f61e54b8c2b389de1a8ad52394729c478c67712dbdcdadb52c2575e41dae94a5"}, 748 | {file = "SQLAlchemy-1.4.45-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e91a5e45a2ea083fe344b3503405978dff14d60ef3aa836432c9ca8cd47806b6"}, 749 | {file = "SQLAlchemy-1.4.45-cp39-cp39-win32.whl", hash = "sha256:0e068b8414d60dd35d43c693555fc3d2e1d822cef07960bb8ca3f1ee6c4ff762"}, 750 | {file = "SQLAlchemy-1.4.45-cp39-cp39-win_amd64.whl", hash = "sha256:2d6f178ff2923730da271c8aa317f70cf0df11a4d1812f1d7a704b1cf29c5fe3"}, 751 | {file = "SQLAlchemy-1.4.45.tar.gz", hash = "sha256:fd69850860093a3f69fefe0ab56d041edfdfe18510b53d9a2eaecba2f15fa795"}, 752 | ] 753 | sqlalchemy-stubs = [ 754 | {file = "sqlalchemy-stubs-0.4.tar.gz", hash = "sha256:c665d6dd4482ef642f01027fa06c3d5e91befabb219dc71fc2a09e7d7695f7ae"}, 755 | {file = "sqlalchemy_stubs-0.4-py3-none-any.whl", hash = "sha256:5eec7aa110adf9b957b631799a72fef396b23ff99fe296df726645d01e312aa5"}, 756 | ] 757 | tomli = [ 758 | {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, 759 | {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, 760 | ] 761 | typed-ast = [ 762 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, 763 | {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, 764 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, 765 | {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, 766 | {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, 767 | {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, 768 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, 769 | {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, 770 | {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, 771 | {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, 772 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, 773 | {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, 774 | {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, 775 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, 776 | {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, 777 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, 778 | {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, 779 | {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, 780 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, 781 | {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, 782 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, 783 | {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, 784 | {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, 785 | {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, 786 | ] 787 | typing-extensions = [ 788 | {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, 789 | {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, 790 | ] 791 | zipp = [ 792 | {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, 793 | {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, 794 | ] 795 | --------------------------------------------------------------------------------