diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 59b6009..0000000 --- a/.flake8 +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -ignore = ANN101, ANN102, W503, S101, CFQ004, S311 -max-complexity = 8 -max-line-length = 88 -import-order-style = pycharm -exclude = p38venv,.pytest_cache diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 9014b9f..70c3b9b 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -27,6 +27,6 @@ jobs: run: | echo $RELEASE_VERSION echo ${{ env.RELEASE_VERSION }} -# - name: Deploy -# run: | -# mike deploy --push --update-aliases ${{ env.RELEASE_VERSION }} latest + - name: Deploy + run: | + mike deploy --push --update-aliases ${{ env.RELEASE_VERSION }} latest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..ae6f2a3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,44 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: lint + +on: + push: + branches-ignore: + - 'gh-pages' + pull_request: + branches: [ master, pydantic_v2 ] + +jobs: + lint: + name: "Python ${{ matrix.python-version }}" + runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'collerek/ormar' + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Install Poetry + uses: snok/install-poetry@v1.3.3 + with: + version: 1.4.2 + virtualenvs-create: false + + - name: Poetry details + run: | + poetry --version + poetry config --list + + - name: Install dependencies + run: poetry install --extras "all" --no-root + + - name: Format + run: make fmt + + - name: Lint + run: make lint diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml index 4b68afd..7d4292e 100644 --- a/.github/workflows/test-package.yml +++ b/.github/workflows/test-package.yml @@ -1,14 +1,14 @@ # This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: build +name: test on: push: branches-ignore: - 'gh-pages' pull_request: - branches: [ master ] + branches: [ master, pydantic_v2 ] jobs: tests: @@ -17,7 +17,7 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'collerek/ormar' strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", 3.11] + python-version: [3.8, 3.9, "3.10", 3.11] fail-fast: false services: mysql: @@ -39,35 +39,75 @@ jobs: POSTGRES_DB: testsuite ports: - 5432:5432 - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 --name postgres steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: false + - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + + - name: Install Poetry + uses: snok/install-poetry@v1.3.3 + with: + version: 1.4.2 + virtualenvs-create: false + + - name: Poetry details run: | - python -m pip install poetry==1.4.2 - poetry install --extras "all" - env: - POETRY_VIRTUALENVS_CREATE: false + poetry --version + poetry config --list + + - name: Install dependencies + run: poetry install --extras "all" + - name: Run mysql env: DATABASE_URL: "mysql://username:password@127.0.0.1:3306/testsuite" run: bash scripts/test.sh + + - name: Install postgresql-client + run: | + sudo apt-get update + sudo apt-get install --yes postgresql-client + + - name: Connect to PostgreSQL with CLI + run: env PGPASSWORD=password psql -h localhost -U username -c 'SELECT VERSION();' testsuite + + - name: Show max connections + run: env PGPASSWORD=password psql -h localhost -U username -c 'SHOW max_connections;' testsuite + + - name: Alter max connections + run: | + + docker exec -i postgres bash << EOF + sed -i -e 's/max_connections = 100/max_connections = 1000/' /var/lib/postgresql/data/postgresql.conf + sed -i -e 's/shared_buffers = 128MB/shared_buffers = 512MB/' /var/lib/postgresql/data/postgresql.conf + EOF + docker restart --time 0 postgres + sleep 5 + + - name: Show max connections + run: env PGPASSWORD=password psql -h localhost -U username -c 'SHOW max_connections;' testsuite + - name: Run postgres env: DATABASE_URL: "postgresql://username:password@localhost:5432/testsuite" run: bash scripts/test.sh + - name: Run sqlite env: DATABASE_URL: "sqlite:///testsuite" run: bash scripts/test.sh - - run: mypy ormar tests benchmarks + - name: Upload coverage uses: codecov/codecov-action@v3.1.6 + - name: Test & publish code coverage uses: paambaati/codeclimate-action@v5.0.0 if: github.event.pull_request.head.repo.full_name == 'collerek/ormar' diff --git a/.github/workflows/test_docs.yml b/.github/workflows/test_docs.yml new file mode 100644 index 0000000..eb5dd4f --- /dev/null +++ b/.github/workflows/test_docs.yml @@ -0,0 +1,31 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: test_docs + +on: + push: + branches-ignore: + - 'gh-pages' + pull_request: + branches: [ master, pydantic_v2 ] + +jobs: + tests_docs: + name: "Python ${{ matrix.python-version }}" + runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'collerek/ormar' + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + - name: Install dependencies + run: | + python -m pip install poetry==1.4.2 + poetry install --extras "all" + env: + POETRY_VIRTUALENVS_CREATE: false + - name: Test docs + run: bash scripts/test_docs.sh diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml new file mode 100644 index 0000000..79bf6aa --- /dev/null +++ b/.github/workflows/type-check.yml @@ -0,0 +1,41 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: type_check + +on: + push: + branches-ignore: + - 'gh-pages' + pull_request: + branches: [ master, pydantic_v2 ] + +jobs: + lint: + name: "Python ${{ matrix.python-version }}" + runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'collerek/ormar' + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Install Poetry + uses: snok/install-poetry@v1.3.3 + with: + version: 1.4.2 + virtualenvs-create: false + + - name: Poetry details + run: | + poetry --version + poetry config --list + + - name: Install dependencies + run: poetry install --extras "all" --no-root + + - name: Type check + run: make type_check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf3df77..c29dc6e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,31 +1,9 @@ repos: - - repo: https://github.com/psf/black - rev: 22.3.0 + - repo: local hooks: - - id: black - exclude: ^(docs_src/|examples/) - - repo: https://github.com/pycqa/flake8 - rev: 3.9.2 - hooks: - - id: flake8 - exclude: ^(docs_src/|examples/|tests/) - args: [ '--max-line-length=88' ] - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.982 - hooks: - - id: mypy - exclude: ^(docs_src/|examples/) - args: [--no-strict-optional, --ignore-missing-imports] - additional_dependencies: [ - types-ujson>=0.1.1, - types-PyMySQL>=1.0.2, - types-ipaddress>=1.0.0, - types-enum34>=1.1.0, - types-cryptography>=3.3.5, - types-orjson>=3.6.0, - types-aiofiles>=0.1.9, - types-pkg-resources>=0.1.3, - types-requests>=2.25.9, - types-toml>=0.10.0, - pydantic>=1.8.2 - ] + - id: pre-commit-local + name: format + entry: make pre-commit + language: python + pass_filenames: false + diff --git a/Makefile b/Makefile index 5aa089d..5b1f0e3 100644 --- a/Makefile +++ b/Makefile @@ -15,18 +15,22 @@ test_mysql: test_sqlite: bash scripts/test.sh -svv +test_docs: + bash scripts/test_docs.sh -svv + test: - pytest + pytest -svv tests/ coverage: - pytest --cov=ormar --cov=tests --cov-fail-under=100 --cov-report=term-missing + pytest --cov=ormar --cov=tests --cov-fail-under=100 --cov-report=term-missing tests -black: - black ormar tests +type_check: + mkdir -p .mypy_cache && poetry run python -m mypy ormar tests --ignore-missing-imports --install-types --non-interactive lint: - black ormar tests - flake8 ormar + poetry run python -m ruff . --fix -mypy: - mypy ormar tests +fmt: + poetry run python -m black . + +pre-commit: fmt lint type_check \ No newline at end of file diff --git a/README.md b/README.md index fa01fb3..34c63b5 100644 --- a/README.md +++ b/README.md @@ -171,39 +171,31 @@ import ormar import sqlalchemy DATABASE_URL = "sqlite:///db.sqlite" -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() - - -# note that this step is optional -> all ormar cares is a internal -# class with name Meta and proper parameters, but this way you do not -# have to repeat the same parameters if you use only one database -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), + engine=sqlalchemy.create_engine(DATABASE_URL), +) # Note that all type hints are optional # below is a perfectly valid model declaration # class Author(ormar.Model): -# class Meta(BaseMeta): -# tablename = "authors" +# ormar_config = base_ormar_config.copy(tablename="authors") # # id = ormar.Integer(primary_key=True) # <= notice no field types # name = ormar.String(max_length=100) class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" + ormar_config = base_ormar_config.copy(tablename="books") id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey(Author) @@ -214,10 +206,9 @@ class Book(ormar.Model): # create the database # note that in production you should use migrations # note that this is not required if you connect to existing database -engine = sqlalchemy.create_engine(DATABASE_URL) # just to be sure we clear the db before -metadata.drop_all(engine) -metadata.create_all(engine) +base_ormar_config.metadata.drop_all(base_ormar_config.engine) +base_ormar_config.metadata.create_all(base_ormar_config.engine) # all functions below are divided into functionality categories @@ -546,7 +537,7 @@ async def raw_data(): async def with_connect(function): # note that for any other backend than sqlite you actually need to # connect to the database to perform db operations - async with database: + async with base_ormar_config.database: await function() # note that if you use framework like `fastapi` you shouldn't connect @@ -576,7 +567,7 @@ for func in [ asyncio.run(with_connect(func)) # drop the database tables -metadata.drop_all(engine) +base_ormar_config.metadata.drop_all(base_ormar_config.engine) ``` ## Ormar Specification @@ -654,7 +645,6 @@ The following keyword arguments are supported on all field types. * `unique: bool` * `choices: typing.Sequence` * `name: str` -* `pydantic_only: bool` All fields are required unless one of the following is set: @@ -664,7 +654,6 @@ All fields are required unless one of the following is set: * `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields** * `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column. Autoincrement is set by default on int primary keys. -* `pydantic_only` - Field is available only as normal pydantic field, not stored in the database. ### Available signals diff --git a/benchmarks/conftest.py b/benchmarks/conftest.py index b5be8da..7105a06 100644 --- a/benchmarks/conftest.py +++ b/benchmarks/conftest.py @@ -3,31 +3,20 @@ import random import string import time -import databases import nest_asyncio +import ormar import pytest import pytest_asyncio -import sqlalchemy - -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config +base_ormar_config = create_config() nest_asyncio.apply() - - -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() pytestmark = pytest.mark.asyncio -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database - - class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -41,8 +30,7 @@ class AuthorWithManyFields(Author): class Publisher(ormar.Model): - class Meta(BaseMeta): - tablename = "publishers" + ormar_config = base_ormar_config.copy(tablename="publishers") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -50,8 +38,7 @@ class Publisher(ormar.Model): class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" + ormar_config = base_ormar_config.copy(tablename="books") id: int = ormar.Integer(primary_key=True) author: Author = ormar.ForeignKey(Author, index=True) @@ -60,13 +47,7 @@ class Book(ormar.Model): year: int = ormar.Integer(nullable=True) -@pytest.fixture(autouse=True, scope="function") # TODO: fix this to be module -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config, scope="function") @pytest_asyncio.fixture @@ -86,7 +67,7 @@ async def authors_in_db(num_models: int): authors = [ Author( name="".join(random.sample(string.ascii_letters, 5)), - score=random.random() * 100, + score=int(random.random() * 100), ) for i in range(0, num_models) ] diff --git a/benchmarks/test_benchmark_bulk_create.py b/benchmarks/test_benchmark_bulk_create.py index 3869cd6..0e6be5b 100644 --- a/benchmarks/test_benchmark_bulk_create.py +++ b/benchmarks/test_benchmark_bulk_create.py @@ -15,7 +15,7 @@ async def test_making_and_inserting_models_in_bulk(aio_benchmark, num_models: in authors = [ Author( name="".join(random.sample(string.ascii_letters, 5)), - score=random.random() * 100, + score=int(random.random() * 100), ) for i in range(0, num_models) ] diff --git a/benchmarks/test_benchmark_create.py b/benchmarks/test_benchmark_create.py index 985fe2f..22ad001 100644 --- a/benchmarks/test_benchmark_create.py +++ b/benchmarks/test_benchmark_create.py @@ -16,7 +16,7 @@ async def test_creating_models_individually(aio_benchmark, num_models: int): for idx in range(0, num_models): author = await Author.objects.create( name="".join(random.sample(string.ascii_letters, 5)), - score=random.random() * 100, + score=int(random.random() * 100), ) authors.append(author) return authors @@ -62,7 +62,7 @@ async def test_get_or_create_when_create(aio_benchmark, num_models: int): for idx in range(0, num_models): author, created = await Author.objects.get_or_create( name="".join(random.sample(string.ascii_letters, 5)), - score=random.random() * 100, + score=int(random.random() * 100), ) assert created authors.append(author) @@ -81,7 +81,7 @@ async def test_update_or_create_when_create(aio_benchmark, num_models: int): for idx in range(0, num_models): author = await Author.objects.update_or_create( name="".join(random.sample(string.ascii_letters, 5)), - score=random.random() * 100, + score=int(random.random() * 100), ) authors.append(author) return authors diff --git a/benchmarks/test_benchmark_init.py b/benchmarks/test_benchmark_init.py index 795324c..510ae89 100644 --- a/benchmarks/test_benchmark_init.py +++ b/benchmarks/test_benchmark_init.py @@ -15,13 +15,13 @@ async def test_initializing_models(aio_benchmark, num_models: int): authors = [ Author( name="".join(random.sample(string.ascii_letters, 5)), - score=random.random() * 100, + score=int(random.random() * 100), ) for i in range(0, num_models) ] assert len(authors) == num_models - initialize_models(num_models) + _ = initialize_models(num_models) @pytest.mark.parametrize("num_models", [10, 20, 40]) diff --git a/benchmarks/test_benchmark_save.py b/benchmarks/test_benchmark_save.py index 333f118..a31c726 100644 --- a/benchmarks/test_benchmark_save.py +++ b/benchmarks/test_benchmark_save.py @@ -15,7 +15,7 @@ async def test_saving_models_individually(aio_benchmark, num_models: int): authors = [ Author( name="".join(random.sample(string.ascii_letters, 5)), - score=random.random() * 100, + score=int(random.random() * 100), ) for i in range(0, num_models) ] diff --git a/docs/fastapi/index.md b/docs/fastapi/index.md index adad782..14cadc0 100644 --- a/docs/fastapi/index.md +++ b/docs/fastapi/index.md @@ -16,17 +16,18 @@ Here you can find a very simple sample application code. It's divided into subsections for clarity. !!!note - If you want to read more on how you can use ormar models in fastapi requests and - responses check the [responses](response.md) and [requests](requests.md) documentation. + If you want to read more on how you can use ormar models in fastapi requests and + responses check the [responses](response.md) and [requests](requests.md) documentation. ## Quick Start !!!note - Note that you can find the full quick start script in the [github](https://github.com/collerek/ormar) repo under examples. + Note that you can find the full quick start script in the [github](https://github.com/collerek/ormar) repo under examples. ### Imports and initialization -First take care of the imports and initialization +Define startup and shutdown procedures using FastAPI lifespan and use is in the +application. ```python from typing import List, Optional @@ -36,29 +37,26 @@ from fastapi import FastAPI import ormar -app = FastAPI() -metadata = sqlalchemy.MetaData() -database = databases.Database("sqlite:///test.db") -app.state.database = database -``` - -### Database connection - -Next define startup and shutdown events (or use middleware) -- note that this is `databases` specific setting not the ormar one -```python -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() +from contextlib import asynccontextmanager +from fastapi import FastAPI -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() +@asynccontextmanager +async def lifespan(_: FastAPI) -> AsyncIterator[None]: + if not config.database.is_connected: + await config.database.connect() + + yield + + if config.database.is_connected: + await config.database.disconnect() + + +base_ormar_config = ormar.OrmarConfig( + metadata=sqlalchemy.MetaData(), + database=databases.Database("sqlite:///test.db"), +) +app = FastAPI(lifespan=lifespan(base_ormar_config)) ``` !!!info @@ -71,21 +69,21 @@ Define ormar models with appropriate fields. Those models will be used instead of pydantic ones. ```python +base_ormar_config = OrmarConfig( + metadata = metadata + database = database +) + + class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -122,7 +120,7 @@ async def create_category(category: Category): @app.put("/items/{item_id}") async def get_item(item_id: int, item: Item): item_db = await Item.objects.get(pk=item_id) - return await item_db.update(**item.dict()) + return await item_db.update(**item.model_dump()) @app.delete("/items/{item_id}") @@ -197,14 +195,14 @@ def test_all_endpoints(): assert items[0] == item item.name = "New name" - response = client.put(f"/items/{item.pk}", json=item.dict()) - assert response.json() == item.dict() + response = client.put(f"/items/{item.pk}", json=item.model_dump()) + assert response.json() == item.model_dump() response = client.get("/items/") items = [Item(**item) for item in response.json()] assert items[0].name == "New name" - response = client.delete(f"/items/{item.pk}", json=item.dict()) + response = client.delete(f"/items/{item.pk}", json=item.model_dump()) assert response.json().get("deleted_rows", "__UNDEFINED__") != "__UNDEFINED__" response = client.get("/items/") items = response.json() diff --git a/docs/fastapi/requests.md b/docs/fastapi/requests.md index 31c8c60..78a7ad5 100644 --- a/docs/fastapi/requests.md +++ b/docs/fastapi/requests.md @@ -23,11 +23,13 @@ Field is not required if (any/many/all) of following: Example: ```python +base_ormar_config = ormar.OrmarConfig( + metadata=metadata + database=database +) + class User(ormar.Model): - class Meta: - tablename: str = "users" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) email: str = ormar.String(max_length=255) @@ -42,8 +44,8 @@ In above example fields `id` (is an `autoincrement` `Integer`), `first_name` ( h If the field is nullable you don't have to include it in payload during creation as well as in response, so given example above you can: !!!Warning - Note that although you do not have to pass the optional field, you still **can** do it. - And if someone will pass a value it will be used later unless you take measures to prevent it. + Note that although you do not have to pass the optional field, you still **can** do it. + And if someone will pass a value it will be used later unless you take measures to prevent it. ```python # note that app is an FastApi app @@ -66,18 +68,18 @@ RequestUser = User.get_pydantic(exclude={"password": ..., "category": {"priority @app.post("/users3/", response_model=User) # here you can also use both ormar/pydantic async def create_user3(user: RequestUser): # use the generated model here # note how now user is pydantic and not ormar Model so you need to convert - return await User(**user.dict()).save() + return await User(**user.model_dump()).save() ``` !!!Note - To see more examples and read more visit [get_pydantic](../models/methods.md#get_pydantic) part of the documentation. + To see more examples and read more visit [get_pydantic](../models/methods.md#get_pydantic) part of the documentation. !!!Warning - The `get_pydantic` method generates all models in a tree of nested models according to an algorithm that allows to avoid loops in models (same algorithm that is used in `dict()`, `select_all()` etc.) + The `get_pydantic` method generates all models in a tree of nested models according to an algorithm that allows to avoid loops in models (same algorithm that is used in `model_dump()`, `select_all()` etc.) - That means that nested models won't have reference to parent model (by default ormar relation is bidirectional). + That means that nested models won't have reference to parent model (by default ormar relation is bidirectional). - Note also that if given model exists in a tree more than once it will be doubled in pydantic models (each occurrence will have separate own model). That way you can exclude/include different fields on different leafs of the tree. + Note also that if given model exists in a tree more than once it will be doubled in pydantic models (each occurrence will have separate own model). That way you can exclude/include different fields on different leafs of the tree. #### Mypy and type checking @@ -94,7 +96,7 @@ RequestUser = User.get_pydantic(exclude={"password": ..., "category": {"priority @app.post("/users3/", response_model=User) async def create_user3(user: RequestUser): # type: ignore # note how now user is not ormar Model so you need to convert - return await User(**user.dict()).save() + return await User(**user.model_dump()).save() ``` The second one is a little bit more hacky and utilizes a way in which fastapi extract function parameters. @@ -105,7 +107,7 @@ You can overwrite the `__annotations__` entry for given param. RequestUser = User.get_pydantic(exclude={"password": ..., "category": {"priority"}}) # do not use the app decorator async def create_user3(user: User): # use ormar model here - return await User(**user.dict()).save() + return await User(**user.model_dump()).save() # overwrite the function annotations entry for user param with generated model create_user3.__annotations__["user"] = RequestUser # manually call app functions (app.get, app.post etc.) and pass your function reference @@ -126,8 +128,7 @@ Sample: import pydantic class UserCreate(pydantic.BaseModel): - class Config: - orm_mode = True + model_config = pydantic.ConfigDict(from_attributes=True) email: str first_name: str @@ -139,5 +140,5 @@ class UserCreate(pydantic.BaseModel): async def create_user3(user: UserCreate): # use pydantic model here # note how now request param is a pydantic model and not the ormar one # so you need to parse/convert it to ormar before you can use database - return await User(**user.dict()).save() + return await User(**user.model_dump()).save() ``` diff --git a/docs/fastapi/response.md b/docs/fastapi/response.md index 2ec0bf2..21d6f2a 100644 --- a/docs/fastapi/response.md +++ b/docs/fastapi/response.md @@ -22,11 +22,13 @@ Field is not required if (any/many/all) of following: Example: ```python +base_ormar_config = ormar.OrmarConfig( + metadata=metadata + database=database +) + class User(ormar.Model): - class Meta: - tablename: str = "users" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) email: str = ormar.String(max_length=255) @@ -50,9 +52,9 @@ async def create_user(user: User): # here we use ormar.Model in request paramet That means that if you do not pass i.e. `first_name` in request it will validate correctly (as field is optional), save in the database and return the saved record without this field (which will also pass validation). !!!Note - Note that although you do not pass the **field value**, the **field itself** is still present in the `response_model` that means it **will be present in response data** and set to `None`. + Note that although you do not pass the **field value**, the **field itself** is still present in the `response_model` that means it **will be present in response data** and set to `None`. - If you want to fully exclude the field from the result read on. + If you want to fully exclude the field from the result read on. ### FastApi `response_model_exclude` @@ -61,7 +63,7 @@ Fastapi has `response_model_exclude` that accepts a set (or a list) of field nam That has it's limitation as `ormar` and `pydantic` accepts also dictionaries in which you can set exclude/include columns also on nested models (more on this below) !!!Warning - Note that you cannot exclude required fields when using `response_model` as it will fail during validation. + Note that you cannot exclude required fields when using `response_model` as it will fail during validation. ```python @app.post("/users/", response_model=User, response_model_exclude={"password"}) @@ -96,9 +98,9 @@ with client as client: ``` !!!Note - Note how in above example `password` field is fully gone from the response data. + Note how in above example `password` field is fully gone from the response data. - Note that you can use this method only for non-required fields. + Note that you can use this method only for non-required fields. #### Nested models excludes @@ -111,13 +113,13 @@ One is a dictionary with nested fields that represents the model tree structure, Assume for a second that our user's category is a separate model: ```python -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = ormar.OrmarConfig( + metadata=metadata + database=database +) class Category(ormar.Model): - class Meta(BaseMeta): - tablename: str = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=255) @@ -125,8 +127,7 @@ class Category(ormar.Model): class User(ormar.Model): - class Meta(BaseMeta): - tablename: str = "users" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) email: str = ormar.String(max_length=255) @@ -147,39 +148,39 @@ Note that you can go in deeper models with double underscore, and if you want to In example `response_model_exclude={"category__priority", "category__other_field", category__nested_model__nested_model_field}` etc. !!!Note - To read more about possible excludes and how to structure your exclude dictionary or set visit [fields](../queries/select-columns.md#fields) section of documentation + To read more about possible excludes and how to structure your exclude dictionary or set visit [fields](../queries/select-columns.md#fields) section of documentation !!!Note - Note that apart from `response_model_exclude` parameter `fastapi` supports also other parameters inherited from `pydantic`. - All of them works also with ormar, but can have some nuances so best to read [dict](../models/methods.md#dict) part of the documentation. + Note that apart from `response_model_exclude` parameter `fastapi` supports also other parameters inherited from `pydantic`. + All of them works also with ormar, but can have some nuances so best to read [dict](../models/methods.md#dict) part of the documentation. -### Exclude in `Model.dict()` +### Exclude in `Model.model_dump()` Alternatively you can just return a dict from `ormar.Model` and use . Like this you can also set exclude/include as dict and exclude fields on nested models too. !!!Warning - Not using a `response_model` will cause api documentation having no response example and schema since in theory response can have any format. + Not using a `response_model` will cause api documentation having no response example and schema since in theory response can have any format. ```python @app.post("/users2/", response_model=User) async def create_user2(user: User): user = await user.save() - return user.dict(exclude={'password'}) - # could be also something like return user.dict(exclude={'category': {'priority'}}) to exclude category priority + return user.model_dump(exclude={'password'}) + # could be also something like return user.model_dump(exclude={'category': {'priority'}}) to exclude category priority ``` !!!Note - Note that above example will nullify the password field even if you pass it in request, but the **field will be still there** as it's part of the response schema, the value will be set to `None`. + Note that above example will nullify the password field even if you pass it in request, but the **field will be still there** as it's part of the response schema, the value will be set to `None`. -If you want to fully exclude the field with this approach simply don't use `response_model` and exclude in Model's dict() +If you want to fully exclude the field with this approach simply don't use `response_model` and exclude in Model's model_dump() Alternatively you can just return a dict from ormar model. Like this you can also set exclude/include as dict and exclude fields on nested models. !!!Note - In theory you loose validation of response here but since you operate on `ormar.Models` the response data have already been validated after db query (as ormar model is pydantic model). + In theory you loose validation of response here but since you operate on `ormar.Models` the response data have already been validated after db query (as ormar model is pydantic model). So if you skip `response_model` altogether you can do something like this: @@ -187,13 +188,13 @@ So if you skip `response_model` altogether you can do something like this: @app.post("/users4/") # note no response_model async def create_user4(user: User): user = await user.save() - return user.dict(exclude={'last_name'}) + return user.model_dump(exclude={'last_name'}) ``` !!!Note - Note that when you skip the response_model you can now **exclude also required fields** as the response is no longer validated after being returned. + Note that when you skip the response_model you can now **exclude also required fields** as the response is no longer validated after being returned. - The cost of this solution is that you loose also api documentation as response schema in unknown from fastapi perspective. + The cost of this solution is that you loose also api documentation as response schema in unknown from fastapi perspective. ### Generate `pydantic` model from `ormar.Model` @@ -210,14 +211,14 @@ async def create_user3(user: User): ``` !!!Note - To see more examples and read more visit [get_pydantic](../models/methods.md#get_pydantic) part of the documentation. + To see more examples and read more visit [get_pydantic](../models/methods.md#get_pydantic) part of the documentation. !!!Warning - The `get_pydantic` method generates all models in a tree of nested models according to an algorithm that allows to avoid loops in models (same algorithm that is used in `dict()`, `select_all()` etc.) + The `get_pydantic` method generates all models in a tree of nested models according to an algorithm that allows to avoid loops in models (same algorithm that is used in `model_dump()`, `select_all()` etc.) - That means that nested models won't have reference to parent model (by default ormar relation is bidirectional). + That means that nested models won't have reference to parent model (by default ormar relation is bidirectional). - Note also that if given model exists in a tree more than once it will be doubled in pydantic models (each occurrence will have separate own model). That way you can exclude/include different fields on different leafs of the tree. + Note also that if given model exists in a tree more than once it will be doubled in pydantic models (each occurrence will have separate own model). That way you can exclude/include different fields on different leafs of the tree. ### Separate `pydantic` model @@ -229,8 +230,7 @@ Sample: import pydantic class UserBase(pydantic.BaseModel): - class Config: - orm_mode = True + model_config = pydantic.ConfigDict(from_attributes=True) email: str first_name: str diff --git a/docs/fields/common-parameters.md b/docs/fields/common-parameters.md index 79ce920..0c05d9d 100644 --- a/docs/fields/common-parameters.md +++ b/docs/fields/common-parameters.md @@ -29,7 +29,6 @@ Automatically changed to True if user provide one of the following: * `default` value or function is provided * `server_default` value or function is provided * `autoincrement` is set on `Integer` `primary_key` field -* **[DEPRECATED]**`pydantic_only=True` is set Specifies if field is optional or required, used both with sql and pydantic. @@ -109,7 +108,7 @@ Used in sql only. Sample usage: -```Python hl_lines="21-23" +```Python hl_lines="20-22" --8<-- "../docs_src/fields/docs004.py" ``` @@ -167,20 +166,6 @@ Sets the unique constraint on a table's column. Used in sql only. -## pydantic_only (**DEPRECATED**) - -**This parameter is deprecated and will be removed in one of next releases!** - -**To check how to declare pydantic only fields that are not saved into database see [pydantic fields section](pydantic-fields.md)** - -`pydantic_only`: `bool` = `False` - -Prevents creation of a sql column for given field. - -Used for data related to given model but not to be stored in the database. - -Used in pydantic only. - ## overwrite_pydantic_type By default, ormar uses predefined pydantic field types that it applies on model creation (hence the type hints are optional). @@ -189,22 +174,25 @@ If you want to, you can apply your own type, that will be **completely** replaci So it's on you as a user to provide a type that is valid in the context of given ormar field type. !!!warning - Note that by default you should use build in arguments that are passed to underlying pydantic field. - - You can check what arguments are supported in field types section or in [pydantic](https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation) docs. + Note that by default you should use build in arguments that are passed to underlying pydantic field. + + You can check what arguments are supported in field types section or in [pydantic](https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation) docs. !!!danger - Setting a wrong type of pydantic field can break your model, so overwrite it only when you know what you are doing. - - As it's easy to break functionality of ormar the `overwrite_pydantic_type` argument is not available on relation fields! + Setting a wrong type of pydantic field can break your model, so overwrite it only when you know what you are doing. + + As it's easy to break functionality of ormar the `overwrite_pydantic_type` argument is not available on relation fields! ```python +base_ormar_config = ormar.OrmarConfig( + metadata=metadata + database=database +) + + # sample overwrites class OverwriteTest(ormar.Model): - class Meta: - tablename = "overwrites" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="overwrites") id: int = ormar.Integer(primary_key=True) my_int: str = ormar.Integer(overwrite_pydantic_type=PositiveInt) @@ -212,18 +200,6 @@ class OverwriteTest(ormar.Model): overwrite_pydantic_type=Optional[Json[Dict[str, int]]]) ``` -## choices - -`choices`: `Sequence` = `[]` - -A set of choices allowed to be used for given field. - -Used for data validation on pydantic side. - -Prevents insertion of value not present in the choices list. - -Used in pydantic only. - [relations]: ../relations/index.md [queries]: ../queries/index.md [pydantic]: https://pydantic-docs.helpmanual.io/usage/types/#constrained-types diff --git a/docs/fields/encryption.md b/docs/fields/encryption.md index 9cc06b0..7fe5be9 100644 --- a/docs/fields/encryption.md +++ b/docs/fields/encryption.md @@ -17,10 +17,14 @@ well as both-way encryption/decryption (`FERNET` backend). To encrypt a field you need to pass at minimum `encrypt_secret` and `encrypt_backend` parameters. -```python hl_lines="7-8" +```python hl_lines="10-12" +base_ormar_config = ormar.OrmarConfig( + metadata=metadata + database=database +) + class Filter(ormar.Model): - class Meta(BaseMeta): - tablename = "filters" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, @@ -59,8 +63,7 @@ Note that since this backend never decrypt the stored value it's only applicable ```python class Hash(ormar.Model): - class Meta(BaseMeta): - tablename = "hashes" + ormar_config = base_ormar_config.copy(tablename="hashes") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=128, @@ -106,8 +109,7 @@ as the returned value is parsed to corresponding python type. ```python class Filter(ormar.Model): - class Meta(BaseMeta): - tablename = "filters" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, @@ -152,8 +154,7 @@ argument by `encrypt_custom_backend`. ```python class Filter(ormar.Model): - class Meta(BaseMeta): - tablename = "filters" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, @@ -161,4 +162,4 @@ class Filter(ormar.Model): encrypt_backend=ormar.EncryptBackends.CUSTOM, encrypt_custom_backend=DummyBackend ) -``` \ No newline at end of file +``` diff --git a/docs/fields/field-types.md b/docs/fields/field-types.md index 4bb25e4..9ca2dd8 100644 --- a/docs/fields/field-types.md +++ b/docs/fields/field-types.md @@ -1,10 +1,10 @@ # Fields -There are 12 basic model field types and a special `ForeignKey` and `Many2Many` fields to establish relationships between models. +There are 12 basic model field types and a special `ForeignKey` and `ManyToMany` fields to establish relationships between models. !!!tip - For explanation of `ForeignKey` and `Many2Many` fields check [relations][relations]. + For explanation of `ForeignKey` and `ManyToMany` fields check [relations][relations]. Each of the `Fields` has assigned both `sqlalchemy` column class and python type that is used to create `pydantic` model. @@ -160,11 +160,16 @@ That way you can i.e. set the value by API, even if value is not `utf-8` compati ```python import base64 ... # other imports skipped for brevity + + +base_ormar_config = ormar.OrmarConfig( + metadata=metadata + database=database +) + + class LargeBinaryStr(ormar.Model): - class Meta: - tablename = "my_str_blobs" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="my_str_blobs") id: int = ormar.Integer(primary_key=True) test_binary: str = ormar.LargeBinary( @@ -215,46 +220,6 @@ So which one to use depends on the backend you use and on the column/ data type * Sqlalchemy column: `sqlalchemy.Enum` * Type (used for pydantic): `Type[Enum]` -#### Choices -You can change any field into `Enum` like field by passing a `choices` list that is accepted by all Field types. - -It will add both: validation in `pydantic` model and will display available options in schema, -therefore it will be available in docs of `fastapi`. - -If you still want to use `Enum` in your application you can do this by passing a `Enum` into choices -and later pass value of given option to a given field (note that Enum is not JsonSerializable). - -```python -# note that imports and endpoints declaration -# is skipped here for brevity -from enum import Enum -class TestEnum(Enum): - val1 = 'Val1' - val2 = 'Val2' - -class TestModel(ormar.Model): - class Meta: - tablename = "org" - metadata = metadata - database = database - - id: int = ormar.Integer(primary_key=True) - # pass list(Enum) to choices - enum_string: str = ormar.String(max_length=100, choices=list(TestEnum)) - -# sample payload coming to fastapi -response = client.post( - "/test_models/", - json={ - "id": 1, - # you need to refer to the value of the `Enum` option - # if called like this, alternatively just use value - # string "Val1" in this case - "enum_string": TestEnum.val1.value - }, -) - -``` [relations]: ../relations/index.md [queries]: ../queries.md diff --git a/docs/fields/pydantic-fields.md b/docs/fields/pydantic-fields.md index 042bd36..b50380c 100644 --- a/docs/fields/pydantic-fields.md +++ b/docs/fields/pydantic-fields.md @@ -22,17 +22,14 @@ If you set a field as `Optional`, it defaults to `None` if not provided and that exactly what's going to happen during loading from database. ```python -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +base_ormar_config = ormar.OrmarConfig( + metadata=sqlalchemy.MetaData(), + database=databases.Database(DATABASE_URL), +) -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database - class ModelTest(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200) @@ -57,17 +54,14 @@ By setting a default value, this value will be set on initialization and databas Note that setting a default to `None` is the same as setting the field to `Optional`. ```python -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +base_ormar_config = ormar.OrmarConfig( + metadata=sqlalchemy.MetaData(), + database=databases.Database(DATABASE_URL), +) -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database - class ModelTest(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200) @@ -97,13 +91,12 @@ on initialization and each database load. from pydantic import Field, PaymentCardNumber # ... -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +base_ormar_config = ormar.OrmarConfig( + metadata=sqlalchemy.MetaData(), + database=databases.Database(DATABASE_URL), +) -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database CARD_NUMBERS = [ "123456789007", @@ -119,8 +112,7 @@ def get_number(): class ModelTest2(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200) @@ -149,13 +141,12 @@ You can provide a value for the field in your `__init__()` method before calling from pydantic import BaseModel # ... -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +base_ormar_config = ormar.OrmarConfig( + metadata=sqlalchemy.MetaData(), + database=databases.Database(DATABASE_URL), +) -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database class PydanticTest(BaseModel): aa: str @@ -163,8 +154,7 @@ class PydanticTest(BaseModel): class ModelTest3(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() # provide your custom init function def __init__(self, **kwargs): @@ -192,4 +182,4 @@ assert test_check.pydantic_test.aa == "random" ``` !!!warning - If you do not provide a value in one of the above ways `ValidationError` will be raised on load from database. \ No newline at end of file + If you do not provide a value in one of the above ways `ValidationError` will be raised on load from database. diff --git a/docs/index.md b/docs/index.md index 5eaaacf..18ccc47 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,7 +21,7 @@ ### Overview -The `ormar` package is an async mini ORM for Python, with support for **Postgres, +The `ormar` package is an async ORM for Python, with support for **Postgres, MySQL**, and **SQLite**. The main benefits of using `ormar` are: @@ -53,13 +53,7 @@ Yet remember that those are - well - tests and not all solutions are suitable to ### Part of the `fastapi` ecosystem -As part of the fastapi ecosystem `ormar` is supported in libraries that somehow work with databases. - -As of now `ormar` is supported by: - -* [`fastapi-users`](https://github.com/frankie567/fastapi-users) -* [`fastapi-crudrouter`](https://github.com/awtkns/fastapi-crudrouter) -* [`fastapi-pagination`](https://github.com/uriyyo/fastapi-pagination) +As part of the fastapi ecosystem `ormar` is supported in selected libraries that somehow work with databases. Ormar remains sql dialect agnostic - so only columns working in all supported backends are implemented. @@ -76,7 +70,6 @@ Ormar is built with: * [`sqlalchemy core`][sqlalchemy-core] for query building. * [`databases`][databases] for cross-database async support. * [`pydantic`][pydantic] for data validation. - * `typing_extensions` for python 3.6 - 3.7 ### License @@ -128,17 +121,20 @@ For tests and basic applications the `sqlalchemy` is more than enough: # 1. Imports import sqlalchemy import databases +import ormar # 2. Initialization DATABASE_URL = "sqlite:///db.sqlite" -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +base_ormar_config = ormar.OrmarConfig( + metadata=sqlalchemy.MetaData(), + database=databases.Database(DATABASE_URL), + engine=sqlalchemy.create_engine(DATABASE_URL), +) # Define models here # 3. Database creation and tables creation -engine = sqlalchemy.create_engine(DATABASE_URL) -metadata.create_all(engine) +base_ormar_config.metadata.create_all(engine) ``` For a sample configuration of alembic and more information regarding migrations and @@ -175,45 +171,39 @@ Note that you can find the same script in examples folder on github. from typing import Optional import databases -import pydantic import ormar import sqlalchemy DATABASE_URL = "sqlite:///db.sqlite" -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() - - -# note that this step is optional -> all ormar cares is a internal -# class with name Meta and proper parameters, but this way you do not -# have to repeat the same parameters if you use only one database -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database - +base_ormar_config = ormar.OrmarConfig( + metadata=sqlalchemy.MetaData(), + database=databases.Database(DATABASE_URL), + engine = sqlalchemy.create_engine(DATABASE_URL), +) +# note that this step is optional -> all ormar cares is a field with name +# ormar_config # and proper parameters, but this way you do not have to repeat +# the same parameters if you use only one database +# # Note that all type hints are optional # below is a perfectly valid model declaration # class Author(ormar.Model): -# class Meta(BaseMeta): -# tablename = "authors" +# ormar_config = base_ormar_config.copy(tablename="authors") # # id = ormar.Integer(primary_key=True) # <= notice no field types # name = ormar.String(max_length=100) class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" + ormar_config = base_ormar_config.copy(tablename="books") id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey(Author) @@ -224,10 +214,9 @@ class Book(ormar.Model): # create the database # note that in production you should use migrations # note that this is not required if you connect to existing database -engine = sqlalchemy.create_engine(DATABASE_URL) # just to be sure we clear the db before -metadata.drop_all(engine) -metadata.create_all(engine) +base_ormar_config.metadata.drop_all(engine) +base_ormar_config.metadata.create_all(engine) # all functions below are divided into functionality categories @@ -662,9 +651,7 @@ The following keyword arguments are supported on all field types. * `server_default: Any` * `index: bool` * `unique: bool` - * `choices: typing.Sequence` * `name: str` - * `pydantic_only: bool` All fields are required unless one of the following is set: @@ -674,7 +661,6 @@ All fields are required unless one of the following is set: * `server_default` - Set a default value for the field on server side (like sqlalchemy's `func.now()`). **Not available for relation fields** * `primary key` with `autoincrement` - When a column is set to primary key and autoincrement is set on this column. Autoincrement is set by default on int primary keys. - * `pydantic_only` - Field is available only as normal pydantic field, not stored in the database. ### Available signals diff --git a/docs/install.md b/docs/install.md index 7ba4afd..fbaff91 100644 --- a/docs/install.md +++ b/docs/install.md @@ -13,22 +13,25 @@ Ormar uses `databases` for connectivity issues, `pydantic` for validation and `s All three should install along the installation of ormar if not present at your system before. * databases -* pydantic>=1.5 +* pydantic * sqlalchemy +The required versions are pinned in the pyproject.toml file. ## Optional dependencies *ormar* has three optional dependencies based on database backend you use: -### Postgresql +### Database backend + +#### Postgresql ```py pip install ormar[postgresql] ``` Will install also `asyncpg` and `psycopg2`. -### Mysql +#### Mysql ```py pip install ormar[mysql] @@ -36,7 +39,7 @@ pip install ormar[mysql] Will install also `aiomysql` and `pymysql`. -### Sqlite +#### Sqlite ```py pip install ormar[sqlite] diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..2637ac9 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,330 @@ +# Migration to 0.20.0 based on pydantic 2.X.X + +Version 0.20.0 provides support for pydantic v2.X.X that provides significant speed boost (validation and serialization is written in rust) and cleaner api for developers, +at the same time it drops support for pydantic v.1.X.X. There are changes in `ormar` interface corresponding to changes made in `pydantic`. + +## Breaking changes + +Migration to version >= 0.20.0 requires several changes in order to work properly. + +## `ormar` Model configuration + +Instead of defining a `Meta` class now each of the ormar models require an ormar_config parameter that is an instance of the `OrmarConfig` class. +Note that the attribute must be named `ormar_config` and be an instance of the config class. + +```python +import databases +import ormar +import sqlalchemy + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + +# ormar < 0.20 +class Album(ormar.Model): + class Meta: + database = database + metadata = metadata + tablename = "albums" + + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + favorite: bool = ormar.Boolean(default=False) + +# ormar >= 0.20 +class AlbumV20(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="albums_v20" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + favorite: bool = ormar.Boolean(default=False) +``` + +### `OrmarConfig` api/ parameters + +The `ormar_config` expose the same set of settings as `Meta` class used to provide. +That means that you can use any of the following parameters initializing the config: + +```python +metadata: Optional[sqlalchemy.MetaData] +database: Optional[databases.Database] +engine: Optional[sqlalchemy.engine.Engine] +tablename: Optional[str] +order_by: Optional[List[str]] +abstract: bool +exclude_parent_fields: Optional[List[str]] +queryset_class: Type[QuerySet] +extra: Extra +constraints: Optional[List[ColumnCollectionConstraint]] +``` + +### `BaseMeta` equivalent - best practice + +Note that to reduce the duplication of code and ease of development it's still recommended to create a base config and provide each of the models with a copy. +OrmarConfig provides a convenient `copy` method for that purpose. + +The `copy` method accepts the same parameters as `OrmarConfig` init, so you can overwrite if needed, but by default it will return already existing attributes, except for: `tablename`, `order_by` and `constraints` which by default are cleared. + +```python hl_lines="5-8 11 20" +import databases +import ormar +import sqlalchemy + +base_ormar_config = ormar.OrmarConfig( + database=databases.Database("sqlite:///db.sqlite"), + metadata=sqlalchemy.MetaData() +) + +class AlbumV20(ormar.Model): + ormar_config = base_ormar_config.copy( + tablename="albums_v20" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + + +class TrackV20(ormar.Model): + ormar_config = base_ormar_config.copy( + tablename="tracks_v20" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) +``` + +## `choices` Field parameter is no longer supported. + +Before version 0.20 you could provide `choices` parameter to any existing ormar Field to limit the accepted values. +This functionality was dropped, and you should use `ormar.Enum` field that was designed for this purpose. +If you want to keep the database field type (i.e. an Integer field) you can always write a custom validator. + +```python +import databases +import ormar +import sqlalchemy + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + +# ormar < 0.20 +class Artist(ormar.Model): + class Meta: + database = database + metadata = metadata + + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + country: str = ormar.String(default=False, max_length=50, choices=["UK", "US", "Vietnam", "Colombia"]) + +# ormar >= 0.20 +from enum import Enum + +class Country(str, Enum): + UK = "UK" + US = "US" + VIETNAM = "Vietnam" + COLOMBIA = "Colombia" + +class ArtistV20(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="artists_v20" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + country: Country = ormar.Enum(enum_class=Country) +``` + + +## `pydantic_only` Field parameter is no longer supported + +`pydantic_only` fields were already deprecated and are removed in v 0.20. Ormar allows defining pydantic fields as in ordinary pydantic model. + +```python +import databases +import ormar +import sqlalchemy + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + +# ormar < 0.20 +class Dish(ormar.Model): + class Meta: + database = database + metadata = metadata + tablename = "dishes" + + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + cook: str = ormar.String(max_length=40, pydantic_only=True, default="sam") + +# ormar >= 0.20 +class DishV20(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="dishes_v20" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + cook: str = "sam" # this is normal pydantic field +``` + +## `property_field` decorator is no longer supported + +`property_field` decorator was used to provide a way to pass calculated fields that were included in dictionary/ serialized json representation of the model. +Version 2.X of pydantic introduced such a possibility, so you should now switch to the one native to the pydantic. + +```python +import databases +import ormar +import sqlalchemy +import pydantic + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + +# ormar < 0.20 +class Employee(ormar.Model): + class Meta: + database = database + metadata = metadata + + + id: int = ormar.Integer(primary_key=True) + first_name: str = ormar.String(max_length=100) + last_name: str = ormar.String(max_length=100) + + @ormar.property_field() + def full_name(self) -> str: + return f"{self.first_name} {self.last_name}" + +# ormar >= 0.20 +class EmployeeV20(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) + + id: int = ormar.Integer(primary_key=True) + first_name: str = ormar.String(max_length=100) + last_name: str = ormar.String(max_length=100) + + @pydantic.computed_field() + def full_name(self) -> str: + return f"{self.first_name} {self.last_name}" +``` + +## Deprecated methods + +All methods listed below are deprecated and will be removed in version 0.30 of `ormar`. + +### `dict()` becomes the `model_dump()` + +```python +import databases +import ormar +import sqlalchemy + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + +class Album(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="albums" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + favorite: bool = ormar.Boolean(default=False) + +album = Album(name="Dark Side of the Moon") + +# ormar < 0.20 +album_dict = album.dict() + +# ormar >= 0.20 +new_album_dict = album.model_dump() +``` + +Note that parameters remain the same i.e. `include`, `exclude` etc. + +### `json()` becomes the `model_dump_json()` + +```python +import databases +import ormar +import sqlalchemy + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + +class Album(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="albums" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + favorite: bool = ormar.Boolean(default=False) + +album = Album(name="Dark Side of the Moon") + +# ormar < 0.20 +album_json= album.json() + +# ormar >= 0.20 +new_album_dict = album.model_dump_json() +``` + +Note that parameters remain the same i.e. `include`, `exclude` etc. + +### `construct()` becomes the `model_construct()` + +```python +import databases +import ormar +import sqlalchemy + +database = databases.Database("sqlite:///db.sqlite") +metadata = sqlalchemy.MetaData() + +class Album(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="albums" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + favorite: bool = ormar.Boolean(default=False) + +params = { + "name": "Dark Side of the Moon", + "favorite": True, +} +# ormar < 0.20 +album = Album.construct(**params) + +# ormar >= 0.20 +album = Album.model_construct(**params) +``` + +To read more about construct please refer to `pydantic` documentation. \ No newline at end of file diff --git a/docs/models/index.md b/docs/models/index.md index c30a4d4..2272e43 100644 --- a/docs/models/index.md +++ b/docs/models/index.md @@ -9,7 +9,7 @@ They are being managed in the background and you do not have to create them on y To build an ormar model you simply need to inherit a `ormar.Model` class. -```Python hl_lines="10" +```Python hl_lines="9" --8<-- "../docs_src/models/docs001.py" ``` @@ -23,7 +23,7 @@ Each table **has to** have a primary key column, which you specify by setting `p Only one primary key column is allowed. -```Python hl_lines="15 16 17" +```Python hl_lines="15-17" --8<-- "../docs_src/models/docs001.py" ``` @@ -42,15 +42,15 @@ id: int = ormar.Integer(primary_key=True, autoincrement=False) #### Non Database Fields Note that if you need a normal pydantic field in your model (used to store value on model or pass around some value) you can define a -field with parameter `pydantic_only=True`. +field like usual in pydantic. Fields created like this are added to the `pydantic` model fields -> so are subject to validation according to `Field` type, -also appear in `dict()` and `json()` result. +also appear in `model_dump()` and `model_dump_json()` result. The difference is that **those fields are not saved in the database**. So they won't be included in underlying sqlalchemy `columns`, or `table` variables (check [Internals][Internals] section below to see how you can access those if you need). -Subsequently `pydantic_only` fields won't be included in migrations or any database operation (like `save`, `update` etc.) +Subsequently, pydantic fields won't be included in migrations or any database operation (like `save`, `update` etc.) Fields like those can be passed around into payload in `fastapi` request and will be returned in `fastapi` response (of course only if you set their value somewhere in your code as the value is **not** fetched from the db. @@ -58,30 +58,32 @@ If you pass a value in `fastapi` `request` and return the same instance that `fa you should get back exactly same value in `response`.). !!!warning - `pydantic_only=True` fields are always **Optional** and it cannot be changed (otherwise db load validation would fail) + pydantic fields have to be always **Optional** and it cannot be changed (otherwise db load validation would fail) -!!!tip - `pydantic_only=True` fields are a good solution if you need to pass additional information from outside of your API - (i.e. frontend). They are not stored in db but you can access them in your `APIRoute` code and they also have `pydantic` validation. - -```Python hl_lines="18" +```Python hl_lines="19" --8<-- "../docs_src/models/docs014.py" ``` -If you combine `pydantic_only=True` field with `default` parameter and do not pass actual value in request you will always get default value. +If you set pydantic field with `default` parameter and do not pass actual value in request you will always get default value. Since it can be a function you can set `default=datetime.datetime.now` and get current timestamp each time you call an endpoint etc. +#### Non Database Fields in Fastapi + !!!note - Note that both `pydantic_only` and `property_field` decorated field can be included/excluded in both `dict()` and `fastapi` + Note, that both pydantic and calculated_fields decorated field can be included/excluded in both `model_dump()` and `fastapi` response with `include`/`exclude` and `response_model_include`/`response_model_exclude` accordingly. ```python -# <==related of code removed for clarity==> +# <==part of related code removed for clarity==> +base_ormar_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), + engine=sqlalchemy.create_engine(DATABASE_URL), +) + + class User(ormar.Model): - class Meta: - tablename: str = "users2" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="users2") id: int = ormar.Integer(primary_key=True) email: str = ormar.String(max_length=255, nullable=False) @@ -89,18 +91,18 @@ class User(ormar.Model): first_name: str = ormar.String(max_length=255) last_name: str = ormar.String(max_length=255) category: str = ormar.String(max_length=255, nullable=True) - timestamp: datetime.datetime = ormar.DateTime( - pydantic_only=True, default=datetime.datetime.now + timestamp: datetime.datetime = pydantic.Field( + default=datetime.datetime.now ) -# <==related of code removed for clarity==> -app =FastAPI() +# <==part of related code removed for clarity==> +app = FastAPI() @app.post("/users/") async def create_user(user: User): return await user.save() -# <==related of code removed for clarity==> +# <==part of related code removed for clarity==> def test_excluding_fields_in_endpoints(): client = TestClient(app) @@ -127,121 +129,7 @@ def test_excluding_fields_in_endpoints(): assert response.json().get("timestamp") == str(timestamp).replace(" ", "T") -# <==related of code removed for clarity==> -``` - -#### Property fields - -Sometimes it's desirable to do some kind of calculation on the model instance. One of the most common examples can be concatenating -two or more fields. Imagine you have `first_name` and `last_name` fields on your model, but would like to have `full_name` in the result -of the `fastapi` query. - -You can create a new `pydantic` model with a `method` that accepts only `self` (so like default python `@property`) -and populate it in your code. - -But it's so common that `ormar` has you covered. You can "materialize" a `property_field` on you `Model`. - -!!!warning - `property_field` fields are always **Optional** and it cannot be changed (otherwise db load validation would fail) - -```Python hl_lines="20-22" ---8<-- "../docs_src/models/docs015.py" -``` - -!!!warning - The decorated function has to accept only one parameter, and that parameter have to be `self`. - - If you try to decorate a function with more parameters `ormar` will raise `ModelDefinitionError`. - - Sample: - - ```python - # will raise ModelDefinitionError - @property_field - def prefixed_name(self, prefix="prefix_"): - return 'custom_prefix__' + self.name - - # will raise ModelDefinitionError - # (calling first param something else than 'self' is a bad practice anyway) - @property_field - def prefixed_name(instance): - return 'custom_prefix__' + self.name - ``` - -Note that `property_field` decorated methods do not go through verification (but that might change in future) and are only available -in the response from `fastapi` and `dict()` and `json()` methods. You cannot pass a value for this field in the request -(or rather you can but it will be discarded by ormar so really no point but no Exception will be raised). - -!!!note - Note that both `pydantic_only` and `property_field` decorated field can be included/excluded in both `dict()` and `fastapi` - response with `include`/`exclude` and `response_model_include`/`response_model_exclude` accordingly. - -!!!tip - Note that `@property_field` decorator is designed to replace the python `@property` decorator, you do not have to combine them. - - In theory you can cause `ormar` have a failsafe mechanism, but note that i.e. `mypy` will complain about re-decorating a property. - - ```python - # valid and working but unnecessary and mypy will complain - @property_field - @property - def prefixed_name(self): - return 'custom_prefix__' + self.name - ``` - -```python -# <==related of code removed for clarity==> -def gen_pass(): # note: NOT production ready - choices = string.ascii_letters + string.digits + "!@#$%^&*()" - return "".join(random.choice(choices) for _ in range(20)) - -class RandomModel(ormar.Model): - class Meta: - tablename: str = "random_users" - metadata = metadata - database = database - - include_props_in_dict = True - - id: int = ormar.Integer(primary_key=True) - password: str = ormar.String(max_length=255, default=gen_pass) - first_name: str = ormar.String(max_length=255, default="John") - last_name: str = ormar.String(max_length=255) - created_date: datetime.datetime = ormar.DateTime( - server_default=sqlalchemy.func.now() - ) - - @property_field - def full_name(self) -> str: - return " ".join([self.first_name, self.last_name]) - -# <==related of code removed for clarity==> -app =FastAPI() - -# explicitly exclude property_field in this endpoint -@app.post("/random/", response_model=RandomModel, response_model_exclude={"full_name"}) -async def create_user(user: RandomModel): - return await user.save() - -# <==related of code removed for clarity==> - -def test_excluding_property_field_in_endpoints2(): - client = TestClient(app) - with client as client: - RandomModel.Meta.include_props_in_dict = True - user3 = {"last_name": "Test"} - response = client.post("/random3/", json=user3) - assert list(response.json().keys()) == [ - "id", - "password", - "first_name", - "last_name", - "created_date", - ] - # despite being decorated with property_field if you explicitly exclude it it will be gone - assert response.json().get("full_name") is None - -# <==related of code removed for clarity==> +# <==part of related code removed for clarity==> ``` #### Fields names vs Column names @@ -252,25 +140,25 @@ If for whatever reason you prefer to change the name in the database but keep th with specifying `name` parameter during Field declaration Here you have a sample model with changed names -```Python hl_lines="16-19" +```Python hl_lines="18-21" --8<-- "../docs_src/models/docs008.py" ``` Note that you can also change the ForeignKey column name -```Python hl_lines="21" +```Python hl_lines="34" --8<-- "../docs_src/models/docs009.py" ``` But for now you cannot change the ManyToMany column names as they go through other Model anyway. -```Python hl_lines="28" +```Python hl_lines="43" --8<-- "../docs_src/models/docs010.py" ``` -## Overwriting the default QuerySet +### Overwriting the default QuerySet If you want to customize the queries run by ormar you can define your own queryset class (that extends the ormar `QuerySet`) in your model class, default one is simply the `QuerySet` -You can provide a new class in `Meta` configuration of your class as `queryset_class` parameter. +You can provide a new class in `ormar_config` of your class as `queryset_class` parameter. ```python import ormar @@ -288,12 +176,10 @@ class MyQuerySetClass(QuerySet): class Book(ormar.Model): - - class Meta(ormar.ModelMeta): - metadata = metadata - database = database - tablename = "book" - queryset_class = MyQuerySetClass + ormar_config = base_ormar_config.copy( + queryset_class=MyQuerySetClass, + tablename="book", + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=32) @@ -304,17 +190,9 @@ book = await Book.objects.first_or_404(name="123") ``` -### Type Hints & Legacy +### Type Hints -Before version 0.4.0 `ormar` supported only one way of defining `Fields` on a `Model` using python type hints as pydantic. - -```Python hl_lines="15-17" ---8<-- "../docs_src/models/docs011.py" -``` - -But that didn't play well with static type checkers like `mypy` and `pydantic` PyCharm plugin. - -Therefore from version >=0.4.0 `ormar` switched to new notation. +Note that for better IDE support and mypy checks you can provide type hints. ```Python hl_lines="15-17" --8<-- "../docs_src/models/docs001.py" @@ -343,9 +221,9 @@ and table creation you need to assign each `Model` with two special parameters. One is `Database` instance created with your database url in [sqlalchemy connection string][sqlalchemy connection string] format. -Created instance needs to be passed to every `Model` with `Meta` class `database` parameter. +Created instance needs to be passed to every `Model` with `ormar_config` object `database` parameter. -```Python hl_lines="1 6 12" +```Python hl_lines="1 5 11" --8<-- "../docs_src/models/docs001.py" ``` @@ -357,9 +235,9 @@ Created instance needs to be passed to every `Model` with `Meta` class `database Second dependency is sqlalchemy `MetaData` instance. -Created instance needs to be passed to every `Model` with `Meta` class `metadata` parameter. +Created instance needs to be passed to every `Model` with `ormar_config` object `metadata` parameter. -```Python hl_lines="2 7 13" +```Python hl_lines="3 6 12" --8<-- "../docs_src/models/docs001.py" ``` @@ -369,25 +247,22 @@ Created instance needs to be passed to every `Model` with `Meta` class `metadata #### Best practice -Only thing that `ormar` expects is a class with name `Meta` and two class variables: `metadata` and `databases`. +Note that `ormar` expects the field with name `ormar_config` that is an instance of `OrmarConfig` class. +To ease the config management, the `OrmarConfig` class provide `copy` method. +So instead of providing the same parameters over and over again for all models +you should create a base object and use its copy in all models. -So instead of providing the same parameters over and over again for all models you should creata a class and subclass it in all models. - -```Python hl_lines="14 20 33" +```Python hl_lines="9-12 19 28" --8<-- "../docs_src/models/docs013.py" ``` -!!!warning - You need to subclass your `MainMeta` class in each `Model` class as those classes store configuration variables - that otherwise would be overwritten by each `Model`. - ### Table Names By default table name is created from Model class name as lowercase name plus 's'. -You can overwrite this parameter by providing `Meta` class `tablename` argument. +You can overwrite this parameter by providing `ormar_config` object's `tablename` argument. -```Python hl_lines="12 13 14" +```Python hl_lines="14-16" --8<-- "../docs_src/models/docs002.py" ``` @@ -395,74 +270,72 @@ You can overwrite this parameter by providing `Meta` class `tablename` argument. On a model level you can also set model-wise constraints on sql columns. -Right now only `IndexColumns` and `UniqueColumns` constraints are supported. +Right now only `IndexColumns`, `UniqueColumns` and `CheckColumns` constraints are supported. !!!note - Note that both constraints should be used only if you want to set a name on constraint or want to set the index on multiple columns, otherwise `index` and `unique` properties on ormar fields are preferred. + Note that both constraints should be used only if you want to set a name on constraint or want to set the index on multiple columns, otherwise `index` and `unique` properties on ormar fields are preferred. !!!tip To read more about columns constraints like `primary_key`, `unique`, `ForeignKey` etc. visit [fields][fields]. #### UniqueColumns -You can set this parameter by providing `Meta` class `constraints` argument. +You can set this parameter by providing `ormar_config` object `constraints` argument. -```Python hl_lines="14-17" +```Python hl_lines="13-16" --8<-- "../docs_src/models/docs006.py" ``` !!!note - Note that constraints are meant for combination of columns that should be unique. - To set one column as unique use [`unique`](../fields/common-parameters.md#unique) common parameter. - Of course you can set many columns as unique with this param but each of them will be checked separately. + Note that constraints are meant for combination of columns that should be unique. + To set one column as unique use [`unique`](../fields/common-parameters.md#unique) common parameter. + Of course you can set many columns as unique with this param but each of them will be checked separately. #### IndexColumns -You can set this parameter by providing `Meta` class `constraints` argument. +You can set this parameter by providing `ormar_config` object `constraints` argument. -```Python hl_lines="14-17" +```Python hl_lines="13-16" --8<-- "../docs_src/models/docs017.py" ``` !!!note - Note that constraints are meant for combination of columns that should be in the index. - To set one column index use [`unique`](../fields/common-parameters.md#index) common parameter. - Of course, you can set many columns as indexes with this param but each of them will be a separate index. + Note that constraints are meant for combination of columns that should be in the index. + To set one column index use [`unique`](../fields/common-parameters.md#index) common parameter. + Of course, you can set many columns as indexes with this param but each of them will be a separate index. #### CheckColumns -You can set this parameter by providing `Meta` class `constraints` argument. +You can set this parameter by providing `ormar_config` object `constraints` argument. -```Python hl_lines="14-17" +```Python hl_lines="15-20" --8<-- "../docs_src/models/docs018.py" ``` !!!note - Note that some databases do not actively support check constraints such as MySQL. + Note that some databases do not actively support check constraints (such as MySQL). ### Pydantic configuration As each `ormar.Model` is also a `pydantic` model, you might want to tweak the settings of the pydantic configuration. -The way to do this in pydantic is to adjust the settings on the `Config` class provided to your model, and it works exactly the same for ormar models. +The way to do this in pydantic is to adjust the settings on the `model_config` dictionary provided to your model, and it works exactly the same for ormar models. -So in order to set your own preferences you need to provide not only the `Meta` class but also the `Config` class to your model. +So in order to set your own preferences you need to provide not only the `ormar_config` class but also the `model_config = ConfigDict()` class to your model. !!!note - To read more about available settings visit the [pydantic](https://pydantic-docs.helpmanual.io/usage/model_config/) config page. + To read more about available settings visit the [pydantic](https://pydantic-docs.helpmanual.io/usage/model_config/) config page. Note that if you do not provide your own configuration, ormar will do it for you. The default config provided is as follows: ```python -class Config(pydantic.BaseConfig): - orm_mode = True - validate_assignment = True +model_config = ConfigDict(validate_assignment=True, ser_json_bytes="base64") ``` So to overwrite setting or provide your own a sample model can look like following: -```Python hl_lines="15-16" +```Python hl_lines="16" --8<-- "../docs_src/models/docs016.py" ``` @@ -474,69 +347,64 @@ If you try to do so the `ModelError` will be raised. Since the extra fields cannot be saved in the database the default to disallow such fields seems a feasible option. -On the contrary in `pydantic` the default option is to ignore such extra fields, therefore `ormar` provides an `Meta.extra` setting to behave in the same way. +On the contrary in `pydantic` the default option is to ignore such extra fields, therefore `ormar` provides an `ormar_config.extra` setting to behave in the same way. To ignore extra fields passed to `ormar` set this setting to `Extra.ignore` instead of default `Extra.forbid`. Note that `ormar` does not allow accepting extra fields, you can only ignore them or forbid them (raise exception if present) ```python -from ormar import Extra +from ormar import Extra, OrmarConfig class Child(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "children" - metadata = metadata - database = database - extra = Extra.ignore # set extra setting to prevent exceptions on extra fields presence + ormar_config = OrmarConfig( + tablename="children", + extra=Extra.ignore # set extra setting to prevent exceptions on extra fields presence + ) id: int = ormar.Integer(name="child_id", primary_key=True) first_name: str = ormar.String(name="fname", max_length=100) last_name: str = ormar.String(name="lname", max_length=100) ``` -To set the same setting on all model check the [best practices]("../models/index/#best-practice") and `BaseMeta` concept. +To set the same setting on all model check the [best practices]("../models/index/#best-practice") and `base_ormar_config` concept. ## Model sort order When querying the database with given model by default the Model is ordered by the `primary_key` column ascending. If you wish to change the default behaviour you can do it by providing `orders_by` -parameter to model `Meta` class. +parameter to model `ormar_config` object. -Sample default ordering: +Sample default ordering (not specified - so by primary key): ```python -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +base_ormar_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database - # default sort by column id ascending class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy( + tablename="authors", + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) ``` Modified -```python - -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +```python hl_lines="9" +base_ormar_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) # now default sort by name descending class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" - orders_by = ["-name"] + ormar_config = base_ormar_config.copy( + orders_by = ["-name"], + tablename="authors", + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -546,12 +414,9 @@ class Author(ormar.Model): There are two ways to create and persist the `Model` instance in the database. -!!!tip - Use `ipython` to try this from the console, since it supports `await`. - If you plan to modify the instance in the later execution of your program you can initiate your `Model` as a normal class and later await a `save()` call. -```Python hl_lines="20 21" +```Python hl_lines="25-26" --8<-- "../docs_src/models/docs007.py" ``` @@ -561,7 +426,7 @@ For creating multiple objects at once a `bulk_create()` QuerySet's method is ava Each model has a `QuerySet` initialised as `objects` parameter -```Python hl_lines="23" +```Python hl_lines="28" --8<-- "../docs_src/models/docs007.py" ``` diff --git a/docs/models/inheritance.md b/docs/models/inheritance.md index 0f4294c..cca98eb 100644 --- a/docs/models/inheritance.md +++ b/docs/models/inheritance.md @@ -7,7 +7,7 @@ Out of various types of ORM models inheritance `ormar` currently supports two of ## Types of inheritance -The short summary of different types of inheritance is: +The short summary of different types of inheritance: * **Mixins [SUPPORTED]** - don't subclass `ormar.Model`, just define fields that are later used on different models (like `created_date` and `updated_date` on each model), @@ -32,6 +32,13 @@ To use Mixins just define a class that is not inheriting from an `ormar.Model` b defining `ormar.Fields` as class variables. ```python +base_ormar_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), + engine=sqlalchemy.create_engine(DATABASE_URL), +) + + # a mixin defines the fields but is a normal python class class AuditMixin: created_by: str = ormar.String(max_length=100) @@ -45,10 +52,7 @@ class DateFieldsMixins: # a models can inherit from one or more mixins class Category(ormar.Model, DateFieldsMixins, AuditMixin): - class Meta(ormar.ModelMeta): - tablename = "categories" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50, unique=True, index=True) @@ -57,7 +61,7 @@ class Category(ormar.Model, DateFieldsMixins, AuditMixin): !!!tip Note that Mixins are **not** models, so you still need to inherit - from `ormar.Model` as well as define `Meta` class in the **final** model. + from `ormar.Model` as well as define `ormar_config` field in the **final** model. A Category class above will have four additional fields: `created_date`, `updated_date`, `created_by` and `updated_by`. @@ -73,11 +77,11 @@ In concept concrete table inheritance is very similar to Mixins, but uses actual `ormar.Models` as base classes. !!!warning - Note that base classes have `abstract=True` set in `Meta` class, if you try + Note that base classes have `abstract=True` set in `ormar_config` object, if you try to inherit from non abstract marked class `ModelDefinitionError` will be raised. Since this abstract Model will never be initialized you can skip `metadata` -and `database` in it's `Meta` definition. +and `database` in it's `ormar_config` definition. But if you provide it - it will be inherited, that way you do not have to provide `metadata` and `databases` in the final/concrete class @@ -91,8 +95,7 @@ otherwise an error will be raised. # note that base classes have abstract=True # since this model will never be initialized you can skip metadata and database class AuditModel(ormar.Model): - class Meta: - abstract = True + ormar_config = base_ormar_config.copy(abstract=True) created_by: str = ormar.String(max_length=100) updated_by: str = ormar.String(max_length=100, default="Sam") @@ -100,10 +103,11 @@ class AuditModel(ormar.Model): # but if you provide it it will be inherited - DRY (Don't Repeat Yourself) in action class DateFieldsModel(ormar.Model): - class Meta: - abstract = True - metadata = metadata - database = db + ormar_config = base_ormar_config.copy( + abstract=True, + metadata=metadata, + database=db, + ) created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now) updated_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now) @@ -111,8 +115,7 @@ class DateFieldsModel(ormar.Model): # that way you do not have to provide metadata and databases in concrete class class Category(DateFieldsModel, AuditModel): - class Meta(ormar.ModelMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50, unique=True, index=True) @@ -123,8 +126,6 @@ class Category(DateFieldsModel, AuditModel): The list of inherited options/settings is as follows: `metadata`, `database` and `constraints`. -Also methods decorated with `@property_field` decorator will be inherited/recognized. - Of course apart from that all fields from base classes are combined and created in the concrete table of the final Model. @@ -140,15 +141,16 @@ inheritance. Whenever you define a field with same name and new definition it will completely replace the previously defined one. -```python +```python hl_lines="28" # base class class DateFieldsModel(ormar.Model): - class Meta: - abstract = True - metadata = metadata - database = db + ormar_config = OrmarConfig( + abstract=True, + metadata=metadata, + database=db, # note that UniqueColumns need sqlalchemy db columns names not the ormar ones - constraints = [ormar.UniqueColumns("creation_date", "modification_date")] + constraints=[ormar.UniqueColumns("creation_date", "modification_date")] + ) created_date: datetime.datetime = ormar.DateTime( default=datetime.datetime.now, name="creation_date" @@ -159,10 +161,11 @@ class DateFieldsModel(ormar.Model): class RedefinedField(DateFieldsModel): - class Meta(ormar.ModelMeta): - tablename = "redefines" - metadata = metadata - database = db + ormar_config = OrmarConfig( + tablename="redefines", + metadata=metadata, + database=db, + ) id: int = ormar.Integer(primary_key=True) # here the created_date is replaced by the String field @@ -170,12 +173,12 @@ class RedefinedField(DateFieldsModel): # you can verify that the final field is correctly declared and created -changed_field = RedefinedField.Meta.model_fields["created_date"] +changed_field = RedefinedField.ormar_config.model_fields["created_date"] assert changed_field.default is None assert changed_field.alias == "creation_date" -assert any(x.name == "creation_date" for x in RedefinedField.Meta.table.columns) +assert any(x.name == "creation_date" for x in RedefinedField.ormar_config.table.columns) assert isinstance( - RedefinedField.Meta.table.columns["creation_date"].type, + RedefinedField.ormar_config.table.columns["creation_date"].type, sqlalchemy.sql.sqltypes.String, ) ``` @@ -225,9 +228,7 @@ That might sound complicated but let's look at the following example: ```python # normal model used in relation class Person(ormar.Model): - class Meta: - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -235,10 +236,7 @@ class Person(ormar.Model): # parent model - needs to be abstract class Car(ormar.Model): - class Meta: - abstract = True - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(abstract=True) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50) @@ -249,16 +247,13 @@ class Car(ormar.Model): class Truck(Car): - class Meta: - pass + ormar_config = base_ormar_config.copy() max_capacity: int = ormar.Integer() class Bus(Car): - class Meta: - # default naming is name.lower()+'s' so it's ugly for buss ;) - tablename = "buses" + ormar_config = base_ormar_config.copy(tablename="buses") max_persons: int = ormar.Integer() ``` @@ -266,7 +261,7 @@ class Bus(Car): Now when you will inspect the fields on Person model you will get: ```python -Person.Meta.model_fields +Person.ormar_config.model_fields """ {'id': , 'name': , @@ -293,8 +288,7 @@ different `related_name` parameter. ```python # rest of the above example remains the same class Bus(Car): - class Meta: - tablename = "buses" + ormar_config = base_ormar_config.copy(tablename="buses") # new field that changes the related_name owner: Person = ormar.ForeignKey(Person, related_name="buses") @@ -304,7 +298,7 @@ class Bus(Car): Now the columns looks much better. ```python -Person.Meta.model_fields +Person.ormar_config.model_fields """ {'id': , 'name': , @@ -328,7 +322,7 @@ Person.Meta.model_fields Similarly, you can inherit from Models that have ManyToMany relations declared but there is one, but substantial difference - the Through model. -Since in the future the Through model will be able to hold additional fields and now it links only two Tables +Since the Through model will be able to hold additional fields, and now it links only two Tables (`from` and `to` ones), each child that inherits the m2m relation field has to have separate Through model. @@ -344,27 +338,18 @@ We will modify the previous example described above to use m2m relation for co_o ```python # person remain the same as above class Person(ormar.Model): - class Meta: - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) # new through model between Person and Car2 class PersonsCar(ormar.Model): - class Meta: - tablename = "cars_x_persons" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="cars_x_persons") # note how co_owners is now ManyToMany relation class Car2(ormar.Model): - class Meta: - # parent class needs to be marked abstract - abstract = True - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(abstract=True) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50) @@ -379,16 +364,13 @@ class Car2(ormar.Model): # child models define only additional Fields class Truck2(Car2): - class Meta: - # note how you don't have to provide inherited Meta params - tablename = "trucks2" + ormar_config = base_ormar_config.copy(tablename="trucks2") max_capacity: int = ormar.Integer() class Bus2(Car2): - class Meta: - tablename = "buses2" + ormar_config = base_ormar_config.copy(tablename="buses2") max_persons: int = ormar.Integer() ``` @@ -402,7 +384,7 @@ That way for class Truck2 the relation defined in You can verify the names by inspecting the list of fields present on `Person` model. ```python -Person.Meta.model_fields +Person.ormar_config.model_fields { # note how all relation fields need to be unique on Person # regardless if autogenerated or manually overwritten @@ -425,14 +407,14 @@ But that's not all. It's kind of internal to `ormar` but affects the data struct so let's examine the through models for both `Bus2` and `Truck2` models. ```python -Bus2.Meta.model_fields['co_owners'].through +Bus2.ormar_config.model_fields['co_owners'].through -Bus2.Meta.model_fields['co_owners'].through.Meta.tablename +Bus2.ormar_config.model_fields['co_owners'].through.ormar_config.tablename 'cars_x_persons_buses2' -Truck2.Meta.model_fields['co_owners'].through +Truck2.ormar_config.model_fields['co_owners'].through -Truck2.Meta.model_fields['co_owners'].through.Meta.tablename +Truck2.ormar_config.model_fields['co_owners'].through.ormar_config.tablename 'cars_x_persons_trucks2' ``` @@ -443,7 +425,7 @@ the name of the **table** from the child is used. Note that original model is not only not used, the table for this model is removed from metadata: ```python -Bus2.Meta.metadata.tables.keys() +Bus2.ormar_config.metadata.tables.keys() dict_keys(['test_date_models', 'categories', 'subjects', 'persons', 'trucks', 'buses', 'cars_x_persons_trucks2', 'trucks2', 'cars_x_persons_buses2', 'buses2']) ``` @@ -469,26 +451,24 @@ Ormar allows you to skip certain fields in inherited model that are coming from !!!Note Note that the same behaviour can be achieved by splitting the model into more abstract models and mixins - which is a preferred way in normal circumstances. -To skip certain fields from a child model, list all fields that you want to skip in `model.Meta.exclude_parent_fields` parameter like follows: +To skip certain fields from a child model, list all fields that you want to skip in `model.ormar_config.exclude_parent_fields` parameter like follows: ```python -metadata = sa.MetaData() -db = databases.Database(DATABASE_URL) +base_ormar_config = OrmarConfig( + metadata=sa.MetaData(), + database=databases.Database(DATABASE_URL), +) class AuditModel(ormar.Model): - class Meta: - abstract = True + ormar_config = base_ormar_config.copy(abstract=True) created_by: str = ormar.String(max_length=100) updated_by: str = ormar.String(max_length=100, default="Sam") class DateFieldsModel(ormar.Model): - class Meta(ormar.ModelMeta): - abstract = True - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(abstract=True) created_date: datetime.datetime = ormar.DateTime( default=datetime.datetime.now, name="creation_date" @@ -499,10 +479,11 @@ class DateFieldsModel(ormar.Model): class Category(DateFieldsModel, AuditModel): - class Meta(ormar.ModelMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy( + tablename="categories", # set fields that should be skipped - exclude_parent_fields = ["updated_by", "updated_date"] + exclude_parent_fields=["updated_by", "updated_date"], + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50, unique=True, index=True) @@ -523,38 +504,32 @@ Note how you simply need to provide field names and it will exclude the parent f The same effect can be achieved by splitting base classes like: ```python -metadata = sa.MetaData() -db = databases.Database(DATABASE_URL) +base_ormar_config = OrmarConfig( + metadata=sa.MetaData(), + database=databases.Database(DATABASE_URL), +) class AuditCreateModel(ormar.Model): - class Meta: - abstract = True + ormar_config = base_ormar_config.copy(abstract=True) created_by: str = ormar.String(max_length=100) class AuditUpdateModel(ormar.Model): - class Meta: - abstract = True + ormar_config = base_ormar_config.copy(abstract=True) updated_by: str = ormar.String(max_length=100, default="Sam") class CreateDateFieldsModel(ormar.Model): - class Meta(ormar.ModelMeta): - abstract = True - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(abstract=True) created_date: datetime.datetime = ormar.DateTime( default=datetime.datetime.now, name="creation_date" ) class UpdateDateFieldsModel(ormar.Model): - class Meta(ormar.ModelMeta): - abstract = True - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(abstract=True) updated_date: datetime.datetime = ormar.DateTime( default=datetime.datetime.now, name="modification_date" @@ -562,8 +537,7 @@ class UpdateDateFieldsModel(ormar.Model): class Category(CreateDateFieldsModel, AuditCreateModel): - class Meta(ormar.ModelMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50, unique=True, index=True) diff --git a/docs/models/internals.md b/docs/models/internals.md index 463d368..358c319 100644 --- a/docs/models/internals.md +++ b/docs/models/internals.md @@ -20,27 +20,27 @@ For example to list pydantic model fields you can: ## Sqlalchemy Table -To access auto created sqlalchemy table you can use `Model.Meta.table` parameter +To access auto created sqlalchemy table you can use `Model.ormar_config.table` parameter For example to list table columns you can: -```Python hl_lines="20" +```Python hl_lines="24" --8<-- "../docs_src/models/docs004.py" ``` !!!tip - You can access table primary key name by `Course.Meta.pkname` + You can access table primary key name by `Course.ormar_config.pkname` !!!info For more options visit official [sqlalchemy-metadata][sqlalchemy-metadata] documentation. ## Fields Definition -To access ormar `Fields` you can use `Model.Meta.model_fields` parameter +To access ormar `Fields` you can use `Model.ormar_config.model_fields` parameter For example to list table model fields you can: -```Python hl_lines="20" +```Python hl_lines="22" --8<-- "../docs_src/models/docs005.py" ``` diff --git a/docs/models/methods.md b/docs/models/methods.md index c7afb6f..85f6e58 100644 --- a/docs/models/methods.md +++ b/docs/models/methods.md @@ -13,37 +13,37 @@ Available methods are described below. ## `pydantic` methods Note that each `ormar.Model` is also a `pydantic.BaseModel`, so all `pydantic` methods are also available on a model, -especially `dict()` and `json()` methods that can also accept `exclude`, `include` and other parameters. +especially `model_dump()` and `model_dump_json()` methods that can also accept `exclude`, `include` and other parameters. To read more check [pydantic][pydantic] documentation -## construct +## model_construct() -`construct` is a raw equivalent of `__init__` method used for construction of new instances. +`model_construct` is a raw equivalent of `__init__` method used for construction of new instances. -The difference is that `construct` skips validations, so it should be used when you know that data is correct and can be trusted. +The difference is that `model_construct` skips validations, so it should be used when you know that data is correct and can be trusted. The benefit of using construct is the speed of execution due to skipped validation. !!!note - Note that in contrast to `pydantic.construct` method - the `ormar` equivalent will also process the nested related models. + Note that in contrast to `pydantic.model_construct` method - the `ormar` equivalent will also process the nested related models. !!!warning - Bear in mind that due to skipped validation the `construct` method does not perform any conversions, checks etc. - So it's your responsibility to provide that data that is valid and can be consumed by the database. - - The only two things that construct still performs are: + Bear in mind that due to skipped validation the `construct` method does not perform any conversions, checks etc. + So it's your responsibility to provide that data that is valid and can be consumed by the database. + + The only two things that construct still performs are: - * Providing a `default` value for not set fields - * Initialize nested ormar models if you pass a dictionary or a primary key value + * Providing a `default` value for not set fields + * Initialize nested ormar models if you pass a dictionary or a primary key value -## dict +## model_dump() -`dict` is a method inherited from `pydantic`, yet `ormar` adds its own parameters and has some nuances when working with default values, +`model_dump` is a method inherited from `pydantic`, yet `ormar` adds its own parameters and has some nuances when working with default values, therefore it's listed here for clarity. -`dict` as the name suggests export data from model tree to dictionary. +`model_dump` as the name suggests export data from model tree to dictionary. -Explanation of dict parameters: +Explanation of model_dump parameters: ### include (`ormar` modified) @@ -55,14 +55,14 @@ Note that `pydantic` has an uncommon pattern of including/ excluding fields in l And if you want to exclude the field in all children you need to pass a `__all__` key to dictionary. You cannot exclude nested models in `Set`s in `pydantic` but you can in `ormar` -(by adding double underscore on relation name i.e. to exclude name of category for a book you cen use `exclude={"book__category__name"}`) +(by adding double underscore on relation name i.e. to exclude name of category for a book you can use `exclude={"book__category__name"}`) `ormar` does not support by index exclusion/ inclusions and accepts a simplified and more user-friendly notation. To check how you can include/exclude fields, including nested fields check out [fields](../queries/select-columns.md#fields) section that has an explanation and a lot of samples. !!!note - The fact that in `ormar` you can exclude nested models in sets, you can exclude from a whole model tree in `response_model_exclude` and `response_model_include` in fastapi! + The fact that in `ormar` you can exclude nested models in sets, you can exclude from a whole model tree in `response_model_exclude` and `response_model_include` in fastapi! ### exclude (`ormar` modified) @@ -81,7 +81,7 @@ You cannot exclude nested models in `Set`s in `pydantic` but you can in `ormar` To check how you can include/exclude fields, including nested fields check out [fields](../queries/select-columns.md#fields) section that has an explanation and a lot of samples. !!!note - The fact that in `ormar` you can exclude nested models in sets, you can exclude from a whole model tree in `response_model_exclude` and `response_model_include` in fastapi! + The fact that in `ormar` you can exclude nested models in sets, you can exclude from a whole model tree in `response_model_exclude` and `response_model_include` in fastapi! ### exclude_unset @@ -90,16 +90,13 @@ To check how you can include/exclude fields, including nested fields check out [ Flag indicates whether fields which were not explicitly set when creating the model should be excluded from the returned dictionary. !!!warning - Note that after you save data into database each field has its own value -> either provided by you, default, or `None`. - - That means that when you load the data from database, **all** fields are set, and this flag basically stop working! + Note that after you save data into database each field has its own value -> either provided by you, default, or `None`. + + That means that when you load the data from database, **all** fields are set, and this flag basically stop working! ```python class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, default="Test") @@ -107,10 +104,7 @@ class Category(ormar.Model): class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -118,17 +112,17 @@ class Item(ormar.Model): categories: List[Category] = ormar.ManyToMany(Category) category = Category(name="Test 2") -assert category.dict() == {'id': None, 'items': [], 'name': 'Test 2', +assert category.model_dump() == {'id': None, 'items': [], 'name': 'Test 2', 'visibility': True} -assert category.dict(exclude_unset=True) == {'items': [], 'name': 'Test 2'} +assert category.model_dump(exclude_unset=True) == {'items': [], 'name': 'Test 2'} await category.save() category2 = await Category.objects.get() -assert category2.dict() == {'id': 1, 'items': [], 'name': 'Test 2', +assert category2.model_dump() == {'id': 1, 'items': [], 'name': 'Test 2', 'visibility': True} # NOTE how after loading from db all fields are set explicitly # as this is what happens when you populate a model from db -assert category2.dict(exclude_unset=True) == {'id': 1, 'items': [], +assert category2.model_dump(exclude_unset=True) == {'id': 1, 'items': [], 'name': 'Test 2', 'visibility': True} ``` @@ -140,20 +134,14 @@ Flag indicates are equal to their default values (whether set or otherwise) shou ```python class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, default="Test") visibility: bool = ormar.Boolean(default=True) class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -162,15 +150,15 @@ class Item(ormar.Model): category = Category() # note that Integer pk is by default autoincrement so optional -assert category.dict() == {'id': None, 'items': [], 'name': 'Test', 'visibility': True} -assert category.dict(exclude_defaults=True) == {'items': []} +assert category.model_dump() == {'id': None, 'items': [], 'name': 'Test', 'visibility': True} +assert category.model_dump(exclude_defaults=True) == {'items': []} # save and reload the data await category.save() category2 = await Category.objects.get() -assert category2.dict() == {'id': 1, 'items': [], 'name': 'Test', 'visibility': True} -assert category2.dict(exclude_defaults=True) == {'id': 1, 'items': []} +assert category2.model_dump() == {'id': 1, 'items': [], 'name': 'Test', 'visibility': True} +assert category2.model_dump(exclude_defaults=True) == {'id': 1, 'items': []} ``` ### exclude_none @@ -181,10 +169,7 @@ Flag indicates whether fields which are equal to `None` should be excluded from ```python class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, default="Test", nullable=True) @@ -192,10 +177,7 @@ class Category(ormar.Model): class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -204,16 +186,16 @@ class Item(ormar.Model): category = Category(name=None) -assert category.dict() == {'id': None, 'items': [], 'name': None, +assert category.model_dump() == {'id': None, 'items': [], 'name': None, 'visibility': True} # note the id is not set yet so None and excluded -assert category.dict(exclude_none=True) == {'items': [], 'visibility': True} +assert category.model_dump(exclude_none=True) == {'items': [], 'visibility': True} await category.save() category2 = await Category.objects.get() -assert category2.dict() == {'id': 1, 'items': [], 'name': None, +assert category2.model_dump() == {'id': 1, 'items': [], 'name': None, 'visibility': True} -assert category2.dict(exclude_none=True) == {'id': 1, 'items': [], +assert category2.model_dump(exclude_none=True) == {'id': 1, 'items': [], 'visibility': True} ``` @@ -226,17 +208,14 @@ Setting flag to `True` will exclude all primary key columns in a tree, including ```python class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) item1 = Item(id=1, name="Test Item") -assert item1.dict() == {"id": 1, "name": "Test Item"} -assert item1.dict(exclude_primary_keys=True) == {"name": "Test Item"} +assert item1.model_dump() == {"id": 1, "name": "Test Item"} +assert item1.model_dump(exclude_primary_keys=True) == {"name": "Test Item"} ``` ### exclude_through_models (`ormar` only) @@ -249,20 +228,14 @@ Setting the `exclude_through_models=True` will exclude all through models, inclu ```python class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -280,7 +253,7 @@ await Item(**item_dict).save_related(follow=True, save_all=True) item = await Item.objects.select_related("categories").get() # by default you can see the through models (itemcategory) -assert item.dict() == {'id': 1, 'name': 'test', +assert item.model_dump() == {'id': 1, 'name': 'test', 'categories': [ {'id': 1, 'name': 'test cat', 'itemcategory': {'id': 1, 'category': None, 'item': None}}, @@ -289,7 +262,7 @@ assert item.dict() == {'id': 1, 'name': 'test', ]} # you can exclude those fields/ models -assert item.dict(exclude_through_models=True) == { +assert item.model_dump(exclude_through_models=True) == { 'id': 1, 'name': 'test', 'categories': [ {'id': 1, 'name': 'test cat'}, @@ -297,49 +270,45 @@ assert item.dict(exclude_through_models=True) == { ]} ``` -## json +## model_dump_json() -`json()` has exactly the same parameters as `dict()` so check above. +`model_dump_json()` has exactly the same parameters as `model_dump()` so check above. Of course the end result is a string with json representation and not a dictionary. -## get_pydantic +## get_pydantic() `get_pydantic(include: Union[Set, Dict] = None, exclude: Union[Set, Dict] = None)` This method allows you to generate `pydantic` models from your ormar models without you needing to retype all the fields. -Note that if you have nested models, it **will generate whole tree of pydantic models for you!** +Note that if you have nested models, it **will generate whole tree of pydantic models for you!** but in a way that prevents cyclic references issues. Moreover, you can pass `exclude` and/or `include` parameters to keep only the fields that you want to, including in nested models. That means that this way you can effortlessly create pydantic models for requests and responses in `fastapi`. !!!Note - To read more about possible excludes/includes and how to structure your exclude dictionary or set visit [fields](../queries/select-columns.md#fields) section of documentation + To read more about possible excludes/includes and how to structure your exclude dictionary or set visit [fields](../queries/select-columns.md#fields) section of documentation Given sample ormar models like follows: ```python -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) +base_ormar_config = ormar.OrmarConfig( + metadata=sqlalchemy.MetaData(), + database=databases.Database(DATABASE_URL, force_rollback=True), +) -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database - class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Item(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, default="test") @@ -361,8 +330,8 @@ class Category(BaseModel): ``` !!!warning - Note that it's not a good practice to have several classes with same name in one module, as well as it would break `fastapi` docs. - Thats's why ormar adds random 3 uppercase letters to the class name. In example above it means that in reality class would be named i.e. `Category_XIP(BaseModel)`. + Note that it's not a good practice to have several classes with same name in one module, as well as it would break `fastapi` docs. + Thats's why ormar adds random 3 uppercase letters to the class name. In example above it means that in reality class would be named i.e. `Category_XIP(BaseModel)`. To exclude or include nested fields you can use dict or double underscores. @@ -382,19 +351,19 @@ class Category(BaseModel): items: Optional[List[Item]] ``` -Of course, you can use also deeply nested structures and ormar will generate it pydantic equivalent you (in a way that exclude loops). +Of course, you can use also deeply nested structures and ormar will generate it's pydantic equivalent for you (in a way that exclude loops). Note how `Item` model above does not have a reference to `Category` although in ormar the relation is bidirectional (and `ormar.Item` has `categories` field). !!!warning - Note that the generated pydantic model will inherit all **field** validators from the original `ormar` model, that includes the ormar choices validator as well as validators defined with `pydantic.validator` decorator. - - But, at the same time all root validators present on `ormar` models will **NOT** be copied to the generated pydantic model. Since root validator can operate on all fields and a user can exclude some fields during generation of pydantic model it's not safe to copy those validators. - If required, you need to redefine/ manually copy them to generated pydantic model. + Note that the generated pydantic model will inherit all **field** validators from the original `ormar` model, that includes the ormar choices validator as well as validators defined with `pydantic.validator` decorator. + + But, at the same time all root validators present on `ormar` models will **NOT** be copied to the generated pydantic model. Since root validator can operate on all fields and a user can exclude some fields during generation of pydantic model it's not safe to copy those validators. + If required, you need to redefine/ manually copy them to generated pydantic model. -## load +## load() -By default when you query a table without prefetching related models, the ormar will still construct +By default, when you query a table without prefetching related models, the ormar will still construct your related models, but populate them only with the pk value. You can load the related model by calling `load()` method. `load()` can also be used to refresh the model from the database (if it was changed by some other process). @@ -409,14 +378,14 @@ await track.album.load() track.album.name # will return 'Malibu' ``` -## load_all +## load_all() `load_all(follow: bool = False, exclude: Union[List, str, Set, Dict] = None) -> Model` Method works like `load()` but also goes through all relations of the `Model` on which the method is called, and reloads them from database. -By default the `load_all` method loads only models that are directly related (one step away) to the model on which the method is called. +By default, the `load_all` method loads only models that are directly related (one step away) to the model on which the method is called. But you can specify the `follow=True` parameter to traverse through nested models and load all of them in the relation tree. @@ -442,7 +411,7 @@ Method performs one database query so it's more efficient than nested calls to ` !!!warning All relations are cleared on `load_all()`, so if you exclude some nested models they will be empty after call. -## save +## save() `save() -> self` @@ -461,7 +430,7 @@ track = await Track.objects.get(name='The Bird') await track.save() # will raise integrity error as pk is populated ``` -## update +## update() `update(_columns: List[str] = None, **kwargs) -> self` @@ -480,12 +449,9 @@ To update only selected columns from model into the database provide a list of c In example: -```python +```python hl_lines="16" class Movie(ormar.Model): - class Meta: - tablename = "movies" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="title") @@ -510,9 +476,9 @@ assert terminator.year == 1984 ``` !!!warning - Note that `update()` does not refresh the instance of the Model, so if you change more columns than you pass in `_columns` list your Model instance will have different values than the database! + Note that `update()` does not refresh the instance of the Model, so if you change more columns than you pass in `_columns` list your Model instance will have different values than the database! -## upsert +## upsert() `upsert(**kwargs) -> self` @@ -531,7 +497,7 @@ await track.upsert(name='The Bird Strikes Again') # will call update as pk is al ``` -## delete +## delete() You can delete models by using `QuerySet.delete()` method or by using your model and calling `delete()` method. @@ -543,7 +509,7 @@ await track.delete() # will delete the model from database !!!tip Note that that `track` object stays the same, only record in the database is removed. -## save_related +## save_related() `save_related(follow: bool = False, save_all: bool = False, exclude=Optional[Union[Set, Dict]]) -> None` @@ -566,11 +532,11 @@ If you want to skip saving some of the relations you can pass `exclude` paramete or it can be a dictionary that can also contain nested items. !!!note - Note that `exclude` parameter in `save_related` accepts only relation fields names, so - if you pass any other fields they will be saved anyway + Note that `exclude` parameter in `save_related` accepts only relation fields names, so + if you pass any other fields they will be saved anyway !!!note - To read more about the structure of possible values passed to `exclude` check `Queryset.fields` method documentation. + To read more about the structure of possible values passed to `exclude` check `Queryset.fields` method documentation. !!!warning To avoid circular updates with `follow=True` set, `save_related` keeps a set of already visited Models on each branch of relation tree, @@ -584,18 +550,14 @@ Example: ```python class Department(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) department_name: str = ormar.String(max_length=100) class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) course_name: str = ormar.String(max_length=100) @@ -604,9 +566,7 @@ class Course(ormar.Model): class Student(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -647,7 +607,7 @@ to_exclude = { } # after excluding ids and through models you get exact same payload used to # construct whole tree -assert department_check.dict(exclude=to_exclude) == to_save +assert department_check.model_dump(exclude=to_exclude) == to_save ``` diff --git a/docs/models/migrations.md b/docs/models/migrations.md index 6841014..7577a68 100644 --- a/docs/models/migrations.md +++ b/docs/models/migrations.md @@ -16,14 +16,14 @@ engine = sqlalchemy.create_engine("sqlite:///test.db") metadata.create_all(engine) ``` -You can also create single tables, sqlalchemy tables are exposed in `ormar.Meta` class. +You can also create single tables, sqlalchemy tables are exposed in `ormar.ormar_config` object. ```python import sqlalchemy # get your database url in sqlalchemy format - same as used with databases instance used in Model definition engine = sqlalchemy.create_engine("sqlite:///test.db") # Artist is an ormar model from previous examples -Artist.Meta.table.create(engine) +Artist.ormar_config.table.create(engine) ``` !!!warning diff --git a/docs/mypy.md b/docs/mypy.md index 4066e3d..d1f13f0 100644 --- a/docs/mypy.md +++ b/docs/mypy.md @@ -1,14 +1,6 @@ To provide better errors check you should use mypy with pydantic [plugin][plugin] -Note that legacy model declaration type will raise static type analyzers errors. - -So you **cannot use the old notation** like this: - -```Python hl_lines="15-17" ---8<-- "../docs_src/models/docs011.py" -``` - -Instead switch to notation introduced in version 0.4.0. +Please use notation introduced in version 0.4.0. ```Python hl_lines="15-17" --8<-- "../docs_src/models/docs012.py" diff --git a/docs/queries/aggregations.md b/docs/queries/aggregations.md index 7a2f9e8..74b8ec0 100644 --- a/docs/queries/aggregations.md +++ b/docs/queries/aggregations.md @@ -32,10 +32,11 @@ the count will be the total number of rows returned ```python class Book(ormar.Model): - class Meta: - tablename = "books" - metadata = metadata - database = database + ormar_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), + tablename="book" + ) id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -60,10 +61,11 @@ Returns a bool value to confirm if there are rows matching the given criteria (a ```python class Book(ormar.Model): - class Meta: - tablename = "books" - metadata = metadata - database = database + ormar_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), + tablename="book" + ) id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) diff --git a/docs/queries/create.md b/docs/queries/create.md index 659e2e2..e0dbce2 100644 --- a/docs/queries/create.md +++ b/docs/queries/create.md @@ -30,10 +30,12 @@ The allowed kwargs are `Model` fields names and proper value types. ```python class Album(ormar.Model): - class Meta: - tablename = "album" - metadata = metadata - database = database + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="album" + ) + id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -52,7 +54,7 @@ await malibu.save() ``` !!!tip - Check other `Model` methods in [models][models] + Check other `Model` methods in [models][models] ## get_or_create @@ -68,10 +70,11 @@ i.e. `get_or_create(_defaults: {"title": "I win"}, title="never used")` will alw ```python class Album(ormar.Model): - class Meta: - tablename = "album" - metadata = metadata - database = database + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="album" + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -106,7 +109,7 @@ assert album == album2 Updates the model, or in case there is no match in database creates a new one. -```Python hl_lines="26-32" +```Python hl_lines="40-48" --8<-- "../docs_src/queries/docs003.py" ``` @@ -122,7 +125,7 @@ Allows you to create multiple objects at once. A valid list of `Model` objects needs to be passed. -```python hl_lines="21-27" +```python hl_lines="26-32" --8<-- "../docs_src/queries/docs004.py" ``` diff --git a/docs/queries/delete.md b/docs/queries/delete.md index aec5171..01f2c93 100644 --- a/docs/queries/delete.md +++ b/docs/queries/delete.md @@ -26,7 +26,7 @@ If you do not provide this flag or a filter a `QueryDefinitionError` will be rai Return number of rows deleted. -```python hl_lines="26-30" +```python hl_lines="40-44" --8<-- "../docs_src/queries/docs005.py" ``` @@ -59,20 +59,14 @@ If you specify the keep_reversed flag to `False` `ormar` will also delete the re ```python class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) is_best_seller: bool = ormar.Boolean(default=False) class Track(ormar.Model): - class Meta: - tablename = "tracks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -104,20 +98,14 @@ If you specify the keep_reversed flag to `False` `ormar` will also delete the re ```python class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) is_best_seller: bool = ormar.Boolean(default=False) class Track(ormar.Model): - class Meta: - tablename = "tracks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -148,4 +136,4 @@ await album.tracks.clear() await album.tracks.clear(keep_reversed=False) ``` -[querysetproxy]: ../relations/queryset-proxy.md \ No newline at end of file +[querysetproxy]: ../relations/queryset-proxy.md diff --git a/docs/queries/filter-and-sort.md b/docs/queries/filter-and-sort.md index 7d9637f..72a4a1d 100644 --- a/docs/queries/filter-and-sort.md +++ b/docs/queries/filter-and-sort.md @@ -35,20 +35,14 @@ a filter across an FK relationship. ```python class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) is_best_seller: bool = ormar.Boolean(default=False) class Track(ormar.Model): - class Meta: - tablename = "tracks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -197,20 +191,14 @@ conditions. ```python class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) is_best_seller: bool = ormar.Boolean(default=False) class Track(ormar.Model): - class Meta: - tablename = "tracks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -245,26 +233,21 @@ Since it sounds more complicated than it is, let's look at some examples. Given a sample models like this: ```python -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey(Author) @@ -721,7 +704,7 @@ Ordering in sql will be applied in order of names you provide in order_by. Given sample Models like following: ```python ---8 < -- "../../docs_src/queries/docs007.py" +--8<-- "../docs_src/queries/docs007.py" ``` To order by main model field just provide a field name @@ -808,7 +791,7 @@ Since order of rows in a database is not guaranteed, `ormar` **always** issues a When querying the database with given model by default the `Model` is ordered by the `primary_key` column ascending. If you wish to change the default behaviour you can do it by providing `orders_by` -parameter to model `Meta` class. +parameter to `ormar_config`. !!!tip To read more about models sort order visit [models](../models/index.md#model-sort-order) section of documentation @@ -823,8 +806,8 @@ Order in which order_by clauses are applied is as follows: * Explicitly passed `order_by()` calls in query * Relation passed `orders_by` and `related_orders_by` if exists - * Model `Meta` class `orders_by` - * Model `primary_key` column ascending (fallback, used if none of above provided) + * Model's `ormar_config` object `orders_by` + * Model's `primary_key` column ascending (fallback, used if none of above provided) **Order from only one source is applied to each `Model` (so that you can always overwrite it in a single query).** diff --git a/docs/queries/joins-and-subqueries.md b/docs/queries/joins-and-subqueries.md index cd840e8..85a092e 100644 --- a/docs/queries/joins-and-subqueries.md +++ b/docs/queries/joins-and-subqueries.md @@ -46,20 +46,14 @@ To chain related `Models` relation use double underscores between names. ```python class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) is_best_seller: bool = ormar.Boolean(default=False) class Track(ormar.Model): - class Meta: - tablename = "tracks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -82,10 +76,7 @@ You can provide a string or a list of strings (or a field/ list of fields) ```python class SchoolClass(ormar.Model): - class Meta: - tablename = "schoolclasses" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="schoolclasses") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -93,20 +84,14 @@ class SchoolClass(ormar.Model): class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Student(ormar.Model): - class Meta: - tablename = "students" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -115,10 +100,7 @@ class Student(ormar.Model): class Teacher(ormar.Model): - class Meta: - tablename = "teachers" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -182,18 +164,14 @@ If `follow=True` is set it adds also related models of related models. With sample date like follow: ```python -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = OrmarConfig( + database=databases.Database(DATABASE_URL, force_rollback=True), + metadata=sqlalchemy.MetaData(), +) class Address(ormar.Model): - class Meta(BaseMeta): - tablename = "addresses" + ormar_config = base_ormar_config.copy(tablename="addresses") id: int = ormar.Integer(primary_key=True) street: str = ormar.String(max_length=100, nullable=False) @@ -203,8 +181,7 @@ class Address(ormar.Model): class Branch(ormar.Model): - class Meta(BaseMeta): - tablename = "branches" + ormar_config = base_ormar_config.copy(tablename="branches") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False) @@ -212,8 +189,7 @@ class Branch(ormar.Model): class Company(ormar.Model): - class Meta(BaseMeta): - tablename = "companies" + ormar_config = base_ormar_config.copy(tablename="companies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="company_name") @@ -264,20 +240,14 @@ To chain related `Models` relation use double underscores between names. ```python class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) is_best_seller: bool = ormar.Boolean(default=False) class Track(ormar.Model): - class Meta: - tablename = "tracks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -301,10 +271,7 @@ You can provide a string, or a list of strings ```python class SchoolClass(ormar.Model): - class Meta: - tablename = "schoolclasses" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="schoolclasses") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -312,20 +279,14 @@ class SchoolClass(ormar.Model): class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Student(ormar.Model): - class Meta: - tablename = "students" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -334,10 +295,7 @@ class Student(ormar.Model): class Teacher(ormar.Model): - class Meta: - tablename = "teachers" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -432,12 +390,12 @@ might be faster despite it needs to perform three separate queries instead of on #### Memory -`ormar` is a mini ORM meaning that it does not keep a registry of already loaded models. +`ormar` is does not keep a registry of already loaded models. That means that in `select_related` example above you will always have 10 000 Models A, 30 000 Models B -(even if the unique number of rows in db is 3 - processing of `select_related` spawns ** -new** child models for each parent model). And 60 000 Models C. +(even if the unique number of rows in db is 3 - processing of `select_related` spawns +**new** child models for each parent model). And 60 000 Models C. If the same Model B is shared by rows 1, 10, 100 etc. and you update one of those, the rest of rows that share the same child will **not** be updated on the spot. If you @@ -471,7 +429,7 @@ that `select_related` will use more memory as each child is instantiated as a ne ```python # will return False (note that id is a python `builtin` function not ormar one). - id(row1.child1) == (ro100.child1) + id(row1.child1) == id(ro100.child1) # from above - will also return False id(model1) == id(model2) diff --git a/docs/queries/pagination-and-rows-number.md b/docs/queries/pagination-and-rows-number.md index 21f4191..be65840 100644 --- a/docs/queries/pagination-and-rows-number.md +++ b/docs/queries/pagination-and-rows-number.md @@ -22,10 +22,11 @@ Combines the `offset` and `limit` methods based on page number and size ```python class Track(ormar.Model): - class Meta: - tablename = "track" - metadata = metadata - database = database + ormar_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), + tablename="track" + ) id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -52,10 +53,11 @@ use the `limit_raw_sql` parameter flag, and set it to `True`. ```python class Track(ormar.Model): - class Meta: - tablename = "track" - metadata = metadata - database = database + ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), + tablename="track" + ) id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -86,10 +88,11 @@ use the `limit_raw_sql` parameter flag, and set it to `True`. ```python class Track(ormar.Model): - class Meta: - tablename = "track" - metadata = metadata - database = database + ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), + tablename="track" + ) id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) diff --git a/docs/queries/raw-data.md b/docs/queries/raw-data.md index 57e0273..00941fc 100644 --- a/docs/queries/raw-data.md +++ b/docs/queries/raw-data.md @@ -40,8 +40,7 @@ Example: # declared models class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) @@ -49,8 +48,7 @@ class Category(ormar.Model): class Post(ormar.Model): - class Meta(BaseMeta): - tablename = "posts" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200) @@ -83,16 +81,14 @@ Note how nested models columns will be prefixed with full relation path coming f # declare models class User(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Role(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -216,8 +212,7 @@ Example: # declared models class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) @@ -225,8 +220,7 @@ class Category(ormar.Model): class Post(ormar.Model): - class Meta(BaseMeta): - tablename = "posts" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200) @@ -257,8 +251,7 @@ Let's complicate the relation and modify the previously mentioned Category model ```python class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) diff --git a/docs/queries/read.md b/docs/queries/read.md index 018d693..e5d7a9f 100644 --- a/docs/queries/read.md +++ b/docs/queries/read.md @@ -31,10 +31,11 @@ Passing a criteria is actually calling filter(*args, **kwargs) method described ```python class Track(ormar.Model): - class Meta: - tablename = "track" - metadata = metadata - database = database + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="track" + ) id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -74,10 +75,7 @@ a new one with given kwargs and _defaults. ```python class Album(ormar.Model): - class Meta: - tablename = "album" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="album") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -114,10 +112,7 @@ Gets the first row from the db ordered by primary key column ascending. ```python class Album(ormar.Model): - class Meta: - tablename = "album" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="album") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -143,20 +138,14 @@ If there are no rows meeting the criteria an empty list is returned. ```python class Album(ormar.Model): - class Meta: - tablename = "album" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="album") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Track(ormar.Model): - class Meta: - tablename = "track" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="track") id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -186,10 +175,7 @@ If there are no rows meeting the criteria an empty async generator is returned. ```python class Album(ormar.Model): - class Meta: - tablename = "album" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="album") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) diff --git a/docs/queries/select-columns.md b/docs/queries/select-columns.md index d5c6c76..206cc7e 100644 --- a/docs/queries/select-columns.md +++ b/docs/queries/select-columns.md @@ -24,52 +24,7 @@ With `fields()` you can select subset of model columns to limit the data load. Given a sample data like following: ```python -import databases -import sqlalchemy - -import ormar -from tests.settings import DATABASE_URL - -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() - - -class Company(ormar.Model): - class Meta: - tablename = "companies" - metadata = metadata - database = database - - id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=100) - founded: int = ormar.Integer(nullable=True) - - -class Car(ormar.Model): - class Meta: - tablename = "cars" - metadata = metadata - database = database - - id: int = ormar.Integer(primary_key=True) - manufacturer = ormar.ForeignKey(Company) - name: str = ormar.String(max_length=100) - year: int = ormar.Integer(nullable=True) - gearbox_type: str = ormar.String(max_length=20, nullable=True) - gears: int = ormar.Integer(nullable=True) - aircon_type: str = ormar.String(max_length=20, nullable=True) - - -# build some sample data -toyota = await Company.objects.create(name="Toyota", founded=1937) -await Car.objects.create(manufacturer=toyota, name="Corolla", year=2020, gearbox_type='Manual', gears=5, - aircon_type='Manual') -await Car.objects.create(manufacturer=toyota, name="Yaris", year=2019, gearbox_type='Manual', gears=5, - aircon_type='Manual') -await Car.objects.create(manufacturer=toyota, name="Supreme", year=2020, gearbox_type='Auto', gears=6, - aircon_type='Auto') - - +--8<-- "../docs_src/select_columns/docs001.py" ``` You can select specified fields by passing a `str, List[str], Set[str] or dict` with @@ -78,8 +33,13 @@ nested definition. To include related models use notation `{related_name}__{column}[__{optional_next} etc.]`. -```python hl_lines="1" -all_cars = await Car.objects.select_related('manufacturer').fields(['id', 'name', 'manufacturer__name']).all() +```python hl_lines="1-6" +all_cars = await ( + Car.objects + .select_related('manufacturer') + .fields(['id', 'name', 'manufacturer__name']) + .all() +) for car in all_cars: # excluded columns will yield None assert all(getattr(car, x) is None for x in ['year', 'gearbox_type', 'gears', 'aircon_type']) @@ -97,9 +57,14 @@ for those models in fields - implies a list of all fields for those nested models. -```python hl_lines="1" -all_cars = await Car.objects.select_related('manufacturer').fields('id').fields( - ['name']).all() +```python hl_lines="1-7" +all_cars = await ( + Car.objects + .select_related('manufacturer') + .fields('id') + .fields(['name']) + .all() +) # all fields from company model are selected assert all_cars[0].manufacturer.name == 'Toyota' assert all_cars[0].manufacturer.founded == 1937 @@ -115,8 +80,12 @@ assert all_cars[0].manufacturer.founded == 1937 You cannot exclude mandatory model columns - `manufacturer__name` in this example. ```python -await Car.objects.select_related('manufacturer').fields( - ['id', 'name', 'manufacturer__founded']).all() +await ( + Car.objects + .select_related('manufacturer') + .fields(['id', 'name', 'manufacturer__founded']) + .all() +) # will raise pydantic ValidationError as company.name is required ``` @@ -138,38 +107,71 @@ Below you can see examples that are equivalent: ```python # 1. like in example above -await Car.objects.select_related('manufacturer').fields(['id', 'name', 'manufacturer__name']).all() +await ( + Car.objects + .select_related('manufacturer') + .fields(['id', 'name', 'manufacturer__name']) + .all() +) # 2. to mark a field as required use ellipsis -await Car.objects.select_related('manufacturer').fields({'id': ..., - 'name': ..., - 'manufacturer': { - 'name': ...} - }).all() +await ( + Car.objects + .select_related('manufacturer') + .fields({'id': ..., + 'name': ..., + 'manufacturer': { + 'name': ... + } + }) + .all() +) # 3. to include whole nested model use ellipsis -await Car.objects.select_related('manufacturer').fields({'id': ..., - 'name': ..., - 'manufacturer': ... - }).all() +await ( + Car.objects + .select_related('manufacturer') + .fields({'id': ..., + 'name': ..., + 'manufacturer': ... + }) + .all() +) -# 4. to specify fields at last nesting level you can also use set - equivalent to 2. above -await Car.objects.select_related('manufacturer').fields({'id': ..., - 'name': ..., - 'manufacturer': {'name'} - }).all() +# 4. to specify fields at last nesting level +# you can also use set - equivalent to 2. above +await ( + Car.objects + .select_related('manufacturer') + .fields({'id': ..., + 'name': ..., + 'manufacturer': {'name'} + }) + .all() +) # 5. of course set can have multiple fields -await Car.objects.select_related('manufacturer').fields({'id': ..., - 'name': ..., - 'manufacturer': {'name', 'founded'} - }).all() +await ( + Car.objects + .select_related('manufacturer') + .fields({'id': ..., + 'name': ..., + 'manufacturer': {'name', 'founded'} + }) + .all() +) -# 6. you can include all nested fields but it will be equivalent of 3. above which is shorter -await Car.objects.select_related('manufacturer').fields({'id': ..., - 'name': ..., - 'manufacturer': {'id', 'name', 'founded'} - }).all() +# 6. you can include all nested fields, +# but it will be equivalent of 3. above which is shorter +await ( + Car.objects + .select_related('manufacturer') + .fields({'id': ..., + 'name': ..., + 'manufacturer': {'id', 'name', 'founded'} + }) + .all() +) ``` @@ -201,74 +203,65 @@ exclude fields from whole hierarchy. Below you can find few simple examples: -```python hl_lines="47 48 60 61 67" -import databases -import sqlalchemy +```python +--8<-- "../docs_src/select_columns/docs001.py" +``` -import ormar -from tests.settings import DATABASE_URL - -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() - - -class Company(ormar.Model): - class Meta: - tablename = "companies" - metadata = metadata - database = database - - id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=100) - founded: int = ormar.Integer(nullable=True) - - -class Car(ormar.Model): - class Meta: - tablename = "cars" - metadata = metadata - database = database - - id: int = ormar.Integer(primary_key=True) - manufacturer = ormar.ForeignKey(Company) - name: str = ormar.String(max_length=100) - year: int = ormar.Integer(nullable=True) - gearbox_type: str = ormar.String(max_length=20, nullable=True) - gears: int = ormar.Integer(nullable=True) - aircon_type: str = ormar.String(max_length=20, nullable=True) - - -# build some sample data -toyota = await Company.objects.create(name="Toyota", founded=1937) -await Car.objects.create(manufacturer=toyota, name="Corolla", year=2020, gearbox_type='Manual', gears=5, - aircon_type='Manual') -await Car.objects.create(manufacturer=toyota, name="Yaris", year=2019, gearbox_type='Manual', gears=5, - aircon_type='Manual') -await Car.objects.create(manufacturer=toyota, name="Supreme", year=2020, gearbox_type='Auto', gears=6, - aircon_type='Auto') - -# select manufacturer but only name - to include related models use notation {model_name}__{column} -all_cars = await Car.objects.select_related('manufacturer').exclude_fields( - ['year', 'gearbox_type', 'gears', 'aircon_type', 'company__founded']).all() +```python +# select manufacturer but only name, +# to include related models use notation {model_name}__{column} +all_cars = await ( + Car.objects + .select_related('manufacturer') + .exclude_fields([ + 'year', + 'gearbox_type', + 'gears', + 'aircon_type', + 'company__founded' + ]) + .all() +) for car in all_cars: # excluded columns will yield None - assert all(getattr(car, x) is None for x in ['year', 'gearbox_type', 'gears', 'aircon_type']) - # included column on related models will be available, pk column is always included + assert all(getattr(car, x) is None + for x in [ + 'year', + 'gearbox_type', + 'gears', + 'aircon_type' + ]) + # included column on related models will be available, + # pk column is always included # even if you do not include it in fields list assert car.manufacturer.name == 'Toyota' - # also in the nested related models - you cannot exclude pk - it's always auto added + # also in the nested related models, + # you cannot exclude pk - it's always auto added assert car.manufacturer.founded is None -# fields() can be called several times, building up the columns to select -# models selected in select_related but with no columns in fields list implies all fields -all_cars = await Car.objects.select_related('manufacturer').exclude_fields('year').exclude_fields( - ['gear', 'gearbox_type']).all() +# fields() can be called several times, +# building up the columns to select +# models included in select_related +# but with no columns in fields list implies all fields +all_cars = await ( + Car.objects + .select_related('manufacturer') + .exclude_fields('year') + .exclude_fields(['gear', 'gearbox_type']) + .all() +) # all fields from company model are selected assert all_cars[0].manufacturer.name == 'Toyota' assert all_cars[0].manufacturer.founded == 1937 -# cannot exclude mandatory model columns - company__name in this example - note usage of dict/set this time -await Car.objects.select_related('manufacturer').exclude_fields([{'company': {'name'}}]).all() +# cannot exclude mandatory model columns, +# company__name in this example - note usage of dict/set this time +await ( + Car.objects + .select_related('manufacturer') + .exclude_fields([{'company': {'name'}}]) + .all() +) # will raise pydantic ValidationError as company.name is required ``` diff --git a/docs/queries/update.md b/docs/queries/update.md index 642f044..f9f00e9 100644 --- a/docs/queries/update.md +++ b/docs/queries/update.md @@ -29,7 +29,7 @@ If you do not provide this flag or a filter a `QueryDefinitionError` will be rai Return number of rows updated. -```Python hl_lines="26-28" +```Python hl_lines="42-44" --8<-- "../docs_src/queries/docs002.py" ``` @@ -44,7 +44,7 @@ Return number of rows updated. Updates the model, or in case there is no match in database creates a new one. -```Python hl_lines="26-32" +```Python hl_lines="40-48" --8<-- "../docs_src/queries/docs003.py" ``` @@ -123,4 +123,4 @@ from other side of the relation. [querysetproxy]: ../relations/queryset-proxy.md [models-upsert]: ../models/methods.md#upsert -[models-save-related]: ../models/methods.md#save_related \ No newline at end of file +[models-save-related]: ../models/methods.md#save_related diff --git a/docs/relations/foreign-key.md b/docs/relations/foreign-key.md index 6cd0536..a511141 100644 --- a/docs/relations/foreign-key.md +++ b/docs/relations/foreign-key.md @@ -14,7 +14,7 @@ Sqlalchemy column and Type are automatically taken from target `Model`. To define a relation add `ForeignKey` field that points to related `Model`. -```Python hl_lines="29" +```Python hl_lines="30" --8<-- "../docs_src/fields/docs003.py" ``` @@ -24,7 +24,7 @@ To define a relation add `ForeignKey` field that points to related `Model`. By default it's child (source) `Model` name + s, like courses in snippet below: -```Python hl_lines="29 35" +```Python hl_lines="29 36" --8<-- "../docs_src/fields/docs001.py" ``` @@ -45,15 +45,14 @@ But you cannot: * Access the related field from reverse model with `related_name` * Even if you `select_related` from reverse side of the model the returned models won't be populated in reversed instance (the join is not prevented so you still can `filter` and `order_by` over the relation) -* The relation won't be populated in `dict()` and `json()` +* The relation won't be populated in `model_dump()` and `model_dump_json()` * You cannot pass the nested related objects when populating from dictionary or json (also through `fastapi`). It will be either ignored or error will be raised depending on `extra` setting in pydantic `Config`. Example: ```python class Author(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) first_name: str = ormar.String(max_length=80) @@ -61,8 +60,7 @@ class Author(ormar.Model): class Post(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -82,8 +80,8 @@ authors = ( assert authors[0].first_name == "Test" # note that posts are not populated for author even if explicitly -# included in select_related - note no posts in dict() -assert author.dict(exclude={"id"}) == {"first_name": "Test", "last_name": "Author"} +# included in select_related - note no posts in model_dump() +assert author.model_dump(exclude={"id"}) == {"first_name": "Test", "last_name": "Author"} # still can filter through fields of related model authors = await Author.objects.filter(posts__title="Test Post").all() @@ -112,7 +110,7 @@ assert department.courses[0] == course !!!warning If you want to add child model on related model the primary key value for parent model **has to exist in database**. - Otherwise ormar will raise RelationshipInstanceError as it cannot set child's ForeignKey column value + Otherwise ormar will raise `RelationshipInstanceError` as it cannot set child's ForeignKey column value if parent model has no primary key value. That means that in example above the department has to be saved before you can call `department.courses.add()`. @@ -151,7 +149,7 @@ await department.courses.remove(course, keep_reversed=False) Removal of all related models in one call. -Like remove by default `clear()` nulls the ForeigKey column on child model (all, not matter if they are loaded or not). +Like with remove, by default, `clear()` nulls the ForeigKey column on child model (all, not matter if they are loaded or not). ```python # nulls department column on all courses related to this department @@ -173,9 +171,9 @@ To read which methods of QuerySet are available read below [querysetproxy][query ## related_name -But you can overwrite this name by providing `related_name` parameter like below: +You can overwrite related model field name by providing `related_name` parameter like below: -```Python hl_lines="29 35" +```Python hl_lines="27-29 35" --8<-- "../docs_src/fields/docs002.py" ``` @@ -230,7 +228,7 @@ You have several ways to set-up a relationship connection. The most obvious one is to pass a related `Model` instance to the constructor. -```Python hl_lines="34-35" +```Python hl_lines="35-36" --8<-- "../docs_src/relations/docs001.py" ``` @@ -238,7 +236,7 @@ The most obvious one is to pass a related `Model` instance to the constructor. You can setup the relation also with just the pk column value of the related model. -```Python hl_lines="37-38" +```Python hl_lines="38-39" --8<-- "../docs_src/relations/docs001.py" ``` @@ -246,9 +244,9 @@ You can setup the relation also with just the pk column value of the related mod Next option is with a dictionary of key-values of the related model. -You can build the dictionary yourself or get it from existing model with `dict()` method. +You can build the dictionary yourself or get it from existing model with `model_dump()` method. -```Python hl_lines="40-41" +```Python hl_lines="41-42" --8<-- "../docs_src/relations/docs001.py" ``` @@ -256,7 +254,7 @@ You can build the dictionary yourself or get it from existing model with `dict() Finally you can explicitly set it to None (default behavior if no value passed). -```Python hl_lines="43-44" +```Python hl_lines="44-45" --8<-- "../docs_src/relations/docs001.py" ``` @@ -283,4 +281,4 @@ Finally you can explicitly set it to None (default behavior if no value passed). [fields]: ./queries.md#fields [exclude_fields]: ./queries.md#exclude_fields [order_by]: ./queries.md#order_by -[server_default]: ../fields/common-parameters.md#server-default \ No newline at end of file +[server_default]: ../fields/common-parameters.md#server-default diff --git a/docs/relations/index.md b/docs/relations/index.md index 385516a..a864bd2 100644 --- a/docs/relations/index.md +++ b/docs/relations/index.md @@ -13,7 +13,7 @@ To read more about methods, possibilities, definition etc. please read the subse To define many-to-one relation use `ForeignKey` field. -```Python hl_lines="17" +```Python hl_lines="26" --8<-- "../docs_src/relations/docs003.py" ``` @@ -24,13 +24,11 @@ To define many-to-one relation use `ForeignKey` field. The definition of one-to-many relation also uses `ForeignKey`, and it's registered for you automatically. -So in relation ato example above. +So in relation to example above. -```Python hl_lines="17" +```Python hl_lines="7-8" class Department(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -52,21 +50,22 @@ class Department(ormar.Model): To define many-to-many relation use `ManyToMany` field. -```python hl_lines="18" +```python hl_lines="19" class Category(ormar.Model): - class Meta: - tablename = "categories" - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="categories", + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) class Post(ormar.Model): - class Meta: - tablename = "posts" - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -92,18 +91,24 @@ side of the current query for m2m models. So if you query from model `A` to model `B`, only model `B` has through field exposed. Which kind of make sense, since it's a one through model/field for each of related models. -```python hl_lines="10-15" +```python hl_lines="12-21" class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="categories", + ) id = ormar.Integer(primary_key=True) name = ormar.String(max_length=40) # you can specify additional fields on through model class PostCategory(ormar.Model): - class Meta(BaseMeta): - tablename = "posts_x_categories" + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="posts_x_categories", + ) id: int = ormar.Integer(primary_key=True) sort_order: int = ormar.Integer(nullable=True) @@ -111,8 +116,10 @@ class PostCategory(ormar.Model): class Post(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -130,7 +137,7 @@ class Post(ormar.Model): ## Relationship default sort order -By default relations follow model default sort order so `primary_key` column ascending, or any sort order se in `Meta` class. +By default relations follow model default sort order so `primary_key` column ascending, or any sort order se in `ormar_config` object. !!!tip To read more about models sort order visit [models](../models/index.md#model-sort-order) section of documentation @@ -143,27 +150,26 @@ columns also `Through` model columns `{through_field_name}__{column_name}` Sample configuration might look like this: -```python hl_lines="24" +```python hl_lines="23" database = databases.Database(DATABASE_URL) metadata = sqlalchemy.MetaData() -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, +) class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey( @@ -186,14 +192,12 @@ In order to create auto-relation or create two models that reference each other different relations (remember the reverse side is auto-registered for you), you need to use `ForwardRef` from `typing` module. -```python hl_lines="1 11 14" +```python hl_lines="1 9 12" PersonRef = ForwardRef("Person") class Person(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -210,4 +214,4 @@ Person.update_forward_refs() [foreign-keys]: ./foreign-key.md [many-to-many]: ./many-to-many.md [queryset-proxy]: ./queryset-proxy.md -[postponed-annotations]: ./postponed-annotations.md \ No newline at end of file +[postponed-annotations]: ./postponed-annotations.md diff --git a/docs/relations/many-to-many.md b/docs/relations/many-to-many.md index 1b06590..3997090 100644 --- a/docs/relations/many-to-many.md +++ b/docs/relations/many-to-many.md @@ -9,7 +9,7 @@ Sqlalchemy column and Type are automatically taken from target `Model`. ## Defining Models -```Python hl_lines="40" +```Python hl_lines="34" --8<-- "../docs_src/relations/docs002.py" ``` @@ -24,20 +24,18 @@ news = await Category.objects.create(name="News") `ForeignKey` fields are automatically registering reverse side of the relation. -By default it's child (source) `Model` name + s, like courses in snippet below: +By default it's child (source) `Model` name + s, like `posts` in snippet below: -```python +```python hl_lines="25-26" class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) class Post(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -81,31 +79,31 @@ categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany( If you are sure you don't want the reverse relation you can use `skip_reverse=True` flag of the `ManyToMany`. - If you set `skip_reverse` flag internally the field is still registered on the other - side of the relationship so you can: +If you set `skip_reverse` flag internally the field is still registered on the other +side of the relationship so you can: + * `filter` by related models fields from reverse model * `order_by` by related models fields from reverse model - But you cannot: +But you cannot: + * access the related field from reverse model with `related_name` * even if you `select_related` from reverse side of the model the returned models won't be populated in reversed instance (the join is not prevented so you still can `filter` and `order_by` over the relation) - * the relation won't be populated in `dict()` and `json()` + * the relation won't be populated in `model_dump()` and `json()` * you cannot pass the nested related objects when populating from dictionary or json (also through `fastapi`). It will be either ignored or error will be raised depending on `extra` setting in pydantic `Config`. Example: ```python class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) class Post(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -126,8 +124,8 @@ categories = ( assert categories[0].first_name == "Test" # note that posts are not populated for author even if explicitly -# included in select_related - note no posts in dict() -assert news.dict(exclude={"id"}) == {"name": "News"} +# included in select_related - note no posts in model_dump() +assert news.model_dump(exclude={"id"}) == {"name": "News"} # still can filter through fields of related model categories = await Category.objects.filter(posts__title="Hello, M2M").all() @@ -141,7 +139,7 @@ assert len(categories) == 1 Optionally if you want to add additional fields you can explicitly create and pass the through model class. -```Python hl_lines="14-20 29" +```Python hl_lines="19-24 32" --8<-- "../docs_src/relations/docs004.py" ``` @@ -170,9 +168,7 @@ So in example like this: ```python ... # course declaration omitted class Student(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -180,10 +176,7 @@ class Student(ormar.Model): # will produce default Through model like follows (example simplified) class StudentCourse(ormar.Model): - class Meta: - database = database - metadata = metadata - tablename = "students_courses" + ormar_config = base_ormar_config.copy(tablename="students_courses") id: int = ormar.Integer(primary_key=True) student = ormar.ForeignKey(Student) # default name @@ -199,10 +192,14 @@ Example: ```python ... # course declaration omitted +base_ormar_config = ormar.OrmarConfig( + database=databases.Database("sqlite:///db.sqlite"), + metadata=sqlalchemy.MetaData(), +) + + class Student(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -212,10 +209,7 @@ class Student(ormar.Model): # will produce Through model like follows (example simplified) class StudentCourse(ormar.Model): - class Meta: - database = database - metadata = metadata - tablename = "students_courses" + ormar_config = base_ormar_config.copy(tablename="student_courses") id: int = ormar.Integer(primary_key=True) student_id = ormar.ForeignKey(Student) # set by through_relation_name @@ -238,7 +232,7 @@ so it's useful only when additional fields are provided on `Through` model. In a sample model setup as following: -```Python hl_lines="14-20 29" +```Python hl_lines="19-24 32" --8<-- "../docs_src/relations/docs004.py" ``` diff --git a/docs/relations/postponed-annotations.md b/docs/relations/postponed-annotations.md index e156296..b5b04d4 100644 --- a/docs/relations/postponed-annotations.md +++ b/docs/relations/postponed-annotations.md @@ -14,21 +14,8 @@ First, you need to import the required ref from typing. from typing import ForwardRef ``` -But note that before python 3.7 it used to be internal, so for python <= 3.6 you need - -```python -from typing import _ForwardRef as ForwardRef -``` - -or since `pydantic` is required by `ormar` it can handle this switch for you. -In that case you can simply import ForwardRef from pydantic regardless of your python version. - -```python -from pydantic.typing import ForwardRef -``` - Now we need a sample model and a reference to the same model, -which will be used to creat a self referencing relation. +which will be used to create a self referencing relation. ```python # create the forwardref to model Person @@ -36,9 +23,7 @@ PersonRef = ForwardRef("Person") class Person(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -72,9 +57,7 @@ PersonRef = ForwardRef("Person") class Person(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -93,14 +76,10 @@ and through parameters. ChildRef = ForwardRef("Child") class ChildFriend(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() class Child(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -132,9 +111,7 @@ TeacherRef = ForwardRef("Teacher") class Student(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -144,16 +121,11 @@ class Student(ormar.Model): class StudentTeacher(ormar.Model): - class Meta(ModelMeta): - tablename = 'students_x_teachers' - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename='students_x_teachers') class Teacher(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -168,4 +140,4 @@ Student.update_forward_refs() !!!warning Remember that `related_name` needs to be unique across related models regardless - of how many relations are defined. \ No newline at end of file + of how many relations are defined. diff --git a/docs/relations/queryset-proxy.md b/docs/relations/queryset-proxy.md index fa70d6f..097fc30 100644 --- a/docs/relations/queryset-proxy.md +++ b/docs/relations/queryset-proxy.md @@ -58,7 +58,7 @@ assert post.categories[0] == news `get_or_create(_defaults: Optional[Dict[str, Any]] = None, **kwargs) -> Tuple[Model, bool]` -Tries to get a row meeting the criteria and if NoMatch exception is raised it creates a new one with given kwargs and _defaults. +Tries to get a row meeting the criteria and if `NoMatch` exception is raised it creates a new one with given kwargs and _defaults. !!!tip Read more in queries documentation [get_or_create][get_or_create] @@ -127,7 +127,7 @@ provided Through model. Given sample like this: -```Python hl_lines="14-20 29" +```Python hl_lines="19-24 32" --8<-- "../docs_src/relations/docs004.py" ``` @@ -174,7 +174,7 @@ Updates the related model with provided keyword arguments, return number of upda Note that for `ManyToMany` relations update can also accept an argument with through field name and a dictionary of fields. -```Python hl_lines="14-20 29" +```Python hl_lines="19-24 32" --8<-- "../docs_src/relations/docs004.py" ``` diff --git a/docs/releases.md b/docs/releases.md index c8070b4..7c44d7f 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,224 +1,552 @@ -# 0.12.2 +# Release notes -## ✨ Features +## 0.20.0 + +### ✨ Breaking changes +* `ormar` Model configuration + + Instead of defining a `Meta` class now each of the ormar models require an ormar_config parameter that is an instance of the `OrmarConfig` class. + Note that the attribute must be named `ormar_config` and be an instance of the config class. + + ```python + import databases + import ormar + import sqlalchemy + + database = databases.Database("sqlite:///db.sqlite") + metadata = sqlalchemy.MetaData() + + # ormar < 0.20 + class Album(ormar.Model): + class Meta: + database = database + metadata = metadata + tablename = "albums" + + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + favorite: bool = ormar.Boolean(default=False) + + # ormar >= 0.20 + class AlbumV20(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="albums_v20" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + favorite: bool = ormar.Boolean(default=False) + ``` + +* `OrmarConfig` api/ parameters + + The `ormar_config` expose the same set of settings as `Meta` class used to provide. + That means that you can use any of the following parameters initializing the config: + + ```python + metadata: Optional[sqlalchemy.MetaData] + database: Optional[databases.Database] + engine: Optional[sqlalchemy.engine.Engine] + tablename: Optional[str] + order_by: Optional[List[str]] + abstract: bool + exclude_parent_fields: Optional[List[str]] + queryset_class: Type[QuerySet] + extra: Extra + constraints: Optional[List[ColumnCollectionConstraint]] + ``` + +* `BaseMeta` equivalent - best practice + + Note that to reduce the duplication of code and ease of development it's still recommended to create a base config and provide each of the models with a copy. + OrmarConfig provides a convenient `copy` method for that purpose. + + The `copy` method accepts the same parameters as `OrmarConfig` init, so you can overwrite if needed, but by default it will return already existing attributes, except for: `tablename`, `order_by` and `constraints` which by default are cleared. + + ```python hl_lines="5-8 11 20" + import databases + import ormar + import sqlalchemy + + base_ormar_config = ormar.OrmarConfig( + database=databases.Database("sqlite:///db.sqlite"), + metadata=sqlalchemy.MetaData() + ) + + class AlbumV20(ormar.Model): + ormar_config = base_ormar_config.copy( + tablename="albums_v20" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + + + class TrackV20(ormar.Model): + ormar_config = base_ormar_config.copy( + tablename="tracks_v20" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + ``` + +* `choices` Field parameter is no longer supported. + + Before version 0.20 you could provide `choices` parameter to any existing ormar Field to limit the accepted values. + This functionality was dropped, and you should use `ormar.Enum` field that was designed for this purpose. + If you want to keep the database field type (i.e. an Integer field) you can always write a custom validator. + + ```python + import databases + import ormar + import sqlalchemy + + database = databases.Database("sqlite:///db.sqlite") + metadata = sqlalchemy.MetaData() + + # ormar < 0.20 + class Artist(ormar.Model): + class Meta: + database = database + metadata = metadata + + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + country: str = ormar.String(default=False, max_length=50, choices=["UK", "US", "Vietnam", "Colombia"]) + + # ormar >= 0.20 + from enum import Enum + + class Country(str, Enum): + UK = "UK" + US = "US" + VIETNAM = "Vietnam" + COLOMBIA = "Colombia" + + class ArtistV20(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="artists_v20" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + country: Country = ormar.Enum(enum_class=Country) + ``` + + +* `pydantic_only` Field parameter is no longer supported + + `pydantic_only` fields were already deprecated and are removed in v 0.20. Ormar allows defining pydantic fields as in ordinary pydantic model. + + ```python + import databases + import ormar + import sqlalchemy + + database = databases.Database("sqlite:///db.sqlite") + metadata = sqlalchemy.MetaData() + + # ormar < 0.20 + class Dish(ormar.Model): + class Meta: + database = database + metadata = metadata + tablename = "dishes" + + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + cook: str = ormar.String(max_length=40, pydantic_only=True, default="sam") + + # ormar >= 0.20 + class DishV20(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="dishes_v20" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + cook: str = "sam" # this is normal pydantic field + ``` + +* `property_field` decorator is no longer supported + + `property_field` decorator was used to provide a way to pass calculated fields that were included in dictionary/ serialized json representation of the model. + Version 2.X of pydantic introduced such a possibility, so you should now switch to the one native to the pydantic. + + ```python + import databases + import ormar + import sqlalchemy + import pydantic + + database = databases.Database("sqlite:///db.sqlite") + metadata = sqlalchemy.MetaData() + + # ormar < 0.20 + class Employee(ormar.Model): + class Meta: + database = database + metadata = metadata + + + id: int = ormar.Integer(primary_key=True) + first_name: str = ormar.String(max_length=100) + last_name: str = ormar.String(max_length=100) + + @ormar.property_field() + def full_name(self) -> str: + return f"{self.first_name} {self.last_name}" + + # ormar >= 0.20 + class EmployeeV20(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) + + id: int = ormar.Integer(primary_key=True) + first_name: str = ormar.String(max_length=100) + last_name: str = ormar.String(max_length=100) + + @pydantic.computed_field() + def full_name(self) -> str: + return f"{self.first_name} {self.last_name}" + ``` + +* Deprecated methods + + All methods listed below are deprecated and will be removed in version 0.30 of `ormar`. + + * `dict()` becomes the `model_dump()` + + ```python + import databases + import ormar + import sqlalchemy + + database = databases.Database("sqlite:///db.sqlite") + metadata = sqlalchemy.MetaData() + + class Album(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="albums" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + favorite: bool = ormar.Boolean(default=False) + + album = Album(name="Dark Side of the Moon") + + # ormar < 0.20 + album_dict = album.dict() + + # ormar >= 0.20 + new_album_dict = album.model_dump() + ``` + + Note that parameters remain the same i.e. `include`, `exclude` etc. + + * `json()` becomes the `model_dump_json()` + + ```python + import databases + import ormar + import sqlalchemy + + database = databases.Database("sqlite:///db.sqlite") + metadata = sqlalchemy.MetaData() + + class Album(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="albums" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + favorite: bool = ormar.Boolean(default=False) + + album = Album(name="Dark Side of the Moon") + + # ormar < 0.20 + album_json= album.json() + + # ormar >= 0.20 + new_album_dict = album.model_dump_json() + ``` + + Note that parameters remain the same i.e. `include`, `exclude` etc. + + * `construct()` becomes the `model_construct()` + + ```python + import databases + import ormar + import sqlalchemy + + database = databases.Database("sqlite:///db.sqlite") + metadata = sqlalchemy.MetaData() + + class Album(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="albums" + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + favorite: bool = ormar.Boolean(default=False) + + params = { + "name": "Dark Side of the Moon", + "favorite": True, + } + # ormar < 0.20 + album = Album.construct(**params) + + # ormar >= 0.20 + album = Album.model_construct(**params) + ``` + + To read more about construct please refer to `pydantic` documentation. + + +##0.12.2 + +###✨ Features * Bump support for `FastAPI` up to the newest version (0.97.0) [#1110](https://github.com/collerek/ormar/pull/1110) * Add support and tests for `Python 3.11` [#1110](https://github.com/collerek/ormar/pull/1110) -# 0.12.1 +##0.12.1 -## ✨ Features +###✨ Features * Massive performance improvements in area of loading the models due to recursive loads and caching of the models and related models. (by @erichaydel - thanks!) [#853](https://github.com/collerek/ormar/pull/948) -## 💬 Internals +###💬 Internals * Benchmarks for comparing performance effect of implemented changes in regard of trends (again, by @erichaydel - thanks!) [#853](https://github.com/collerek/ormar/pull/948) -# 0.12.0 +##0.12.0 -## ✨ Breaking Changes +###✨ Breaking Changes * `Queryset.bulk_create` will now raise `ModelListEmptyError` on empty list of models (by @ponytailer - thanks!) [#853](https://github.com/collerek/ormar/pull/853) -## ✨ Features +###✨ Features * `Model.upsert()` now handles a flag `__force_save__`: `bool` that allow upserting the models regardless of the fact if they have primary key set or not. Note that setting this flag will cause two queries for each upserted model -> `get` to check if model exists and later `update/insert` accordingly. [#889](https://github.com/collerek/ormar/pull/853) -## 🐛 Fixes +###🐛 Fixes * Fix for empty relations breaking `construct` method (by @Abdeldjalil-H - thanks!) [#870](https://github.com/collerek/ormar/issues/870) * Fix save related not saving models with already set pks (including uuid) [#885](https://github.com/collerek/ormar/issues/885) * Fix for wrong relations exclusions depending on the order of exclusions [#779](https://github.com/collerek/ormar/issues/779) * Fix `property_fields` not being inherited properly [#774](https://github.com/collerek/ormar/issues/774) -# 0.11.3 +##0.11.3 -## ✨ Features +###✨ Features * Document `onupdate` and `ondelete` referential actions in `ForeignKey` and provide `ReferentialAction` enum to specify the behavior of the relationship (by @SepehrBazyar - thanks!) [#724](https://github.com/collerek/ormar/issues/724) * Add `CheckColumn` to supported constraints in models Meta (by @SepehrBazyar - thanks!) [#729](https://github.com/collerek/ormar/issues/729) -## 🐛 Fixes +###🐛 Fixes * Fix limiting query result to 0 should return empty list (by @SepehrBazyar - thanks!) [#766](https://github.com/collerek/ormar/issues/713) -## 💬 Other +###💬 Other * Add dark mode to docs (by @SepehrBazyar - thanks!) [#717](https://github.com/collerek/ormar/pull/717) * Update aiomysql dependency [#778](https://github.com/collerek/ormar/issues/778) -# 0.11.2 +##0.11.2 -## 🐛 Fixes +###🐛 Fixes * Fix database drivers being required, while they should be optional [#713](https://github.com/collerek/ormar/issues/713) * Fix boolean field problem in `limit` queries in postgres without `limit_raw_sql` flag [#704](https://github.com/collerek/ormar/issues/704) * Fix enum_class spilling to schema causing errors in OpenAPI [#699](https://github.com/collerek/ormar/issues/699) -# 0.11.1 +##0.11.1 -## 🐛 Fixes +###🐛 Fixes * Fix deepcopy issues introduced in pydantic 1.9 [#685](https://github.com/collerek/ormar/issues/685) -# 0.11.0 +##0.11.0 -## ✨ Breaking Changes +###✨ Breaking Changes * Dropped support for python 3.6 * `Queryset.get_or_create` returns now a tuple with model and bool value indicating if the model was created (by @MojixCoder - thanks!) [#554](https://github.com/collerek/ormar/pull/554) * `Queryset.count()` now counts the number of distinct parent model rows by default, counting all rows is possible by setting `distinct=False` (by @erichaydel - thanks) [#588](https://github.com/collerek/ormar/pull/588) -## ✨ Features +###✨ Features * Added support for python 3.10 -## 🐛 Fixes +###🐛 Fixes * Fix inconsistent `JSON` fields behaviour in `save` and `bulk_create` [#584](https://github.com/collerek/ormar/issues/584) * Fix maximum recursion error [#580](https://github.com/collerek/ormar/pull/580) -# 0.10.25 +##0.10.25 -## ✨ Features +###✨ Features * Add `queryset_class` option to `Model.Meta` that allows you to easily swap `QuerySet` for your Model (by @ponytailer - thanks!) [#538](https://github.com/collerek/ormar/pull/538) * Allow passing extra `kwargs` to `IndexColumns` that will be passed to sqlalchemy `Index` (by @zevisert - thanks) [#575](https://github.com/collerek/ormar/pull/538) -## 🐛 Fixes +###🐛 Fixes * Fix nullable setting on `JSON` fields [#529](https://github.com/collerek/ormar/issues/529) * Fix bytes/str mismatch in bulk operations when using orjson instead of json (by @ponytailer - thanks!) [#538](https://github.com/collerek/ormar/pull/538) -# 0.10.24 +##0.10.24 -## ✨ Features +###✨ Features * Add `post_bulk_update` signal (by @ponytailer - thanks!) [#524](https://github.com/collerek/ormar/pull/524) -## 🐛 Fixes +###🐛 Fixes * Fix support for `pydantic==1.9.0` [#502](https://github.com/collerek/ormar/issues/502) * Fix timezone issues with datetime [#504](https://github.com/collerek/ormar/issues/504) * Remove literal binds in query generation to unblock postgres arrays [#/tophat/ormar-postgres-extensions/9](https://github.com/tophat/ormar-postgres-extensions/pull/9) * Fix bulk update for `JSON` fields [#519](https://github.com/collerek/ormar/issues/519) -## 💬 Other +###💬 Other * Improve performance of `bulk_create` by bypassing `databases` `execute_many` suboptimal implementation. (by @Mng-dev-ai thanks!) [#520](https://github.com/collerek/ormar/pull/520) * Bump min. required `databases` version to `>=5.4`. -# 0.10.23 +##0.10.23 -## ✨ Features +###✨ Features * Add ability to pass `comment` to sqlalchemy when creating a column [#485](https://github.com/collerek/ormar/issues/485) -## 🐛 Fixes +###🐛 Fixes * Fix `LargeBinary` fields that can be nullable [#409](https://github.com/collerek/ormar/issues/409) * Make `ormar.Model` pickable [#413](https://github.com/collerek/ormar/issues/413) * Make `first()` and `get()` without arguments respect ordering of main model set by user, fallback to primary key (asc, and desc respectively) [#453](https://github.com/collerek/ormar/issues/453) * Fix improper quoting of non-aliased join `on` clauses in postgress [#455](https://github.com/collerek/ormar/issues/455) -# 0.10.22 +##0.10.22 -## 🐛 Fixes +###🐛 Fixes * Hot fix for validators not being inherited when parent `ormar` model was set [#365](https://github.com/collerek/ormar/issues/365) -# 0.10.21 +##0.10.21 -## 🐛 Fixes +###🐛 Fixes * Add `ormar` implementation of `construct` classmethod that allows to build `Model` instances without validating the input to speed up the whole flow, if your data is already validated [#318](https://github.com/collerek/ormar/issues/318) * Fix for "inheriting" field validators from `ormar` model when newly created pydanic model is generated with `get_pydantic` [#365](https://github.com/collerek/ormar/issues/365) -# 0.10.20 +##0.10.20 -## ✨ Features +###✨ Features * Add `extra` parameter in `Model.Meta` that accepts `Extra.ignore` and `Extra.forbid` (default) and either ignores the extra fields passed to `ormar` model or raises an exception if one is encountered [#358](https://github.com/collerek/ormar/issues/358) -## 🐛 Fixes +###🐛 Fixes * Allow `None` if field is nullable and have choices set [#354](https://github.com/collerek/ormar/issues/354) * Always set `primary_key` to `not null` regardless of `autoincrement` and explicit `nullable` setting to avoid problems with migrations [#348](https://github.com/collerek/ormar/issues/348) -# 0.10.19 +##0.10.19 -## ✨ Features +###✨ Features * Add support for multi-column non-unique `IndexColumns` in `Meta.constraints` [#307](https://github.com/collerek/ormar/issues/307) * Add `sql_nullable` field attribute that allows to set different nullable setting for pydantic model and for underlying sql column [#308](https://github.com/collerek/ormar/issues/308) -## 🐛 Fixes +###🐛 Fixes * Enable caching of relation map to increase performance [#337](https://github.com/collerek/ormar/issues/337) * Clarify and fix documentation in regard of nullable fields [#339](https://github.com/collerek/ormar/issues/339) -## 💬 Other +###💬 Other * Bump supported `databases` version to `<=5.2`. -# 0.10.18 +##0.10.18 -## 🐛 Fixes +###🐛 Fixes * Fix order of fields in pydantic models [#328](https://github.com/collerek/ormar/issues/328) * Fix databases 0.5.0 support [#142](https://github.com/collerek/ormar/issues/142) -# 0.10.17 +##0.10.17 -## ✨ Features +###✨ Features * Allow overwriting the default pydantic type for model fields [#312](https://github.com/collerek/ormar/issues/312) * Add support for `sqlalchemy` >=1.4 (requires `databases` >= 0.5.0) [#142](https://github.com/collerek/ormar/issues/142) -# 0.10.16 +##0.10.16 -## ✨ Features +###✨ Features * Allow passing your own pydantic `Config` to `ormar.Model` that will be merged with the default one by @naturalethic (thanks!) [#285](https://github.com/collerek/ormar/issues/285) * Add `SmallInteger` field type by @ProgrammerPlus1998 (thanks!) [#297](https://github.com/collerek/ormar/pull/297) -## 🐛 Fixes +###🐛 Fixes * Fix generating openapi schema by removing obsolete pydantic field parameters that were directly exposed in schema [#291](https://github.com/collerek/ormar/issues/291) * Fix unnecessary warning for auto generated through models [#295](https://github.com/collerek/ormar/issues/295) -# 0.10.15 +##0.10.15 -## 🐛 Fixes +###🐛 Fixes * Fix generating pydantic models tree with nested models (by @pawamoy - thanks!) [#278](https://github.com/collerek/ormar/issues/278) * Fix missing f-string in warning about missing primary key field [#274](https://github.com/collerek/ormar/issues/274) * Fix passing foreign key value as relation (additional guard, fixed already in the latest release) [#270](https://github.com/collerek/ormar/issues/270) -# 0.10.14 +##0.10.14 -## ✨ Features +###✨ Features * Allow passing `timezone:bool = False` parameter to `DateTime` and `Time` fields for timezone aware database columns [#264](https://github.com/collerek/ormar/issues/264) * Allow passing datetime, date and time for filter on `DateTime`, `Time` and `Date` fields to allow filtering by datetimes instead of converting the value to string [#79](https://github.com/collerek/ormar/issues/79) -## 🐛 Fixes +###🐛 Fixes * Fix dependencies from `psycopg2` to `psycopg2-binary` [#255](https://github.com/collerek/ormar/issues/255) -# 0.10.13 +##0.10.13 -## ✨ Features +###✨ Features * Allow passing field accessors in `select_related` and `prefetch_related` aka. python style `select_related` [#225](https://github.com/collerek/ormar/issues/225). * Previously: @@ -232,21 +560,21 @@ Note that setting this flag will cause two queries for each upserted model -> `g await Author.objects.prefetch_related(Author.posts.categories).get() ``` -## 🐛 Fixes +###🐛 Fixes * Fix overwriting default value for inherited primary key [#253](https://github.com/collerek/ormar/issues/253) -# 0.10.12 +##0.10.12 -## 🐛 Fixes +###🐛 Fixes * Fix `QuerySet.create` method not using init (if custom provided) [#245](https://github.com/collerek/ormar/issues/245) * Fix `ForwardRef` `ManyToMany` relation setting wrong pydantic type [#250](https://github.com/collerek/ormar/issues/250) -# 0.10.11 +##0.10.11 -## ✨ Features +###✨ Features * Add `values` and `values_list` to `QuerySet` and `QuerysetProxy` that allows to return raw data from query [#223](https://github.com/collerek/ormar/issues/223). * Allow returning list of tuples or list of dictionaries from a query @@ -254,53 +582,53 @@ Note that setting this flag will cause two queries for each upserted model -> `g * Allow excluding models in between in chain of relations, so you can extract only needed columns * `values_list` allows you to flatten the result if you extract only one column. -## 🐛 Fixes +###🐛 Fixes * Fix creation of auto through model for m2m relation with ForwardRef [#226](https://github.com/collerek/ormar/issues/226) -# 0.10.10 +##0.10.10 -## ✨ Features +###✨ Features * Add [`get_pydantic`](https://collerek.github.io/ormar/models/methods/#get_pydantic) flag that allows you to auto generate equivalent pydantic models tree from ormar.Model. This newly generated model tree can be used in requests and responses to exclude fields you do not want to include in the data. * Add [`exclude_parent_fields`](https://collerek.github.io/ormar/models/inheritance/#exclude_parent_fields) parameter to model Meta that allows you to exclude fields from parent models during inheritance. Note that best practice is to combine models and mixins but if you have many similar models and just one that differs it might be useful tool to achieve that. -## 🐛 Fixes +###🐛 Fixes * Fix is null filter with pagination and relations (by @erichaydel) [#214](https://github.com/collerek/ormar/issues/214) * Fix not saving child object on reverse side of the relation if not saved before [#216](https://github.com/collerek/ormar/issues/216) -## 💬 Other +###💬 Other * Expand [fastapi](https://collerek.github.io/ormar/fastapi) part of the documentation to show samples of using ormar in requests and responses in fastapi. * Improve the docs in regard of `default`, `ForeignKey.add` etc. -# 0.10.9 +##0.10.9 -## Important security fix +###Important security fix * Update pin for pydantic to fix security vulnerability [CVE-2021-29510](https://github.com/samuelcolvin/pydantic/security/advisories/GHSA-5jqp-qgf6-3pvh) You are advised to update to version of pydantic that was patched. In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. -## 🐛 Fixes +###🐛 Fixes * Fix OpenAPi schema for LargeBinary [#204](https://github.com/collerek/ormar/issues/204) -# 0.10.8 +##0.10.8 -## 🐛 Fixes +###🐛 Fixes * Fix populating default values in pk_only child models [#202](https://github.com/collerek/ormar/issues/202) * Fix mypy for LargeBinary fields with base64 str representation [#199](https://github.com/collerek/ormar/issues/199) * Fix OpenAPI schema format for LargeBinary fields with base64 str representation [#199](https://github.com/collerek/ormar/issues/199) * Fix OpenAPI choices encoding for LargeBinary fields with base64 str representation -# 0.10.7 +##0.10.7 -## ✨ Features +###✨ Features * Add `exclude_primary_keys: bool = False` flag to `dict()` method that allows to exclude all primary key columns in the resulting dictionaru. [#164](https://github.com/collerek/ormar/issues/164) * Add `exclude_through_models: bool = False` flag to `dict()` that allows excluding all through models from `ManyToMany` relations [#164](https://github.com/collerek/ormar/issues/164) @@ -308,19 +636,19 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. on access to attribute and string is converted to bytes on setting. Data in database is stored as bytes. [#187](https://github.com/collerek/ormar/issues/187) * Add `pk` alias to allow field access by `Model.pk` in filters and order by clauses (python style) -## 🐛 Fixes +###🐛 Fixes * Remove default `None` option for `max_length` for `LargeBinary` field [#186](https://github.com/collerek/ormar/issues/186) * Remove default `None` option for `max_length` for `String` field -## 💬 Other +###💬 Other * Provide a guide and samples of `dict()` parameters in the [docs](https://collerek.github.io/ormar/models/methods/) * Major refactor of getting/setting attributes from magic methods into descriptors -> noticeable performance improvement -# 0.10.6 +##0.10.6 -## ✨ Features +###✨ Features * Add `LargeBinary(max_length)` field type [#166](https://github.com/collerek/ormar/issues/166) * Add support for normal pydantic fields (including Models) instead of `pydantic_only` @@ -340,30 +668,30 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. The same algorithm is used to iterate related models without looks as with `dict()` and `select/load_all`. Examples appear also in `fastapi`. [#157](https://github.com/collerek/ormar/issues/157) -## 🐛 Fixes +###🐛 Fixes * By default `pydantic` is not validating fields during assignment, which is not a desirable setting for an ORM, now all `ormar.Models` have validation turned-on during assignment (like `model.column = 'value'`) -## 💬 Other +###💬 Other * Add connecting to the database in QuickStart in readme [#180](https://github.com/collerek/ormar/issues/180) * OpenAPI schema does no longer include `ormar.Model` docstring as description, instead just model name is provided if you do not provide your own docstring. * Some performance improvements. -# 0.10.5 +##0.10.5 -## 🐛 Fixes +###🐛 Fixes * Fix bug in `fastapi-pagination` [#73](https://github.com/uriyyo/fastapi-pagination/issues/73) * Remove unnecessary `Optional` in `List[Optional[T]]` in return value for `QuerySet.all()` and `Querysetproxy.all()` return values [#174](https://github.com/collerek/ormar/issues/174) * Run tests coverage publish only on internal prs instead of all in github action. -# 0.10.4 +##0.10.4 -## ✨ Features +###✨ Features * Add **Python style** to `filter` and `order_by` with field access instead of dunder separated strings. [#51](https://github.com/collerek/ormar/issues/51) * Accessing a field with attribute access (chain of dot notation) can be used to construct `FilterGroups` (`ormar.and_` and `ormar.or_`) @@ -475,15 +803,15 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. * You can of course also combine different models and many order_bys: `Product.objects.order_by([Product.category.name.asc(), Product.name.desc()]).all()` -## 🐛 Fixes +### 🐛 Fixes * Not really a bug but rather inconsistency. Providing a filter with nested model i.e. `album__category__name = 'AA'` is checking if album and category models are included in `select_related()` and if not it's auto-adding them there. The same functionality was not working for `FilterGroups` (`and_` and `or_`), now it works (also for python style filters which return `FilterGroups`). -# 0.10.3 +## 0.10.3 -## ✨ Features +### ✨ Features * `ForeignKey` and `ManyToMany` now support `skip_reverse: bool = False` flag [#118](https://github.com/collerek/ormar/issues/118). If you set `skip_reverse` flag internally the field is still registered on the other @@ -511,7 +839,7 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. * By default `Through` model relation names default to related model name in lowercase. So in example like this: ```python - ... # course declaration omitted + ... ## course declaration omitted class Student(ormar.Model): class Meta: database = database @@ -521,7 +849,7 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. name: str = ormar.String(max_length=100) courses = ormar.ManyToMany(Course) - # will produce default Through model like follows (example simplified) + ## will produce default Through model like follows (example simplified) class StudentCourse(ormar.Model): class Meta: database = database @@ -529,7 +857,7 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. tablename = "students_courses" id: int = ormar.Integer(primary_key=True) - student = ormar.ForeignKey(Student) # default name + student = ormar.ForeignKey(Student) ## default name course = ormar.ForeignKey(Course) # default name ``` * To customize the names of fields/relation in Through model now you can use new parameters to `ManyToMany`: @@ -562,22 +890,22 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. course_id = ormar.ForeignKey(Course) # set by through_reverse_relation_name ``` -## 🐛 Fixes +### 🐛 Fixes * Fix weakref `ReferenceError` error [#118](https://github.com/collerek/ormar/issues/118) * Fix error raised by Through fields when pydantic `Config.extra="forbid"` is set * Fix bug with `pydantic.PrivateAttr` not being initialized at `__init__` [#149](https://github.com/collerek/ormar/issues/149) * Fix bug with pydantic-type `exclude` in `dict()` with `__all__` key not working -## 💬 Other +### 💬 Other * Introduce link to `sqlalchemy-to-ormar` auto-translator for models * Provide links to fastapi ecosystem libraries that support `ormar` * Add transactions to docs (supported with `databases`) -# 0.10.2 +## 0.10.2 -## ✨ Features +### ✨ Features * `Model.save_related(follow=False)` now accept also two additional arguments: `Model.save_related(follow=False, save_all=False, exclude=None)`. * `save_all:bool` -> By default (so with `save_all=False`) `ormar` only upserts models that are not saved (so new or updated ones), @@ -588,7 +916,7 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. the `fields/exclude_fields` (from `QuerySet`) methods schema so when in doubt you can refer to docs in queries -> selecting subset of fields -> fields. * `Model.update()` method now accepts `_columns: List[str] = None` parameter, that accepts list of column names to update. If passed only those columns will be updated in database. Note that `update()` does not refresh the instance of the Model, so if you change more columns than you pass in `_columns` list your Model instance will have different values than the database! -* `Model.dict()` method previously included only directly related models or nested models if they were not nullable and not virtual, +* `Model.model_dump()` method previously included only directly related models or nested models if they were not nullable and not virtual, now all related models not previously visited without loops are included in `dict()`. This should be not breaking as just more data will be dumped to dict, but it should not be missing. * `QuerySet.delete(each=False, **kwargs)` previously required that you either pass a `filter` (by `**kwargs` or as a separate `filter()` call) or set `each=True` now also accepts @@ -598,7 +926,7 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. * Same thing applies to `QuerysetProxy.update(each=False, **kwargs)` which also previously required that you either pass a `filter` (by `**kwargs` or as a separate `filter()` call) or set `each=True` now also accepts `exclude()` calls that generates NOT filter. So either `each=True` needs to be set to update whole table or at least one of `filter/exclude` clauses. -## 🐛 Fixes +### 🐛 Fixes * Fix improper relation field resolution in `QuerysetProxy` if fk column has different database alias. * Fix hitting recursion error with very complicated models structure with loops when calling `dict()`. @@ -607,32 +935,32 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. * Fix bug when bulk_create would try to save also `property_field` decorated methods and `pydantic` fields * Fix wrong merging of deeply nested chain of reversed relations -## 💬 Other +### 💬 Other * Performance optimizations * Split tests into packages based on tested area -# 0.10.1 +## 0.10.1 -## Features +### Features * add `get_or_none(**kwargs)` method to `QuerySet` and `QuerysetProxy`. It is exact equivalent of `get(**kwargs)` but instead of raising `ormar.NoMatch` exception if there is no db record matching the criteria, `get_or_none` simply returns `None`. -## Fixes +### Fixes * Fix dialect dependent quoting of column and table names in order_by clauses not working properly in postgres. -# 0.10.0 +## 0.10.0 -## Breaking +### Breaking * Dropped supported for long deprecated notation of field definition in which you use ormar fields as type hints i.e. `test_field: ormar.Integger() = None` * Improved type hints -> `mypy` can properly resolve related models fields (`ForeignKey` and `ManyToMany`) as well as return types of `QuerySet` methods. Those mentioned are now returning proper model (i.e. `Book`) instead or `ormar.Model` type. There is still problem with reverse sides of relation and `QuerysetProxy` methods, to ease type hints now those return `Any`. Partially fixes #112. -## Features +### Features * add `select_all(follow: bool = False)` method to `QuerySet` and `QuerysetProxy`. It is kind of equivalent of the Model's `load_all()` method but can be used directly in a query. @@ -641,14 +969,14 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. so you still have to issue `get()`, `all()` etc. as `select_all()` returns a QuerySet (or proxy) like `fields()` or `order_by()`. -## Internals +### Internals * `ormar` fields are no longer stored as classes in `Meta.model_fields` dictionary but instead they are stored as instances. -# 0.9.9 +## 0.9.9 -## Features +### Features * Add possibility to change default ordering of relations and models. * To change model sorting pass `orders_by = [columns]` where `columns: List[str]` to model `Meta` class * To change relation order_by pass `orders_by = [columns]` where `columns: List[str]` @@ -677,7 +1005,7 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. `relation_name: str` - name of the relation to which child is added, for add signals also `passed_kwargs: Dict` - dict of kwargs passed to `add()` -## Changes +### Changes * `Through` models for ManyToMany relations are now instantiated on creation, deletion and update, so you can provide not only autoincrement int as a primary key but any column type with default function provided. * Since `Through` models are now instantiated you can also subscribe to `Through` model @@ -685,15 +1013,15 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. * `pre_update` signals receivers now get also passed_args argument which is a dict of values passed to update function if any (else empty dict) -## Fixes +### Fixes * `pre_update` signal now is sent before the extraction of values so you can modify the passed instance in place and modified fields values will be reflected in database * `bulk_update` now works correctly also with `UUID` primary key column type -# 0.9.8 +## 0.9.8 -## Features +### Features * Add possibility to encrypt the selected field(s) in the database * As minimum you need to provide `encrypt_secret` and `encrypt_backend` * `encrypt_backend` can be one of the `ormar.EncryptBackends` enum (`NONE, FERNET, HASH, CUSTOM`) - default: `NONE` @@ -706,12 +1034,12 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. * Note that in HASH backend you can filter by full value but filters like `contain` will not work as comparison is make on encrypted values * Note that adding `encrypt_backend` changes the database column type to `TEXT`, which needs to be reflected in db either by migration or manual change -## Fixes +### Fixes * (Advanced/ Internal) Restore custom sqlalchemy types (by `types.TypeDecorator` subclass) functionality that ceased to working so `process_result_value` was never called -# 0.9.7 +## 0.9.7 -## Features +### Features * Add `isnull` operator to filter and exclude methods. ```python album__name__isnull=True #(sql: album.name is null) @@ -734,10 +1062,10 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. ``` Check the updated docs in Queries -> Filtering and sorting -> Complex filters -## Other +### Other * Setting default on `ForeignKey` or `ManyToMany` raises and `ModelDefinition` exception as it is (and was) not supported -# 0.9.6 +## 0.9.6 ##Important * `Through` model for `ManyToMany` relations now **becomes optional**. It's not a breaking change @@ -749,7 +1077,7 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. Note that you still need to provide it if you want to customize the `Through` model name or the database table name. -## Features +### Features * Add `update` method to `QuerysetProxy` so now it's possible to update related models directly from parent model in `ManyToMany` relations and in reverse `ForeignKey` relations. Note that update like in `QuerySet` `update` returns number of updated models and **does not update related models in place** on parent model. To get the refreshed data on parent model you need to refresh @@ -772,27 +1100,27 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. but now if you try to do so `ModelDefinitionError` will be thrown * check the updated ManyToMany relation docs for more information -# Other +## Other * Updated docs and api docs * Refactors and optimisations mainly related to filters, exclusions and order bys -# 0.9.5 +## 0.9.5 -## Fixes +### Fixes * Fix creation of `pydantic` FieldInfo after update of `pydantic` to version >=1.8 * Pin required dependency versions to avoid such situations in the future -# 0.9.4 +## 0.9.4 -## Fixes +### Fixes * Fix `fastapi` OpenAPI schema generation for automatic docs when multiple models refer to the same related one -# 0.9.3 +## 0.9.3 -## Fixes +### Fixes * Fix `JSON` field being double escaped when setting value after initialization * Fix `JSON` field not respecting `nullable` field setting due to `pydantic` internals * Fix `choices` verification for `JSON` field @@ -800,25 +1128,25 @@ In 0.10.9 ormar excludes versions with vulnerability in pinned dependencies. * Fix `choices` not being verified during `update` call from `QuerySet` -# 0.9.2 +## 0.9.2 -## Other +### Other * Updated the Quick Start in docs/readme * Updated docs with links to queries subpage * Added badges for code climate and pepy downloads -# 0.9.1 +## 0.9.1 -## Features +### Features * Add choices values to `OpenAPI` specs, so it looks like native `Enum` field in the result schema. -## Fixes +### Fixes * Fix `choices` behavior with `fastapi` usage when special fields can be not initialized yet but passed as strings etc. -# 0.9.0 +## 0.9.0 -## Important +### Important * **Braking Fix:** Version 0.8.0 introduced a bug that prevents generation of foreign_keys constraint in the database, both in alembic and during creation through sqlalchemy.engine, this is fixed now. * **THEREFORE IF YOU USE VERSION >=0.8.0 YOU ARE STRONGLY ADVISED TO UPDATE** cause despite @@ -828,7 +1156,7 @@ that most of the `ormar` functions are working your database **CREATED with orma should be fine nevertheless you should update to reflect all future schema updates in your models. -## Breaking +### Breaking * **Breaking:** All foreign_keys and unique constraints now have a name so `alembic` can identify them in db and not depend on db * **Breaking:** During model construction if `Meta` class of the `Model` does not @@ -839,14 +1167,14 @@ for sqlite backend, meaning that each query is run with a new connection and the This is changed in `ormar` since >=0.9.0 and by default each sqlite3 query has `"PRAGMA foreign_keys=1;"` run so now each sqlite3 connection by default enforces ForeignKey constraints including cascades. -## Other +### Other * Update api docs. * Add tests for fk creation in db and for cascades in db -# 0.8.1 +## 0.8.1 -## Features +### Features * Introduce processing of `ForwardRef` in relations. Now you can create self-referencing models - both `ForeignKey` and `ManyToMany` relations. @@ -864,15 +1192,15 @@ for sqlite backend, meaning that each query is run with a new connection and the * Introduce the `paginate` method that allows to limit/offset by `page` and `page_size`. Available for `QuerySet` and `QuerysetProxy`. -## Other +### Other * Refactoring and performance optimization in queries and joins. * Add python 3.9 to tests and pypi setup. * Update API docs and docs -> i.e. split of queries documentation. -# 0.8.0 +## 0.8.0 -## Breaking +### Breaking * **Breaking:** `remove()` parent from child side in reverse ForeignKey relation now requires passing a relation `name`, as the same model can be registered multiple times and `ormar` needs to know from which relation on the parent you want to remove the child. * **Breaking:** applying `limit` and `offset` with `select_related` is by default applied only on the main table before the join -> meaning that not the total @@ -881,20 +1209,20 @@ as the same model can be registered multiple times and `ormar` needs to know fro * **Breaking:** issuing `get()` **without any filters** now fetches the first row ordered by the primary key desc (so should be last one inserted (can be different for non number primary keys - i.e. alphabetical order of string)) * **Breaking (internal):** sqlalchemy columns kept at `Meta.columns` are no longer bind to table, so you cannot get the column straight from there -## Features +### Features * Introduce **inheritance**. For now two types of inheritance are possible: * **Mixins** - don't subclass `ormar.Model`, just define fields that are later used on different models (like `created_date` and `updated_date` on each child model), only actual models create tables, but those fields from mixins are added * **Concrete table inheritance** - means that parent is marked as `abstract=True` in Meta class and each child has its own table with columns from the parent and own child columns, kind of similar to Mixins but parent also is a (an abstract) Model * To read more check the docs on models -> inheritance section. * QuerySet `first()` can be used with `prefetch_related` -## Fixes +### Fixes * Fix minor bug in `order_by` for primary model order bys * Fix in `prefetch_query` for multiple related_names for the same model. * Fix using same `related_name` on different models leading to the same related `Model` overwriting each other, now `ModelDefinitionError` is raised and you need to change the name. * Fix `order_by` overwriting conditions when multiple joins to the same table applied. -## Docs +### Docs * Split and cleanup in docs: * Divide models section into subsections * Divide relations section into subsections @@ -902,11 +1230,11 @@ as the same model can be registered multiple times and `ormar` needs to know fro * Add model inheritance section * Add API (BETA) documentation -# 0.7.5 +## 0.7.5 * Fix for wrong relation column name in many_to_many relation joins (fix [#73][#73]) -# 0.7.4 +## 0.7.4 * Allow multiple relations to the same related model/table. * Fix for wrong relation column used in many_to_many relation joins (fix [#73][#73]) @@ -914,19 +1242,19 @@ as the same model can be registered multiple times and `ormar` needs to know fro * Add check if user provide related_name if there are multiple relations to same table on one model. * More eager cleaning of the dead weak proxy models. -# 0.7.3 +## 0.7.3 * Fix for setting fetching related model with UUDI pk, which is a string in raw (fix [#71][#71]) -# 0.7.2 +## 0.7.2 * Fix for overwriting related models with pk only in `Model.update() with fields passed as parameters` (fix [#70][#70]) -# 0.7.1 +## 0.7.1 * Fix for overwriting related models with pk only in `Model.save()` (fix [#68][#68]) -# 0.7.0 +## 0.7.0 * **Breaking:** QuerySet `bulk_update` method now raises `ModelPersistenceError` for unsaved models passed instead of `QueryDefinitionError` * **Breaking:** Model initialization with unknown field name now raises `ModelError` instead of `KeyError` @@ -936,19 +1264,19 @@ as the same model can be registered multiple times and `ormar` needs to know fro * Performance optimization * Updated docs -# 0.6.2 +## 0.6.2 * Performance optimization * Fix for bug with `pydantic_only` fields being required * Add `property_field` decorator that registers a function as a property that will - be included in `Model.dict()` and in `fastapi` response + be included in `Model.model_dump()` and in `fastapi` response * Update docs -# 0.6.1 +## 0.6.1 * Explicitly set None to excluded nullable fields to avoid pydantic setting a default value (fix [#60][#60]). -# 0.6.0 +## 0.6.0 * **Breaking:** calling instance.load() when the instance row was deleted from db now raises `NoMatch` instead of `ValueError` * **Breaking:** calling add and remove on ReverseForeignKey relation now updates the child model in db setting/removing fk column @@ -960,12 +1288,12 @@ as the same model can be registered multiple times and `ormar` needs to know fro so now you can use those methods directly from relation * Update docs -# 0.5.5 +## 0.5.5 * Fix for alembic autogenaration of migration `UUID` columns. It should just produce sqlalchemy `CHAR(32)` or `CHAR(36)` * In order for this to work you have to set user_module_prefix='sa.' (must be equal to sqlalchemy_module_prefix option (default 'sa.')) -# 0.5.4 +## 0.5.4 * Allow to pass `uuid_format` (allowed 'hex'(default) or 'string') to `UUID` field to change the format in which it's saved. By default field is saved in hex format (trimmed to 32 chars (without dashes)), but you can pass @@ -975,21 +1303,21 @@ so now you can use those methods directly from relation * hex value = c616ab438cce49dbbf4380d109251dce * string value = c616ab43-8cce-49db-bf43-80d109251dce -# 0.5.3 +## 0.5.3 -* Fixed bug in `Model.dict()` method that was ignoring exclude parameter and not include dictionary argument. +* Fixed bug in `Model.model_dump()` method that was ignoring exclude parameter and not include dictionary argument. -# 0.5.2 +## 0.5.2 * Added `prefetch_related` method to load subsequent models in separate queries. * Update docs -# 0.5.1 +## 0.5.1 * Switched to github actions instead of travis * Update badges in the docs -# 0.5.0 +## 0.5.0 * Added save status -> you can check if model is saved with `ModelInstance.saved` property * Model is saved after `save/update/load/upsert` method on model @@ -1010,7 +1338,7 @@ so now you can use those methods directly from relation * Optional performance dependency orjson added (**strongly recommended**) * Updated docs -# 0.4.4 +## 0.4.4 * add exclude_fields() method to exclude fields from sql * refactor column names setting (aliases) @@ -1018,20 +1346,20 @@ so now you can use those methods directly from relation * additional tests for fields and exclude_fields * update docs -# 0.4.3 +## 0.4.3 -* include properties in models.dict() and model.json() +* include properties in models.model_dump() and model.model_dump_json() -# 0.4.2 +## 0.4.2 * modify creation of pydantic models to allow returning related models with only pk populated -# 0.4.1 +## 0.4.1 * add order_by method to queryset to allow sorting * update docs -# 0.4.0 +## 0.4.0 * Changed notation in Model definition -> now use name = ormar.Field() not name: ormar.Field() * Note that old notation is still supported but deprecated and will not play nice with static checkers like mypy and pydantic pycharm plugin @@ -1043,42 +1371,42 @@ so now you can use those methods directly from relation * Add mypy and pydantic plugin to docs * Expand the docs on ManyToMany relation -# 0.3.11 +## 0.3.11 * Fix setting server_default as default field value in python -# 0.3.10 +## 0.3.10 * Fix postgresql check to avoid exceptions with drivers not installed if using different backend -# 0.3.9 +## 0.3.9 * Fix json schema generation as of [#19][#19] * Fix for not initialized ManyToMany relations in fastapi copies of ormar.Models * Update docs in regard of fastapi use * Add tests to verify fastapi/docs proper generation -# 0.3.8 +## 0.3.8 * Added possibility to provide alternative database column names with name parameter to all fields. * Fix bug with selecting related ManyToMany fields with `fields()` if they are empty. * Updated documentation -# 0.3.7 +## 0.3.7 * Publish documentation and update readme -# 0.3.6 +## 0.3.6 * Add fields() method to limit the selected columns from database - only nullable columns can be excluded. * Added UniqueColumns and constraints list in model Meta to build unique constraints on list of columns. * Added UUID field type based on Char(32) column type. -# 0.3.5 +## 0.3.5 * Added bulk_create and bulk_update for operations on multiple objects. -# 0.3.4 +## 0.3.4 Add queryset level methods * delete @@ -1086,21 +1414,21 @@ Add queryset level methods * get_or_create * update_or_create -# 0.3.3 +## 0.3.3 * Add additional filters - startswith and endswith -# 0.3.2 +## 0.3.2 * Add choices parameter to all fields - limiting the accepted values to ones provided -# 0.3.1 +## 0.3.1 * Added exclude to filter where not conditions. * Added tests for mysql and postgres with fixes for postgres. * Rafactors and cleanup. -# 0.3.0 +## 0.3.0 * Added ManyToMany field and support for many to many relations diff --git a/docs/signals.md b/docs/signals.md index ce6b261..81ad965 100644 --- a/docs/signals.md +++ b/docs/signals.md @@ -14,15 +14,15 @@ import sqlalchemy import ormar -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() + +base_ormar_config = ormar.OrmarConfig( + database=databases.Database("sqlite:///db.sqlite"), + metadata=sqlalchemy.MetaData(), +) class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -34,7 +34,7 @@ You can for example define a trigger that will set `album.is_best_seller` status Import `pre_update` decorator, for list of currently available decorators/ signals check below. -```Python hl_lines="1" +```Python hl_lines="7" --8<-- "../docs_src/signals/docs002.py" ``` @@ -54,7 +54,7 @@ for which you want to run the signal receiver. Currently there is no way to set signal for all models at once without explicitly passing them all into registration of receiver. -```Python hl_lines="4-7" +```Python hl_lines="28-31" --8<-- "../docs_src/signals/docs002.py" ``` @@ -65,7 +65,7 @@ Currently there is no way to set signal for all models at once without explicitl Note that our newly created function has instance and class of the instance so you can easily run database queries inside your receivers if you want to. -```Python hl_lines="15-22" +```Python hl_lines="41-48" --8<-- "../docs_src/signals/docs002.py" ``` @@ -75,15 +75,15 @@ You can define same receiver for multiple models at once by passing a list of mo # define a dummy debug function @pre_update([Album, Track]) async def before_update(sender, instance, **kwargs): - print(f"{sender.get_name()}: {instance.json()}: {kwargs}") + print(f"{sender.get_name()}: {instance.model_dump_json()}: {kwargs}") ``` -Of course you can also create multiple functions for the same signal and model. Each of them will run at each signal. +Of course, you can also create multiple functions for the same signal and model. Each of them will run at each signal. ```python @pre_update(Album) async def before_update(sender, instance, **kwargs): - print(f"{sender.get_name()}: {instance.json()}: {kwargs}") + print(f"{sender.get_name()}: {instance.model_dump_json()}: {kwargs}") @pre_update(Album) async def before_update2(sender, instance, **kwargs): @@ -100,13 +100,13 @@ class AlbumAuditor: async def before_save(self, sender, instance, **kwargs): await AuditLog( - event_type=f"{self.event_type}_SAVE", event_log=instance.json() + event_type=f"{self.event_type}_SAVE", event_log=instance.model_dump_json() ).save() auditor = AlbumAuditor() pre_save(Album)(auditor.before_save) # call above has same result like the one below -Album.Meta.signals.pre_save.connect(auditor.before_save) +Album.ormar_config.signals.pre_save.connect(auditor.before_save) # signals are also exposed on instance album = Album(name='Miami') album.signals.pre_save.connect(auditor.before_save) @@ -127,7 +127,7 @@ async def before_update(sender, instance, **kwargs): instance.is_best_seller = True # disconnect given function from signal for given Model -Album.Meta.signals.pre_save.disconnect(before_save) +Album.ormar_config.signals.pre_save.disconnect(before_save) # signals are also exposed on instance album = Album(name='Miami') album.signals.pre_save.disconnect(before_save) @@ -142,7 +142,7 @@ album.signals.pre_save.disconnect(before_save) * bulk operations (`QuerySet.bulk_create` and `QuerySet.bulk_update`) as they are designed for speed. * queryset table level operations (`QuerySet.update` and `QuerySet.delete`) as they run on the underlying tables - (more lak raw sql update/delete operations) and do not have specific instance. + (more like raw sql update/delete operations) and do not have specific instance. ### pre_save @@ -251,23 +251,23 @@ import sqlalchemy import ormar -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() + +base_ormar_config = ormar.OrmarConfig( + database=databases.Database("sqlite:///db.sqlite"), + metadata=sqlalchemy.MetaData(), +) class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) is_best_seller: bool = ormar.Boolean(default=False) play_count: int = ormar.Integer(default=0) -Album.Meta.signals.your_custom_signal = ormar.Signal() -Album.Meta.signals.your_custom_signal.connect(your_receiver_name) +Album.ormar_config.signals.your_custom_signal = ormar.Signal() +Album.ormar_config.signals.your_custom_signal.connect(your_receiver_name) ``` Actually under the hood signal is a `SignalEmitter` instance that keeps a dictionary of know signals, and allows you @@ -276,13 +276,13 @@ to access them as attributes. When you try to access a signal that does not exis So example above can be simplified to. The `Signal` will be created for you. ``` -Album.Meta.signals.your_custom_signal.connect(your_receiver_name) +Album.ormar_config.signals.your_custom_signal.connect(your_receiver_name) ``` Now to trigger this signal you need to call send method of the Signal. ```python -await Album.Meta.signals.your_custom_signal.send(sender=Album) +await Album.ormar_config.signals.your_custom_signal.send(sender=Album) ``` Note that sender is the only required parameter and it should be ormar Model class. @@ -290,6 +290,6 @@ Note that sender is the only required parameter and it should be ormar Model cla Additional parameters have to be passed as keyword arguments. ```python -await Album.Meta.signals.your_custom_signal.send(sender=Album, my_param=True) +await Album.ormar_config.signals.your_custom_signal.send(sender=Album, my_param=True) ``` diff --git a/docs/transactions.md b/docs/transactions.md index b764192..d4d28a2 100644 --- a/docs/transactions.md +++ b/docs/transactions.md @@ -15,10 +15,10 @@ async with database.transaction(): ``` !!!note - Note that it has to be the same `database` that the one used in Model's `Meta` class. + Note that it has to be the same `database` that the one used in Model's `ormar_config` object. To avoid passing `database` instance around in your code you can extract the instance from each `Model`. -Database provided during declaration of `ormar.Model` is available through `Meta.database` and can +Database provided during declaration of `ormar.Model` is available through `ormar_config.database` and can be reached from both class and instance. ```python @@ -26,24 +26,25 @@ import databases import sqlalchemy import ormar -metadata = sqlalchemy.MetaData() -database = databases.Database("sqlite:///") + +base_ormar_config = OrmarConfig( + metadata=sqlalchemy.MetaData(), + database = databases.Database("sqlite:///"), +) + class Author(ormar.Model): - class Meta: - database=database - metadata=metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=255) # database is accessible from class -database = Author.Meta.database +database = Author.ormar_config.database # as well as from instance author = Author(name="Stephen King") -database = author.Meta.database - +database = author.ormar_config.database ``` You can also use `.transaction()` as a function decorator on any async function: diff --git a/docs_src/aggregations/docs001.py b/docs_src/aggregations/docs001.py index bc81e04..c92b459 100644 --- a/docs_src/aggregations/docs001.py +++ b/docs_src/aggregations/docs001.py @@ -1,33 +1,32 @@ from typing import Optional import databases -import sqlalchemy - import ormar +import sqlalchemy from tests.settings import DATABASE_URL database = databases.Database(DATABASE_URL) metadata = sqlalchemy.MetaData() -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = ormar.OrmarConfig( + metadata=metadata, + database=database, +) class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" - order_by = ["-name"] + ormar_config = base_ormar_config.copy(tablename="authors", order_by=["-name"]) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" - order_by = ["year", "-ranking"] + + ormar_config = base_ormar_config.copy( + tablename="books", order_by=["year", "-ranking"] + ) id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey(Author) diff --git a/docs_src/fastapi/docs001.py b/docs_src/fastapi/docs001.py index 4d2087c..a290a95 100644 --- a/docs_src/fastapi/docs001.py +++ b/docs_src/fastapi/docs001.py @@ -1,46 +1,23 @@ from typing import List, Optional -import databases -import sqlalchemy -from fastapi import FastAPI - import ormar +from fastapi import FastAPI +from tests.lifespan import lifespan +from tests.settings import create_config -app = FastAPI() -metadata = sqlalchemy.MetaData() -database = databases.Database("sqlite:///test.db") -app.state.database = database - - -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="items") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -68,7 +45,7 @@ async def create_category(category: Category): @app.put("/items/{item_id}") async def get_item(item_id: int, item: Item): item_db = await Item.objects.get(pk=item_id) - return await item_db.update(**item.dict()) + return await item_db.update(**item.model_dump()) @app.delete("/items/{item_id}") diff --git a/docs_src/fastapi/mypy/docs001.py b/docs_src/fastapi/mypy/docs001.py index 2c74a77..47b87a0 100644 --- a/docs_src/fastapi/mypy/docs001.py +++ b/docs_src/fastapi/mypy/docs001.py @@ -1,16 +1,16 @@ import databases -import sqlalchemy - import ormar +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id = ormar.Integer(primary_key=True) name = ormar.String(max_length=100) diff --git a/docs_src/fields/docs001.py b/docs_src/fields/docs001.py index d1c9144..3d17a30 100644 --- a/docs_src/fields/docs001.py +++ b/docs_src/fields/docs001.py @@ -1,27 +1,27 @@ +import asyncio from typing import Optional import databases -import sqlalchemy - import ormar +import sqlalchemy +from examples import create_drop_database -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() +) class Department(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar_base_config.copy(tablename="departments") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar_base_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -29,12 +29,17 @@ class Course(ormar.Model): department: Optional[Department] = ormar.ForeignKey(Department) -department = await Department(name="Science").save() -course = Course(name="Math", completed=False, department=department) +@create_drop_database(base_config=ormar_base_config) +async def verify(): + department = await Department(name="Science").save() + course = Course(name="Math", completed=False, department=department) + print(department.courses[0]) + # Will produce: + # Course(id=None, + # name='Math', + # completed=False, + # department=Department(id=None, name='Science')) + await course.save() -print(department.courses[0]) -# Will produce: -# Course(id=None, -# name='Math', -# completed=False, -# department=Department(id=None, name='Science')) + +asyncio.run(verify()) diff --git a/docs_src/fields/docs002.py b/docs_src/fields/docs002.py index 2432856..505eef0 100644 --- a/docs_src/fields/docs002.py +++ b/docs_src/fields/docs002.py @@ -1,32 +1,32 @@ from typing import Optional import databases +import ormar import sqlalchemy -import ormar +DATABASE_URL = "sqlite:///test.db" -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() +) class Department(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar_base_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar_base_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) completed: bool = ormar.Boolean(default=False) - department: Optional[Department] = ormar.ForeignKey(Department, related_name="my_courses") + department: Optional[Department] = ormar.ForeignKey( + Department, related_name="my_courses" + ) department = Department(name="Science") diff --git a/docs_src/fields/docs003.py b/docs_src/fields/docs003.py index 5066f60..cfd5836 100644 --- a/docs_src/fields/docs003.py +++ b/docs_src/fields/docs003.py @@ -1,27 +1,28 @@ from typing import Optional import databases -import sqlalchemy - import ormar +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Department(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) diff --git a/docs_src/fields/docs004.py b/docs_src/fields/docs004.py index 8a4e1ab..cb70a2a 100644 --- a/docs_src/fields/docs004.py +++ b/docs_src/fields/docs004.py @@ -1,20 +1,19 @@ from datetime import datetime import databases +import ormar import sqlalchemy from sqlalchemy import func, text -import ormar - database = databases.Database("sqlite:///test.db") metadata = sqlalchemy.MetaData() class Product(ormar.Model): - class Meta: - tablename = "product" - metadata = metadata - database = database + + ormar_config = ormar.OrmarConfig( + database=database, metadata=metadata, tablename="product" + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) diff --git a/docs_src/models/docs001.py b/docs_src/models/docs001.py index 36d7fd9..c93c767 100644 --- a/docs_src/models/docs001.py +++ b/docs_src/models/docs001.py @@ -1,16 +1,16 @@ import databases -import sqlalchemy - import ormar +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) diff --git a/docs_src/models/docs002.py b/docs_src/models/docs002.py index 3450dae..87a72cd 100644 --- a/docs_src/models/docs002.py +++ b/docs_src/models/docs002.py @@ -1,19 +1,20 @@ import databases -import sqlalchemy - import ormar +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Course(ormar.Model): - class Meta: + + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, # if you omit this parameter it will be created automatically # as class.__name__.lower()+'s' -> "courses" in this example - tablename = "my_courses" - database = database - metadata = metadata + tablename="my_courses", + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) diff --git a/docs_src/models/docs003.py b/docs_src/models/docs003.py index e79bfba..c5b7ff3 100644 --- a/docs_src/models/docs003.py +++ b/docs_src/models/docs003.py @@ -1,34 +1,34 @@ import databases -import sqlalchemy - import ormar +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) completed: bool = ormar.Boolean(default=False) -print(Course.__fields__) +print(Course.model_fields) """ Will produce: -{'id': ModelField(name='id', +{'id': Field(name='id', type=Optional[int], required=False, default=None), - 'name': ModelField(name='name', + 'name': Field(name='name', type=Optional[str], required=False, default=None), -'completed': ModelField(name='completed', +'completed': Field(name='completed', type=bool, required=False, default=False)} diff --git a/docs_src/models/docs004.py b/docs_src/models/docs004.py index cc8bce8..0d714e9 100644 --- a/docs_src/models/docs004.py +++ b/docs_src/models/docs004.py @@ -1,24 +1,28 @@ import databases +import ormar import sqlalchemy -import ormar +DATABASE_URL = "sqlite:///test.db" -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() +) class Course(ormar.Model): - class Meta(ormar.ModelMeta): # note you don't have to subclass - but it's recommended for ide completion and mypy - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + tablename="courses", + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) completed: bool = ormar.Boolean(default=False) -print(Course.Meta.table.columns) +print(Course.ormar_config.table.columns) """ Will produce: -['courses.id', 'courses.name', 'courses.completed'] +ImmutableColumnCollection(courses.id, courses.name, courses.completed) """ diff --git a/docs_src/models/docs005.py b/docs_src/models/docs005.py index 5359606..0d309eb 100644 --- a/docs_src/models/docs005.py +++ b/docs_src/models/docs005.py @@ -1,67 +1,144 @@ -import databases -import sqlalchemy +import pprint +import databases import ormar +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Course(ormar.Model): - class Meta(ormar.ModelMeta): - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) completed: bool = ormar.Boolean(default=False) -print({x: v.__dict__ for x, v in Course.Meta.model_fields.items()}) +pprint.pp({x: v.__dict__ for x, v in Course.ormar_config.model_fields.items()}) """ Will produce: -{'completed': mappingproxy({'autoincrement': False, - 'choices': set(), - 'column_type': Boolean(), - 'default': False, - 'index': False, - 'name': 'completed', - 'nullable': True, - 'primary_key': False, - 'pydantic_only': False, - 'server_default': None, - 'unique': False}), - 'id': mappingproxy({'autoincrement': True, - 'choices': set(), - 'column_type': Integer(), - 'default': None, - 'ge': None, - 'index': False, - 'le': None, - 'maximum': None, - 'minimum': None, - 'multiple_of': None, - 'name': 'id', - 'nullable': False, - 'primary_key': True, - 'pydantic_only': False, - 'server_default': None, - 'unique': False}), - 'name': mappingproxy({'allow_blank': False, - 'autoincrement': False, - 'choices': set(), - 'column_type': String(max_length=100), - 'curtail_length': None, - 'default': None, - 'index': False, - 'max_length': 100, - 'min_length': None, - 'name': 'name', - 'nullable': False, - 'primary_key': False, - 'pydantic_only': False, - 'regex': None, - 'server_default': None, - 'strip_whitespace': False, - 'unique': False})} +{'id': {'__type__': , + '__pydantic_type__': , + '__sample__': 0, + 'related_name': None, + 'column_type': Integer(), + 'constraints': [], + 'name': 'id', + 'db_alias': None, + 'primary_key': True, + 'autoincrement': True, + 'nullable': True, + 'sql_nullable': False, + 'index': False, + 'unique': False, + 'virtual': None, + 'is_multi': None, + 'is_relation': None, + 'is_through': False, + 'through_relation_name': None, + 'through_reverse_relation_name': None, + 'skip_reverse': False, + 'skip_field': False, + 'owner': , + 'to': None, + 'to_pk_only': None, + 'through': None, + 'self_reference': False, + 'self_reference_primary': None, + 'orders_by': None, + 'related_orders_by': None, + 'encrypt_secret': None, + 'encrypt_backend': , + 'encrypt_custom_backend': None, + 'ormar_default': None, + 'server_default': None, + 'comment': None, + 'represent_as_base64_str': False, + 'minimum': None, + 'maximum': None, + 'multiple_of': None, + 'ge': None, + 'le': None}, + 'name': {'__type__': , + '__pydantic_type__': , + '__sample__': 'string', + 'related_name': None, + 'column_type': String(length=100), + 'constraints': [], + 'name': 'name', + 'db_alias': None, + 'primary_key': False, + 'autoincrement': False, + 'nullable': False, + 'sql_nullable': False, + 'index': False, + 'unique': False, + 'virtual': None, + 'is_multi': None, + 'is_relation': None, + 'is_through': False, + 'through_relation_name': None, + 'through_reverse_relation_name': None, + 'skip_reverse': False, + 'skip_field': False, + 'owner': , + 'to': None, + 'to_pk_only': None, + 'through': None, + 'self_reference': False, + 'self_reference_primary': None, + 'orders_by': None, + 'related_orders_by': None, + 'encrypt_secret': None, + 'encrypt_backend': , + 'encrypt_custom_backend': None, + 'ormar_default': None, + 'server_default': None, + 'comment': None, + 'represent_as_base64_str': False, + 'max_length': 100, + 'min_length': None, + 'regex': None}, + 'completed': {'__type__': , + '__pydantic_type__': , + '__sample__': True, + 'related_name': None, + 'column_type': Boolean(), + 'constraints': [], + 'name': 'completed', + 'db_alias': None, + 'primary_key': False, + 'autoincrement': False, + 'nullable': True, + 'sql_nullable': True, + 'index': False, + 'unique': False, + 'virtual': None, + 'is_multi': None, + 'is_relation': None, + 'is_through': False, + 'through_relation_name': None, + 'through_reverse_relation_name': None, + 'skip_reverse': False, + 'skip_field': False, + 'owner': , + 'to': None, + 'to_pk_only': None, + 'through': None, + 'self_reference': False, + 'self_reference_primary': None, + 'orders_by': None, + 'related_orders_by': None, + 'encrypt_secret': None, + 'encrypt_backend': , + 'encrypt_custom_backend': None, + 'ormar_default': False, + 'server_default': None, + 'comment': None, + 'represent_as_base64_str': False}} """ diff --git a/docs_src/models/docs006.py b/docs_src/models/docs006.py index 7649c2c..535aa13 100644 --- a/docs_src/models/docs006.py +++ b/docs_src/models/docs006.py @@ -1,20 +1,20 @@ import databases -import sqlalchemy - import ormar +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Course(ormar.Model): - class Meta: - database = database - metadata = metadata - # define your constraints in Meta class of the model + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + # define your constraints in OrmarConfig of the model # it's a list that can contain multiple constraints # hera a combination of name and column will have to be unique in db - constraints = [ormar.UniqueColumns("name", "completed")] + constraints=[ormar.UniqueColumns("name", "completed")], + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) diff --git a/docs_src/models/docs007.py b/docs_src/models/docs007.py index 9dfe28a..ed408cc 100644 --- a/docs_src/models/docs007.py +++ b/docs_src/models/docs007.py @@ -1,23 +1,31 @@ +import asyncio + import databases -import sqlalchemy - import ormar +import sqlalchemy +from examples import create_drop_database -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() +) class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar_base_config.copy(tablename="courses") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) completed: bool = ormar.Boolean(default=False) -course = Course(name="Painting for dummies", completed=False) -await course.save() +@create_drop_database(base_config=ormar_base_config) +async def run_query(): + course = Course(name="Painting for dummies", completed=False) + await course.save() -await Course.objects.create(name="Painting for dummies", completed=False) + await Course.objects.create(name="Painting for dummies", completed=False) + + +asyncio.run(run_query()) diff --git a/docs_src/models/docs008.py b/docs_src/models/docs008.py index da2444c..0fe87a6 100644 --- a/docs_src/models/docs008.py +++ b/docs_src/models/docs008.py @@ -1,17 +1,19 @@ import databases +import ormar import sqlalchemy -import ormar +DATABASE_URl = "sqlite:///test.db" -database = databases.Database("sqlite:///test.db", force_rollback=True) +database = databases.Database(DATABASE_URl, force_rollback=True) metadata = sqlalchemy.MetaData() class Child(ormar.Model): - class Meta: - tablename = "children" - metadata = metadata - database = database + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="children", + ) id: int = ormar.Integer(name="child_id", primary_key=True) first_name: str = ormar.String(name="fname", max_length=100) diff --git a/docs_src/models/docs009.py b/docs_src/models/docs009.py index 747f612..9f7b753 100644 --- a/docs_src/models/docs009.py +++ b/docs_src/models/docs009.py @@ -1,20 +1,33 @@ from typing import Optional import databases -import sqlalchemy - import ormar -from .docs010 import Artist # previous example +import sqlalchemy database = databases.Database("sqlite:///test.db", force_rollback=True) metadata = sqlalchemy.MetaData() +class Artist(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="artists", + ) + + id: int = ormar.Integer(name="artist_id", primary_key=True) + first_name: str = ormar.String(name="fname", max_length=100) + last_name: str = ormar.String(name="lname", max_length=100) + born_year: int = ormar.Integer(name="year") + + class Album(ormar.Model): - class Meta: - tablename = "music_albums" - metadata = metadata - database = database + + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="music_albums", + ) id: int = ormar.Integer(name="album_id", primary_key=True) name: str = ormar.String(name="album_name", max_length=100) diff --git a/docs_src/models/docs010.py b/docs_src/models/docs010.py index d63101e..63cb712 100644 --- a/docs_src/models/docs010.py +++ b/docs_src/models/docs010.py @@ -1,25 +1,40 @@ import databases +import ormar import sqlalchemy -import ormar -from .docs008 import Child +DATABASE_URl = "sqlite:///test.db" -database = databases.Database("sqlite:///test.db", force_rollback=True) +database = databases.Database(DATABASE_URl, force_rollback=True) metadata = sqlalchemy.MetaData() +class Child(ormar.Model): + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="children", + ) + + id: int = ormar.Integer(name="child_id", primary_key=True) + first_name: str = ormar.String(name="fname", max_length=100) + last_name: str = ormar.String(name="lname", max_length=100) + born_year: int = ormar.Integer(name="year_born", nullable=True) + + class ArtistChildren(ormar.Model): - class Meta: - tablename = "children_x_artists" - metadata = metadata - database = database + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="children_x_artists", + ) class Artist(ormar.Model): - class Meta: - tablename = "artists" - metadata = metadata - database = database + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + tablename="artists", + ) id: int = ormar.Integer(name="artist_id", primary_key=True) first_name: str = ormar.String(name="fname", max_length=100) diff --git a/docs_src/models/docs011.py b/docs_src/models/docs011.py deleted file mode 100644 index 962de1d..0000000 --- a/docs_src/models/docs011.py +++ /dev/null @@ -1,19 +0,0 @@ -import databases -import sqlalchemy - -import ormar - -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() - - -class Course(ormar.Model): - class Meta: - database = database - metadata = metadata - - id: ormar.Integer(primary_key=True) - name: ormar.String(max_length=100) - completed: ormar.Boolean(default=False) - -c1 = Course() \ No newline at end of file diff --git a/docs_src/models/docs012.py b/docs_src/models/docs012.py index 2c74a77..47b87a0 100644 --- a/docs_src/models/docs012.py +++ b/docs_src/models/docs012.py @@ -1,16 +1,16 @@ import databases -import sqlalchemy - import ormar +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id = ormar.Integer(primary_key=True) name = ormar.String(max_length=100) diff --git a/docs_src/models/docs013.py b/docs_src/models/docs013.py index 8d584f3..2aa3958 100644 --- a/docs_src/models/docs013.py +++ b/docs_src/models/docs013.py @@ -1,27 +1,22 @@ from typing import Optional import databases +import ormar import sqlalchemy -import ormar +DATABASE_URL = "sqlite:///test.db" -database = databases.Database("sqlite:///test.db", force_rollback=True) -metadata = sqlalchemy.MetaData() - - -# note that you do not have to subclass ModelMeta, -# it's useful for type hints and code completion -class MainMeta(ormar.ModelMeta): - metadata = metadata - database = database +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) class Artist(ormar.Model): - class Meta(MainMeta): - # note that tablename is optional - # if not provided ormar will user class.__name__.lower()+'s' - # -> artists in this example - pass + # note that tablename is optional + # if not provided ormar will user class.__name__.lower()+'s' + # -> artists in this example + ormar_config = ormar_base_config.copy() id: int = ormar.Integer(primary_key=True) first_name: str = ormar.String(max_length=100) @@ -30,8 +25,7 @@ class Artist(ormar.Model): class Album(ormar.Model): - class Meta(MainMeta): - pass + ormar_config = ormar_base_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) diff --git a/docs_src/models/docs014.py b/docs_src/models/docs014.py index fc02ddb..5f5a573 100644 --- a/docs_src/models/docs014.py +++ b/docs_src/models/docs014.py @@ -1,18 +1,19 @@ import databases -import sqlalchemy - import ormar +import pydantic +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) completed: bool = ormar.Boolean(default=False) - non_db_field: str = ormar.String(max_length=100, pydantic_only=True) + non_db_field: str = pydantic.Field(max_length=100) diff --git a/docs_src/models/docs015.py b/docs_src/models/docs015.py index f3053b8..a6d6716 100644 --- a/docs_src/models/docs015.py +++ b/docs_src/models/docs015.py @@ -1,22 +1,21 @@ import databases -import sqlalchemy - import ormar -from ormar import property_field +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) completed: bool = ormar.Boolean(default=False) - @property_field + @property def prefixed_name(self): return "custom_prefix__" + self.name diff --git a/docs_src/models/docs016.py b/docs_src/models/docs016.py index 5f9d71c..6d456a4 100644 --- a/docs_src/models/docs016.py +++ b/docs_src/models/docs016.py @@ -1,19 +1,19 @@ import databases -import sqlalchemy - import ormar +import pydantic +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) - class Config: - allow_mutation = False + model_config = pydantic.ConfigDict(frozen=True) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) diff --git a/docs_src/models/docs017.py b/docs_src/models/docs017.py index b1ba5e5..464b1eb 100644 --- a/docs_src/models/docs017.py +++ b/docs_src/models/docs017.py @@ -1,20 +1,20 @@ import databases -import sqlalchemy - import ormar +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Course(ormar.Model): - class Meta: - database = database - metadata = metadata - # define your constraints in Meta class of the model + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + # define your constraints in OrmarConfig of the model # it's a list that can contain multiple constraints # hera a combination of name and column will have a compound index in the db - constraints = [ormar.IndexColumns("name", "completed")] + constraints=[ormar.IndexColumns("name", "completed")], + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) diff --git a/docs_src/models/docs018.py b/docs_src/models/docs018.py index d3aee4e..7ef2988 100644 --- a/docs_src/models/docs018.py +++ b/docs_src/models/docs018.py @@ -1,23 +1,24 @@ import datetime -import databases -import sqlalchemy +import databases import ormar +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Course(ormar.Model): - class Meta: - database = database - metadata = metadata - # define your constraints in Meta class of the model + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + # define your constraints in OrmarConfig of the model # it's a list that can contain multiple constraints # hera a combination of name and column will have a level check in the db - constraints = [ + constraints=[ ormar.CheckColumns("start_time < end_time", name="date_check"), - ] + ], + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) diff --git a/docs_src/queries/docs001.py b/docs_src/queries/docs001.py index 850030e..dd38ac8 100644 --- a/docs_src/queries/docs001.py +++ b/docs_src/queries/docs001.py @@ -4,25 +4,23 @@ import databases import ormar import sqlalchemy -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) class Album(ormar.Model): - class Meta: - tablename = "album" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy(tablename="album") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Track(ormar.Model): - class Meta: - tablename = "track" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy(tablename="track") id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) diff --git a/docs_src/queries/docs002.py b/docs_src/queries/docs002.py index d9cdeff..d30e13d 100644 --- a/docs_src/queries/docs002.py +++ b/docs_src/queries/docs002.py @@ -1,28 +1,47 @@ +import asyncio + import databases import ormar import sqlalchemy +from examples import create_drop_database -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) class Book(ormar.Model): - class Meta: - tablename = "books" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy( + tablename="books", + ) id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) author: str = ormar.String(max_length=100) - genre: str = ormar.String(max_length=100, default='Fiction', - choices=['Fiction', 'Adventure', 'Historic', 'Fantasy']) + genre: str = ormar.String( + max_length=100, + default="Fiction", + ) -await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure') -await Book.objects.create(title='War and Peace', author="Tolstoy, Leo", genre='Fiction') -await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction') +@create_drop_database(base_config=ormar_base_config) +async def run_query(): + await Book.objects.create( + title="Tom Sawyer", author="Twain, Mark", genre="Adventure" + ) + await Book.objects.create( + title="War and Peace", author="Tolstoy, Leo", genre="Fiction" + ) + await Book.objects.create( + title="Anna Karenina", author="Tolstoy, Leo", genre="Fiction" + ) -await Book.objects.update(each=True, genre='Fiction') -all_books = await Book.objects.filter(genre='Fiction').all() -assert len(all_books) == 3 + await Book.objects.update(each=True, genre="Fiction") + all_books = await Book.objects.filter(genre="Fiction").all() + assert len(all_books) == 3 + + +asyncio.run(run_query()) diff --git a/docs_src/queries/docs003.py b/docs_src/queries/docs003.py index 58e4f1b..1c1e0ae 100644 --- a/docs_src/queries/docs003.py +++ b/docs_src/queries/docs003.py @@ -1,32 +1,51 @@ +import asyncio + import databases import ormar import sqlalchemy +from examples import create_drop_database -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) class Book(ormar.Model): - class Meta: - tablename = "books" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy() id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) author: str = ormar.String(max_length=100) - genre: str = ormar.String(max_length=100, default='Fiction', - choices=['Fiction', 'Adventure', 'Historic', 'Fantasy']) + genre: str = ormar.String( + max_length=100, + default="Fiction", + ) -await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure') -await Book.objects.create(title='War and Peace', author="Tolstoy, Leo", genre='Fiction') -await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction') +@create_drop_database(base_config=ormar_base_config) +async def run_query(): + await Book.objects.create( + title="Tom Sawyer", author="Twain, Mark", genre="Adventure" + ) + await Book.objects.create( + title="War and Peace", author="Tolstoy, Leo", genre="Fiction" + ) + await Book.objects.create( + title="Anna Karenina", author="Tolstoy, Leo", genre="Fiction" + ) -# if not exist the instance will be persisted in db -vol2 = await Book.objects.update_or_create(title="Volume II", author='Anonymous', genre='Fiction') -assert await Book.objects.count() == 1 + # if not exist the instance will be persisted in db + vol2 = await Book.objects.update_or_create( + title="Volume II", author="Anonymous", genre="Fiction" + ) + assert await Book.objects.count() == 4 -# if pk or pkname passed in kwargs (like id here) the object will be updated -assert await Book.objects.update_or_create(id=vol2.id, genre='Historic') -assert await Book.objects.count() == 1 + # if pk or pkname passed in kwargs (like id here) the object will be updated + assert await Book.objects.update_or_create(id=vol2.id, genre="Historic") + assert await Book.objects.count() == 4 + + +asyncio.run(run_query()) diff --git a/docs_src/queries/docs004.py b/docs_src/queries/docs004.py index 6aaf01f..4697409 100644 --- a/docs_src/queries/docs004.py +++ b/docs_src/queries/docs004.py @@ -1,30 +1,39 @@ +import asyncio + import databases import ormar import sqlalchemy +from examples import create_drop_database -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) class ToDo(ormar.Model): - class Meta: - tablename = "todos" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy(tablename="todos") id: int = ormar.Integer(primary_key=True) text: str = ormar.String(max_length=500) completed = ormar.Boolean(default=False) -# create multiple instances at once with bulk_create -await ToDo.objects.bulk_create( - [ - ToDo(text="Buy the groceries."), - ToDo(text="Call Mum.", completed=True), - ToDo(text="Send invoices.", completed=True), - ] -) +@create_drop_database(base_config=ormar_base_config) +async def run_query(): + # create multiple instances at once with bulk_create + await ToDo.objects.bulk_create( + [ + ToDo(text="Buy the groceries."), + ToDo(text="Call Mum.", completed=True), + ToDo(text="Send invoices.", completed=True), + ] + ) -todoes = await ToDo.objects.all() -assert len(todoes) == 3 + todoes = await ToDo.objects.all() + assert len(todoes) == 3 + + +asyncio.run(run_query()) diff --git a/docs_src/queries/docs005.py b/docs_src/queries/docs005.py index dca0757..5f3acca 100644 --- a/docs_src/queries/docs005.py +++ b/docs_src/queries/docs005.py @@ -1,30 +1,47 @@ +import asyncio + import databases import ormar import sqlalchemy +from examples import create_drop_database -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) class Book(ormar.Model): - class Meta: - tablename = "books" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy(tablename="books") id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) author: str = ormar.String(max_length=100) - genre: str = ormar.String(max_length=100, default='Fiction', - choices=['Fiction', 'Adventure', 'Historic', 'Fantasy']) + genre: str = ormar.String( + max_length=100, + default="Fiction", + ) -await Book.objects.create(title='Tom Sawyer', author="Twain, Mark", genre='Adventure') -await Book.objects.create(title='War and Peace in Space', author="Tolstoy, Leo", genre='Fantasy') -await Book.objects.create(title='Anna Karenina', author="Tolstoy, Leo", genre='Fiction') +@create_drop_database(base_config=ormar_base_config) +async def run_query(): + await Book.objects.create( + title="Tom Sawyer", author="Twain, Mark", genre="Adventure" + ) + await Book.objects.create( + title="War and Peace in Space", author="Tolstoy, Leo", genre="Fantasy" + ) + await Book.objects.create( + title="Anna Karenina", author="Tolstoy, Leo", genre="Fiction" + ) -# delete accepts kwargs that will be used in filter -# acting in same way as queryset.filter(**kwargs).delete() -await Book.objects.delete(genre='Fantasy') # delete all fantasy books -all_books = await Book.objects.all() -assert len(all_books) == 2 + # delete accepts kwargs that will be used in filter + # acting in same way as queryset.filter(**kwargs).delete() + await Book.objects.delete(genre="Fantasy") # delete all fantasy books + all_books = await Book.objects.all() + assert len(all_books) == 2 + + +asyncio.run(run_query()) diff --git a/docs_src/queries/docs006.py b/docs_src/queries/docs006.py index 13143d2..f5b9d52 100644 --- a/docs_src/queries/docs006.py +++ b/docs_src/queries/docs006.py @@ -1,18 +1,20 @@ +import asyncio + import databases -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import sqlalchemy +from examples import create_drop_database -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) class Company(ormar.Model): - class Meta: - tablename = "companies" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy(tablename="companies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -20,10 +22,7 @@ class Company(ormar.Model): class Car(ormar.Model): - class Meta: - tablename = "cars" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy(tablename="cars") id: int = ormar.Integer(primary_key=True) manufacturer = ormar.ForeignKey(Company) @@ -34,12 +33,34 @@ class Car(ormar.Model): aircon_type: str = ormar.String(max_length=20, nullable=True) -# build some sample data -toyota = await Company.objects.create(name="Toyota", founded=1937) -await Car.objects.create(manufacturer=toyota, name="Corolla", year=2020, gearbox_type='Manual', gears=5, - aircon_type='Manual') -await Car.objects.create(manufacturer=toyota, name="Yaris", year=2019, gearbox_type='Manual', gears=5, - aircon_type='Manual') -await Car.objects.create(manufacturer=toyota, name="Supreme", year=2020, gearbox_type='Auto', gears=6, - aircon_type='Auto') +@create_drop_database(base_config=ormar_base_config) +async def run_query(): + # build some sample data + toyota = await Company.objects.create(name="Toyota", founded=1937) + await Car.objects.create( + manufacturer=toyota, + name="Corolla", + year=2020, + gearbox_type="Manual", + gears=5, + aircon_type="Manual", + ) + await Car.objects.create( + manufacturer=toyota, + name="Yaris", + year=2019, + gearbox_type="Manual", + gears=5, + aircon_type="Manual", + ) + await Car.objects.create( + manufacturer=toyota, + name="Supreme", + year=2020, + gearbox_type="Auto", + gears=6, + aircon_type="Auto", + ) + +asyncio.run(run_query()) diff --git a/docs_src/queries/docs007.py b/docs_src/queries/docs007.py index 7e8ddc0..b1a09fc 100644 --- a/docs_src/queries/docs007.py +++ b/docs_src/queries/docs007.py @@ -1,42 +1,46 @@ +import asyncio + import databases -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import sqlalchemy +from examples import create_drop_database -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) class Owner(ormar.Model): - class Meta: - tablename = "owners" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy(tablename="owners") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Toy(ormar.Model): - class Meta: - tablename = "toys" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy(tablename="toys") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) owner: Owner = ormar.ForeignKey(Owner) -# build some sample data -aphrodite = await Owner.objects.create(name="Aphrodite") -hermes = await Owner.objects.create(name="Hermes") -zeus = await Owner.objects.create(name="Zeus") +@create_drop_database(base_config=ormar_base_config) +async def run_query(): + # build some sample data + aphrodite = await Owner.objects.create(name="Aphrodite") + hermes = await Owner.objects.create(name="Hermes") + zeus = await Owner.objects.create(name="Zeus") -await Toy.objects.create(name="Toy 4", owner=zeus) -await Toy.objects.create(name="Toy 5", owner=hermes) -await Toy.objects.create(name="Toy 2", owner=aphrodite) -await Toy.objects.create(name="Toy 1", owner=zeus) -await Toy.objects.create(name="Toy 3", owner=aphrodite) -await Toy.objects.create(name="Toy 6", owner=hermes) + await Toy.objects.create(name="Toy 4", owner=zeus) + await Toy.objects.create(name="Toy 5", owner=hermes) + await Toy.objects.create(name="Toy 2", owner=aphrodite) + await Toy.objects.create(name="Toy 1", owner=zeus) + await Toy.objects.create(name="Toy 3", owner=aphrodite) + await Toy.objects.create(name="Toy 6", owner=hermes) + + +asyncio.run(run_query()) diff --git a/docs_src/queries/docs008.py b/docs_src/queries/docs008.py index 511e1d7..814a0b9 100644 --- a/docs_src/queries/docs008.py +++ b/docs_src/queries/docs008.py @@ -1,18 +1,21 @@ +import asyncio + import databases -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import sqlalchemy +from examples import create_drop_database +from pydantic import ValidationError -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) class Company(ormar.Model): - class Meta: - tablename = "companies" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy(tablename="companies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -20,10 +23,7 @@ class Company(ormar.Model): class Car(ormar.Model): - class Meta: - tablename = "cars" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy(tablename="cars") id: int = ormar.Integer(primary_key=True) manufacturer = ormar.ForeignKey(Company) @@ -34,35 +34,81 @@ class Car(ormar.Model): aircon_type: str = ormar.String(max_length=20, nullable=True) -# build some sample data -toyota = await Company.objects.create(name="Toyota", founded=1937) -await Car.objects.create(manufacturer=toyota, name="Corolla", year=2020, gearbox_type='Manual', gears=5, - aircon_type='Manual') -await Car.objects.create(manufacturer=toyota, name="Yaris", year=2019, gearbox_type='Manual', gears=5, - aircon_type='Manual') -await Car.objects.create(manufacturer=toyota, name="Supreme", year=2020, gearbox_type='Auto', gears=6, - aircon_type='Auto') +@create_drop_database(base_config=ormar_base_config) +async def run_query(): + # build some sample data + toyota = await Company.objects.create(name="Toyota", founded=1937) + await Car.objects.create( + manufacturer=toyota, + name="Corolla", + year=2020, + gearbox_type="Manual", + gears=5, + aircon_type="Manual", + ) + await Car.objects.create( + manufacturer=toyota, + name="Yaris", + year=2019, + gearbox_type="Manual", + gears=5, + aircon_type="Manual", + ) + await Car.objects.create( + manufacturer=toyota, + name="Supreme", + year=2020, + gearbox_type="Auto", + gears=6, + aircon_type="Auto", + ) -# select manufacturer but only name - to include related models use notation {model_name}__{column} -all_cars = await Car.objects.select_related('manufacturer').exclude_fields( - ['year', 'gearbox_type', 'gears', 'aircon_type', 'company__founded']).all() -for car in all_cars: - # excluded columns will yield None - assert all(getattr(car, x) is None for x in ['year', 'gearbox_type', 'gears', 'aircon_type']) - # included column on related models will be available, pk column is always included - # even if you do not include it in fields list - assert car.manufacturer.name == 'Toyota' - # also in the nested related models - you cannot exclude pk - it's always auto added - assert car.manufacturer.founded is None + # select manufacturer but only name, + # to include related models use notation {model_name}__{column} + all_cars = ( + await Car.objects.select_related("manufacturer") + .exclude_fields( + ["year", "gearbox_type", "gears", "aircon_type", "manufacturer__founded"] + ) + .all() + ) + for car in all_cars: + # excluded columns will yield None + assert all( + getattr(car, x) is None + for x in ["year", "gearbox_type", "gears", "aircon_type"] + ) + # included column on related models will be available, + # pk column is always included + # even if you do not include it in fields list + assert car.manufacturer.name == "Toyota" + # also in the nested related models - + # you cannot exclude pk - it's always auto added + assert car.manufacturer.founded is None -# fields() can be called several times, building up the columns to select -# models selected in select_related but with no columns in fields list implies all fields -all_cars = await Car.objects.select_related('manufacturer').exclude_fields('year').exclude_fields( - ['gear', 'gearbox_type']).all() -# all fields from company model are selected -assert all_cars[0].manufacturer.name == 'Toyota' -assert all_cars[0].manufacturer.founded == 1937 + # fields() can be called several times, + # building up the columns to select, + # models selected in select_related + # but with no columns in fields list implies all fields + all_cars = ( + await Car.objects.select_related("manufacturer") + .exclude_fields("year") + .exclude_fields(["gear", "gearbox_type"]) + .all() + ) + # all fiels from company model are selected + assert all_cars[0].manufacturer.name == "Toyota" + assert all_cars[0].manufacturer.founded == 1937 -# cannot exclude mandatory model columns - company__name in this example - note usage of dict/set this time -await Car.objects.select_related('manufacturer').exclude_fields([{'company': {'name'}}]).all() -# will raise pydantic ValidationError as company.name is required + # cannot exclude mandatory model columns - + # manufacturer__name in this example - note usage of dict/set this time + try: + await Car.objects.select_related("manufacturer").exclude_fields( + {"manufacturer": {"name"}} + ).all() + except ValidationError: + # will raise pydantic ValidationError as company.name is required + pass + + +asyncio.run(run_query()) diff --git a/docs_src/queries/docs009.py b/docs_src/queries/docs009.py index 74ecc71..93ce404 100644 --- a/docs_src/queries/docs009.py +++ b/docs_src/queries/docs009.py @@ -1,33 +1,71 @@ -# 1. like in example above -await Car.objects.select_related('manufacturer').fields(['id', 'name', 'manufacturer__name']).all() +import asyncio -# 2. to mark a field as required use ellipsis -await Car.objects.select_related('manufacturer').fields({'id': ..., - 'name': ..., - 'manufacturer': { - 'name': ...} - }).all() +import databases +import ormar +import sqlalchemy +from examples import create_drop_database -# 3. to include whole nested model use ellipsis -await Car.objects.select_related('manufacturer').fields({'id': ..., - 'name': ..., - 'manufacturer': ... - }).all() +DATABASE_URL = "sqlite:///test.db" -# 4. to specify fields at last nesting level you can also use set - equivalent to 2. above -await Car.objects.select_related('manufacturer').fields({'id': ..., - 'name': ..., - 'manufacturer': {'name'} - }).all() +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) -# 5. of course set can have multiple fields -await Car.objects.select_related('manufacturer').fields({'id': ..., - 'name': ..., - 'manufacturer': {'name', 'founded'} - }).all() -# 6. you can include all nested fields but it will be equivalent of 3. above which is shorter -await Car.objects.select_related('manufacturer').fields({'id': ..., - 'name': ..., - 'manufacturer': {'id', 'name', 'founded'} - }).all() +class Company(ormar.Model): + ormar_config = ormar_base_config.copy(tablename="companies") + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + founded: int = ormar.Integer(nullable=True) + + +class Car(ormar.Model): + ormar_config = ormar_base_config.copy(tablename="cars") + + id: int = ormar.Integer(primary_key=True) + manufacturer = ormar.ForeignKey(Company) + name: str = ormar.String(max_length=100) + year: int = ormar.Integer(nullable=True) + gearbox_type: str = ormar.String(max_length=20, nullable=True) + gears: int = ormar.Integer(nullable=True) + aircon_type: str = ormar.String(max_length=20, nullable=True) + + +@create_drop_database(base_config=ormar_base_config) +async def run_query(): + # 1. like in example above + await Car.objects.select_related("manufacturer").fields( + ["id", "name", "manufacturer__name"] + ).all() + + # 2. to mark a field as required use ellipsis + await Car.objects.select_related("manufacturer").fields( + {"id": ..., "name": ..., "manufacturer": {"name": ...}} + ).all() + + # 3. to include whole nested model use ellipsis + await Car.objects.select_related("manufacturer").fields( + {"id": ..., "name": ..., "manufacturer": ...} + ).all() + + # 4. to specify fields at last nesting level you can also use set + # - equivalent to 2. above + await Car.objects.select_related("manufacturer").fields( + {"id": ..., "name": ..., "manufacturer": {"name"}} + ).all() + + # 5. of course set can have multiple fields + await Car.objects.select_related("manufacturer").fields( + {"id": ..., "name": ..., "manufacturer": {"name", "founded"}} + ).all() + + # 6. you can include all nested fields, + # but it will be equivalent of 3. above which is shorter + await Car.objects.select_related("manufacturer").fields( + {"id": ..., "name": ..., "manufacturer": {"id", "name", "founded"}} + ).all() + + +asyncio.run(run_query()) diff --git a/docs_src/relations/docs001.py b/docs_src/relations/docs001.py index 48307d2..ccf8541 100644 --- a/docs_src/relations/docs001.py +++ b/docs_src/relations/docs001.py @@ -1,27 +1,28 @@ -from typing import Optional, Dict, Union +from typing import Dict, Optional, Union import databases -import sqlalchemy - import ormar +import sqlalchemy database = databases.Database("sqlite:///db.sqlite") metadata = sqlalchemy.MetaData() class Department(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar.OrmarConfig( + database=database, + metadata=metadata, + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -38,7 +39,7 @@ course = Course(name="Math", completed=False, department=department) course2 = Course(name="Math II", completed=False, department=department.pk) # set up a relation with dictionary corresponding to related model -course3 = Course(name="Math III", completed=False, department=department.dict()) +course3 = Course(name="Math III", completed=False, department=department.model_dump()) # explicitly set up None course4 = Course(name="Math III", completed=False, department=None) diff --git a/docs_src/relations/docs002.py b/docs_src/relations/docs002.py index 9831ccb..b39d66c 100644 --- a/docs_src/relations/docs002.py +++ b/docs_src/relations/docs002.py @@ -1,18 +1,18 @@ -from typing import Optional, Union, List +from typing import List, Optional import databases import ormar import sqlalchemy -database = databases.Database("sqlite:///db.sqlite") -metadata = sqlalchemy.MetaData() +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() +) class Author(ormar.Model): - class Meta: - tablename = "authors" - database = database - metadata = metadata + ormar_config = ormar_base_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) first_name: str = ormar.String(max_length=80) @@ -20,20 +20,14 @@ class Author(ormar.Model): class Category(ormar.Model): - class Meta: - tablename = "categories" - database = database - metadata = metadata + ormar_config = ormar_base_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) class Post(ormar.Model): - class Meta: - tablename = "posts" - database = database - metadata = metadata + ormar_config = ormar_base_config.copy(tablename="posts") id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) diff --git a/docs_src/relations/docs003.py b/docs_src/relations/docs003.py index fe33608..f1d6044 100644 --- a/docs_src/relations/docs003.py +++ b/docs_src/relations/docs003.py @@ -1,16 +1,25 @@ +from typing import Dict, Optional, Union + +import databases +import ormar +import sqlalchemy + +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() +) + + class Department(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar_base_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = ormar_base_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) diff --git a/docs_src/relations/docs004.py b/docs_src/relations/docs004.py index 9a3b0c0..5c9a3b4 100644 --- a/docs_src/relations/docs004.py +++ b/docs_src/relations/docs004.py @@ -1,19 +1,23 @@ -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +import databases +import ormar +import sqlalchemy + +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() +) class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = ormar_base_config.copy(tablename="categories") id = ormar.Integer(primary_key=True) name = ormar.String(max_length=40) class PostCategory(ormar.Model): - class Meta(BaseMeta): - tablename = "posts_x_categories" + ormar_config = ormar_base_config.copy(tablename="posts_x_categories") id: int = ormar.Integer(primary_key=True) sort_order: int = ormar.Integer(nullable=True) @@ -21,8 +25,7 @@ class PostCategory(ormar.Model): class Post(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = ormar_base_config.copy() id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) diff --git a/docs_src/select_columns/__init__.py b/docs_src/select_columns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs_src/select_columns/docs001.py b/docs_src/select_columns/docs001.py new file mode 100644 index 0000000..be760a6 --- /dev/null +++ b/docs_src/select_columns/docs001.py @@ -0,0 +1,65 @@ +import asyncio + +import databases +import ormar +import sqlalchemy +from examples import create_drop_database +from tests.settings import DATABASE_URL + +base_ormar_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL, force_rollback=True), + metadata=sqlalchemy.MetaData(), +) + + +class Company(ormar.Model): + ormar_config = base_ormar_config.copy(tablename="companies") + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + founded: int = ormar.Integer(nullable=True) + + +class Car(ormar.Model): + ormar_config = base_ormar_config.copy() + + id: int = ormar.Integer(primary_key=True) + manufacturer = ormar.ForeignKey(Company) + name: str = ormar.String(max_length=100) + year: int = ormar.Integer(nullable=True) + gearbox_type: str = ormar.String(max_length=20, nullable=True) + gears: int = ormar.Integer(nullable=True) + aircon_type: str = ormar.String(max_length=20, nullable=True) + + +@create_drop_database(base_config=base_ormar_config) +async def sample_data(): + # build some sample data + toyota = await Company.objects.create(name="Toyota", founded=1937) + await Car.objects.create( + manufacturer=toyota, + name="Corolla", + year=2020, + gearbox_type="Manual", + gears=5, + aircon_type="Manual", + ) + await Car.objects.create( + manufacturer=toyota, + name="Yaris", + year=2019, + gearbox_type="Manual", + gears=5, + aircon_type="Manual", + ) + await Car.objects.create( + manufacturer=toyota, + name="Supreme", + year=2020, + gearbox_type="Auto", + gears=6, + aircon_type="Auto", + ) + + +asyncio.run(sample_data()) diff --git a/docs_src/signals/docs002.py b/docs_src/signals/docs002.py index 5b1a15f..0795d67 100644 --- a/docs_src/signals/docs002.py +++ b/docs_src/signals/docs002.py @@ -1,5 +1,29 @@ +import asyncio + +import databases +import ormar +import sqlalchemy +from examples import create_drop_database from ormar import pre_update +DATABASE_URL = "sqlite:///test.db" + +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), +) + + +class Album(ormar.Model): + ormar_config = ormar_base_config.copy( + tablename="albums", + ) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + is_best_seller: bool = ormar.Boolean(default=False) + play_count: int = ormar.Integer(default=0) + @pre_update(Album) async def before_update(sender, instance, **kwargs): @@ -7,16 +31,21 @@ async def before_update(sender, instance, **kwargs): instance.is_best_seller = True -# here album.play_count ans is_best_seller get default values -album = await Album.objects.create(name="Venice") -assert not album.is_best_seller -assert album.play_count == 0 +@create_drop_database(base_config=ormar_base_config) +async def run_query(): + # here album.play_count ans is_best_seller get default values + album = await Album.objects.create(name="Venice") + assert not album.is_best_seller + assert album.play_count == 0 -album.play_count = 30 -# here a trigger is called but play_count is too low -await album.update() -assert not album.is_best_seller + album.play_count = 30 + # here a trigger is called but play_count is too low + await album.update() + assert not album.is_best_seller -album.play_count = 60 -await album.update() -assert album.is_best_seller + album.play_count = 60 + await album.update() + assert album.is_best_seller + + +asyncio.run(run_query()) diff --git a/docs_src/test_all_docs.py b/docs_src/test_all_docs.py new file mode 100644 index 0000000..699c00a --- /dev/null +++ b/docs_src/test_all_docs.py @@ -0,0 +1,20 @@ +import subprocess +import sys +from pathlib import Path + +import pytest + +filepaths = [] +path = Path(__file__).parent +for p in path.rglob("*"): + print(p.name) +for p in path.rglob("*"): + if p.name.endswith(".py") and not p.name == "__init__.py" and p != Path(__file__): + filepath_ = str(p.resolve()) + filepaths.append(filepath_) + + +@pytest.mark.parametrize("filepath", filepaths) +def test_all_docs(filepath: str): + result = subprocess.run([sys.executable, filepath]) + assert result.returncode == 0 diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..b574bee --- /dev/null +++ b/examples/__init__.py @@ -0,0 +1,3 @@ +from .utils import create_drop_database + +__all__ = ["create_drop_database"] diff --git a/examples/fastapi_quick_start.py b/examples/fastapi_quick_start.py index 386a0d5..01bb011 100644 --- a/examples/fastapi_quick_start.py +++ b/examples/fastapi_quick_start.py @@ -1,47 +1,45 @@ +from contextlib import asynccontextmanager from typing import List, Optional import databases +import ormar import sqlalchemy import uvicorn from fastapi import FastAPI -import ormar +DATABASE_URL = "sqlite:///test.db" -app = FastAPI() -metadata = sqlalchemy.MetaData() -database = databases.Database("sqlite:///test.db") -app.state.database = database +ormar_base_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() +) -@app.on_event("startup") -async def startup() -> None: +@asynccontextmanager +async def lifespan(app: FastAPI): database_ = app.state.database if not database_.is_connected: await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: + yield database_ = app.state.database if database_.is_connected: await database_.disconnect() +app = FastAPI(lifespan=lifespan) +metadata = sqlalchemy.MetaData() +database = databases.Database("sqlite:///test.db") +app.state.database = database + + class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = ormar_base_config.copy(tablename="items") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -69,7 +67,7 @@ async def create_category(category: Category): @app.put("/items/{item_id}") async def get_item(item_id: int, item: Item): item_db = await Item.objects.get(pk=item_id) - return await item_db.update(**item.dict()) + return await item_db.update(**item.model_dump()) @app.delete("/items/{item_id}") diff --git a/examples/script_from_readme.py b/examples/script_from_readme.py index 57139df..e00db65 100644 --- a/examples/script_from_readme.py +++ b/examples/script_from_readme.py @@ -1,45 +1,40 @@ from typing import Optional import databases -import pydantic - import ormar +import pydantic import sqlalchemy DATABASE_URL = "sqlite:///db.sqlite" -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() -# note that this step is optional -> all ormar cares is a internal -# class with name Meta and proper parameters, but this way you do not +# note that this step is optional -> all ormar cares is an individual +# OrmarConfig for each of the models, but this way you do not # have to repeat the same parameters if you use only one database -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database - +base_ormar_config = ormar.OrmarConfig( + database=databases.Database(DATABASE_URL), + metadata=sqlalchemy.MetaData(), + engine=sqlalchemy.create_engine(DATABASE_URL), +) # Note that all type hints are optional # below is a perfectly valid model declaration # class Author(ormar.Model): -# class Meta(BaseMeta): -# tablename = "authors" +# ormar_config = base_ormar_config.copy(tablename="authors") # # id = ormar.Integer(primary_key=True) # <= notice no field types # name = ormar.String(max_length=100) class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey(Author) @@ -50,10 +45,9 @@ class Book(ormar.Model): # create the database # note that in production you should use migrations # note that this is not required if you connect to existing database -engine = sqlalchemy.create_engine(DATABASE_URL) # just to be sure we clear the db before -metadata.drop_all(engine) -metadata.create_all(engine) +base_ormar_config.metadata.drop_all(base_ormar_config.engine) +base_ormar_config.metadata.create_all(base_ormar_config.engine) # all functions below are divided into functionality categories @@ -381,7 +375,7 @@ async def raw_data(): async def with_connect(function): # note that for any other backend than sqlite you actually need to # connect to the database to perform db operations - async with database: + async with base_ormar_config.database: await function() # note that if you use framework like `fastapi` you shouldn't connect @@ -411,4 +405,4 @@ for func in [ asyncio.run(with_connect(func)) # drop the database tables -metadata.drop_all(engine) +base_ormar_config.metadata.drop_all(base_ormar_config.engine) diff --git a/examples/utils.py b/examples/utils.py new file mode 100644 index 0000000..f2f9b6b --- /dev/null +++ b/examples/utils.py @@ -0,0 +1,21 @@ +import functools + +import ormar +import sqlalchemy + + +def create_drop_database(base_config: ormar.OrmarConfig) -> None: + # create all tables in the database before execution + # and drop them after, note that in production you should use migrations + def wrapper(func): + @functools.wraps(func) + async def wrapped(*args): + engine = sqlalchemy.create_engine(str(base_config.database.url)) + base_config.metadata.drop_all(engine) + base_config.metadata.create_all(engine) + await func(*args) + base_config.metadata.drop_all(engine) + + return wrapped + + return wrapper diff --git a/mkdocs.yml b/mkdocs.yml index 110ae5a..e5052e5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,10 +16,10 @@ nav: - Pydantic only fields: fields/pydantic-fields.md - Fields encryption: fields/encryption.md - Relations: - - relations/index.md - - relations/postponed-annotations.md + - Relation types: relations/index.md - relations/foreign-key.md - relations/many-to-many.md + - relations/postponed-annotations.md - relations/queryset-proxy.md - Queries: - queries/index.md @@ -40,6 +40,7 @@ nav: - Using ormar in responses: fastapi/response.md - Using ormar in requests: fastapi/requests.md - Use with mypy: mypy.md + - Migration to v 0.20: migration.md - PyCharm plugin: plugin.md - Contributing: contributing.md - Release Notes: releases.md diff --git a/ormar/__init__.py b/ormar/__init__.py index 9259d1d..21a07b4 100644 --- a/ormar/__init__.py +++ b/ormar/__init__.py @@ -19,11 +19,10 @@ snakes, and ormar(e) in italian which means cabinet. And what's a better name for python ORM than snakes cabinet :) """ -try: - from importlib.metadata import version # type: ignore -except ImportError: # pragma: no cover - from importlib_metadata import version # type: ignore -from ormar.protocols import QuerySetProtocol, RelationProtocol # noqa: I100 + +from ormar.protocols import QuerySetProtocol, RelationProtocol # noqa: I001 +from importlib.metadata import version + from ormar.decorators import ( # noqa: I100 post_bulk_update, post_delete, @@ -36,7 +35,6 @@ from ormar.decorators import ( # noqa: I100 pre_relation_remove, pre_save, pre_update, - property_field, ) from ormar.exceptions import ( # noqa: I100 ModelDefinitionError, @@ -44,37 +42,38 @@ from ormar.exceptions import ( # noqa: I100 NoMatch, ) from ormar.fields import ( + DECODERS_MAP, + ENCODERS_MAP, + JSON, + SQL_ENCODERS_MAP, + UUID, BaseField, BigInteger, Boolean, - DECODERS_MAP, + CheckColumns, Date, DateTime, Decimal, - ENCODERS_MAP, EncryptBackends, Enum, Float, ForeignKey, ForeignKeyField, + IndexColumns, Integer, - JSON, LargeBinary, ManyToMany, ManyToManyField, - SQL_ENCODERS_MAP, + ReferentialAction, SmallInteger, String, Text, Time, - UUID, UniqueColumns, - IndexColumns, - CheckColumns, - ReferentialAction, -) # noqa: I100 -from ormar.models import ExcludableItems, Extra, Model -from ormar.models.metaclass import ModelMeta +) + +# noqa: I100 +from ormar.models import ExcludableItems, Extra, Model, OrmarConfig from ormar.queryset import OrderAction, QuerySet, and_, or_ from ormar.relations import RelationType from ormar.signals import Signal @@ -104,7 +103,6 @@ __all__ = [ "Float", "ManyToMany", "Model", - "Action", "ModelDefinitionError", "MultipleMatches", "NoMatch", @@ -119,8 +117,6 @@ __all__ = [ "ReferentialAction", "QuerySetProtocol", "RelationProtocol", - "ModelMeta", - "property_field", "post_bulk_update", "post_delete", "post_save", @@ -146,4 +142,5 @@ __all__ = [ "DECODERS_MAP", "LargeBinary", "Extra", + "OrmarConfig", ] diff --git a/ormar/decorators/__init__.py b/ormar/decorators/__init__.py index c6f42d0..04005f2 100644 --- a/ormar/decorators/__init__.py +++ b/ormar/decorators/__init__.py @@ -3,11 +3,10 @@ Module with all decorators that are exposed for users. Currently only: -* property_field - exposing @property like function as field in Model.dict() * predefined signals decorators (pre/post + save/update/delete) """ -from ormar.decorators.property_field import property_field + from ormar.decorators.signals import ( post_bulk_update, post_delete, @@ -23,7 +22,6 @@ from ormar.decorators.signals import ( ) __all__ = [ - "property_field", "post_bulk_update", "post_delete", "post_save", diff --git a/ormar/decorators/property_field.py b/ormar/decorators/property_field.py deleted file mode 100644 index 69ec9da..0000000 --- a/ormar/decorators/property_field.py +++ /dev/null @@ -1,32 +0,0 @@ -import inspect -from collections.abc import Callable -from typing import Union - -from ormar.exceptions import ModelDefinitionError - - -def property_field(func: Callable) -> Union[property, Callable]: - """ - Decorator to set a property like function on Model to be exposed - as field in dict() and fastapi response. - Although you can decorate a @property field like this and this will work, - mypy validation will complain about this. - Note that "fields" exposed like this do not go through validation. - - :raises ModelDefinitionError: if method has any other argument than self. - :param func: decorated function to be exposed - :type func: Callable - :return: decorated function passed in func param, with set __property_field__ = True - :rtype: Union[property, Callable] - """ - if isinstance(func, property): # pragma: no cover - func.fget.__property_field__ = True - else: - arguments = list(inspect.signature(func).parameters.keys()) - if len(arguments) > 1 or arguments[0] != "self": - raise ModelDefinitionError( - "property_field decorator can be used " - "only on methods with no arguments" - ) - func.__dict__["__property_field__"] = True - return func diff --git a/ormar/decorators/signals.py b/ormar/decorators/signals.py index 3e10c1d..b3b38b9 100644 --- a/ormar/decorators/signals.py +++ b/ormar/decorators/signals.py @@ -1,4 +1,4 @@ -from typing import Callable, List, TYPE_CHECKING, Type, Union +from typing import TYPE_CHECKING, Callable, List, Type, Union if TYPE_CHECKING: # pragma: no cover from ormar import Model @@ -34,7 +34,7 @@ def receiver( else: _senders = senders for sender in _senders: - signals = getattr(sender.Meta.signals, signal) + signals = getattr(sender.ormar_config.signals, signal) signals.connect(func) return func diff --git a/ormar/exceptions.py b/ormar/exceptions.py index 12e2e3a..d7e56d1 100644 --- a/ormar/exceptions.py +++ b/ormar/exceptions.py @@ -15,11 +15,9 @@ class ModelDefinitionError(AsyncOrmException): """ Raised for errors related to the model definition itself: - * setting @property_field on method with arguments other than func(self) * defining a Field without required parameters * defining a model with more than one primary_key * defining a model without primary_key - * setting primary_key column as pydantic_only """ pass diff --git a/ormar/fields/__init__.py b/ormar/fields/__init__.py index 08fec4f..84d2acb 100644 --- a/ormar/fields/__init__.py +++ b/ormar/fields/__init__.py @@ -4,11 +4,14 @@ Base Fields types (like String, Integer etc.) as well as relation Fields (ForeignKey, ManyToMany). Also a definition for custom CHAR based sqlalchemy UUID field """ + from ormar.fields.base import BaseField -from ormar.fields.constraints import IndexColumns, UniqueColumns, CheckColumns +from ormar.fields.constraints import CheckColumns, IndexColumns, UniqueColumns from ormar.fields.foreign_key import ForeignKey, ForeignKeyField from ormar.fields.many_to_many import ManyToMany, ManyToManyField from ormar.fields.model_fields import ( + JSON, + UUID, BigInteger, Boolean, Date, @@ -17,18 +20,16 @@ from ormar.fields.model_fields import ( Enum, Float, Integer, - JSON, LargeBinary, SmallInteger, String, Text, Time, - UUID, ) from ormar.fields.parsers import DECODERS_MAP, ENCODERS_MAP, SQL_ENCODERS_MAP +from ormar.fields.referential_actions import ReferentialAction from ormar.fields.sqlalchemy_encrypted import EncryptBackend, EncryptBackends from ormar.fields.through_field import Through, ThroughField -from ormar.fields.referential_actions import ReferentialAction __all__ = [ "Decimal", diff --git a/ormar/fields/base.py b/ormar/fields/base.py index f7eaff5..a55f9a9 100644 --- a/ormar/fields/base.py +++ b/ormar/fields/base.py @@ -1,9 +1,7 @@ -import warnings -from typing import Any, Dict, List, Optional, TYPE_CHECKING, Type, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union import sqlalchemy -from pydantic import Json, typing -from pydantic.fields import FieldInfo, Required, Undefined +from pydantic.fields import FieldInfo, _Unset import ormar # noqa I101 from ormar import ModelDefinitionError @@ -14,8 +12,7 @@ from ormar.fields.sqlalchemy_encrypted import ( ) if TYPE_CHECKING: # pragma no cover - from ormar.models import Model - from ormar.models import NewBaseModel + from ormar.models import Model, NewBaseModel class BaseField(FieldInfo): @@ -46,16 +43,6 @@ class BaseField(FieldInfo): self.sql_nullable: bool = kwargs.pop("sql_nullable", False) self.index: bool = kwargs.pop("index", False) self.unique: bool = kwargs.pop("unique", False) - self.pydantic_only: bool = kwargs.pop("pydantic_only", False) - if self.pydantic_only: - warnings.warn( - "Parameter `pydantic_only` is deprecated and will " - "be removed in one of the next releases.\n You can declare " - "pydantic fields in a normal way. \n Check documentation: " - "https://collerek.github.io/ormar/fields/pydantic-fields", - DeprecationWarning, - ) - self.choices: typing.Sequence = kwargs.pop("choices", False) self.virtual: bool = kwargs.pop( "virtual", None @@ -76,6 +63,7 @@ class BaseField(FieldInfo): self.owner: Type["Model"] = kwargs.pop("owner", None) self.to: Type["Model"] = kwargs.pop("to", None) + self.to_pk_only: Type["Model"] = kwargs.pop("to_pk_only", None) self.through: Type["Model"] = kwargs.pop("through", None) self.self_reference: bool = kwargs.pop("self_reference", False) self.self_reference_primary: Optional[str] = kwargs.pop( @@ -145,9 +133,7 @@ class BaseField(FieldInfo): """ base = self.default_value() if base is None: - base = dict(default=None) if self.nullable else dict(default=Undefined) - if self.__type__ == Json and base.get("default") is Undefined: - base["default"] = Required + base = dict(default=None) if self.nullable else dict(default=_Unset) return base def default_value(self, use_server: bool = False) -> Optional[Dict]: @@ -181,7 +167,9 @@ class BaseField(FieldInfo): return dict(default=default) return None - def get_default(self, use_server: bool = False) -> Any: # noqa CCR001 + def get_default( + self, use_server: bool = False, call_default_factory: bool = True + ) -> Any: # noqa CCR001 """ Return default value for a field. If the field is Callable the function is called and actual result is returned. @@ -197,11 +185,26 @@ class BaseField(FieldInfo): default = ( self.ormar_default if self.ormar_default is not None - else (self.server_default if use_server else None) + else self._get_default_server_value(use_server=use_server) ) - if callable(default): # pragma: no cover - default = default() - return default + return self._get_default_callable_value( + default=default, + call_default_factory=call_default_factory, + ) + + def _get_default_server_value(self, use_server: bool) -> Any: + """ + Return default value for a server side if use_server is True + """ + return self.server_default if use_server else None + + @staticmethod + def _get_default_callable_value(default: Any, call_default_factory: bool) -> Any: + """ + Return default factory value if call_default_factory is True + and default is a callable. + """ + return default() if (callable(default) and call_default_factory) else default def has_default(self, use_server: bool = True) -> bool: """ @@ -244,8 +247,8 @@ class BaseField(FieldInfo): con.reference, ondelete=con.ondelete, onupdate=con.onupdate, - name=f"fk_{self.owner.Meta.tablename}_{self.to.Meta.tablename}" - f"_{self.to.get_column_alias(self.to.Meta.pkname)}_{self.name}", + name=f"fk_{self.owner.ormar_config.tablename}_{self.to.ormar_config.tablename}" + f"_{self.to.get_column_alias(self.to.ormar_config.pkname)}_{self.name}", ) for con in self.constraints ] @@ -339,7 +342,7 @@ class BaseField(FieldInfo): :rtype: None """ if self.owner is not None and ( - self.owner == self.to or self.owner.Meta == self.to.Meta + self.owner == self.to or self.owner.ormar_config == self.to.ormar_config ): self.self_reference = True self.self_reference_primary = self.name diff --git a/ormar/fields/constraints.py b/ormar/fields/constraints.py index e799d0c..66a90f0 100644 --- a/ormar/fields/constraints.py +++ b/ormar/fields/constraints.py @@ -1,6 +1,6 @@ -from typing import Any +from typing import Any, Optional -from sqlalchemy import Index, UniqueConstraint, CheckConstraint +from sqlalchemy import CheckConstraint, Index, UniqueConstraint class UniqueColumns(UniqueConstraint): @@ -11,7 +11,7 @@ class UniqueColumns(UniqueConstraint): class IndexColumns(Index): - def __init__(self, *args: Any, name: str = None, **kw: Any) -> None: + def __init__(self, *args: Any, name: Optional[str] = None, **kw: Any) -> None: if not name: name = "TEMPORARY_NAME" super().__init__(name, *args, **kw) diff --git a/ormar/fields/foreign_key.py b/ormar/fields/foreign_key.py index cfa2205..a867c39 100644 --- a/ormar/fields/foreign_key.py +++ b/ormar/fields/foreign_key.py @@ -1,30 +1,32 @@ import string +import sys import uuid from dataclasses import dataclass from random import choices from typing import ( + TYPE_CHECKING, Any, Dict, + ForwardRef, List, Optional, - TYPE_CHECKING, Tuple, Type, Union, overload, ) -import ormar # noqa I101 import sqlalchemy +from pydantic import BaseModel, create_model + +import ormar # noqa I101 from ormar.exceptions import ModelDefinitionError, RelationshipInstanceError from ormar.fields.base import BaseField from ormar.fields.referential_actions import ReferentialAction -from pydantic import BaseModel, create_model -from pydantic.typing import ForwardRef, evaluate_forwardref if TYPE_CHECKING: # pragma no cover - from ormar.models import Model, NewBaseModel, T from ormar.fields import ManyToManyField + from ormar.models import Model, NewBaseModel, T def create_dummy_instance(fk: Type["T"], pk: Any = None) -> "T": @@ -47,10 +49,10 @@ def create_dummy_instance(fk: Type["T"], pk: Any = None) -> "T": :rtype: Model """ init_dict = { - **{fk.Meta.pkname: pk or -1, "__pk_only__": True}, + **{fk.ormar_config.pkname: pk or -1, "__pk_only__": True}, **{ k: create_dummy_instance(v.to) - for k, v in fk.Meta.model_fields.items() + for k, v in fk.ormar_config.model_fields.items() if v.is_relation and not v.nullable and not v.virtual }, } @@ -86,8 +88,11 @@ def create_dummy_model( def populate_fk_params_based_on_to_model( - to: Type["T"], nullable: bool, onupdate: str = None, ondelete: str = None -) -> Tuple[Any, List, Any]: + to: Type["T"], + nullable: bool, + onupdate: Optional[str] = None, + ondelete: Optional[str] = None, +) -> Tuple[Any, List, Any, Any]: """ Based on target to model to which relation leads to populates the type of the pydantic field to use, ForeignKey constraint and type of the target column field. @@ -105,8 +110,10 @@ def populate_fk_params_based_on_to_model( :return: tuple with target pydantic type, list of fk constraints and target col type :rtype: Tuple[Any, List, Any] """ - fk_string = to.Meta.tablename + "." + to.get_column_alias(to.Meta.pkname) - to_field = to.Meta.model_fields[to.Meta.pkname] + fk_string = ( + to.ormar_config.tablename + "." + to.get_column_alias(to.ormar_config.pkname) + ) + to_field = to.ormar_config.model_fields[to.ormar_config.pkname] pk_only_model = create_dummy_model(to, to_field) __type__ = ( Union[to_field.__type__, to, pk_only_model] @@ -119,7 +126,7 @@ def populate_fk_params_based_on_to_model( ) ] column_type = to_field.column_type - return __type__, constraints, column_type + return __type__, constraints, column_type, pk_only_model def validate_not_allowed_fields(kwargs: Dict) -> None: @@ -200,13 +207,13 @@ def ForeignKey(to: ForwardRef, **kwargs: Any) -> "Model": # pragma: no cover def ForeignKey( # type: ignore # noqa CFQ002 to: Union[Type["T"], "ForwardRef"], *, - name: str = None, + name: Optional[str] = None, unique: bool = False, nullable: bool = True, - related_name: str = None, + related_name: Optional[str] = None, virtual: bool = False, - onupdate: Union[ReferentialAction, str] = None, - ondelete: Union[ReferentialAction, str] = None, + onupdate: Union[ReferentialAction, str, None] = None, + ondelete: Union[ReferentialAction, str, None] = None, **kwargs: Any, ) -> "T": """ @@ -256,13 +263,18 @@ def ForeignKey( # type: ignore # noqa CFQ002 sql_nullable = nullable if sql_nullable is None else sql_nullable validate_not_allowed_fields(kwargs) - + pk_only_model = None if to.__class__ == ForwardRef: __type__ = to if not nullable else Optional[to] constraints: List = [] column_type = None else: - __type__, constraints, column_type = populate_fk_params_based_on_to_model( + ( + __type__, + constraints, + column_type, + pk_only_model, + ) = populate_fk_params_based_on_to_model( to=to, # type: ignore nullable=nullable, ondelete=ondelete, @@ -272,6 +284,7 @@ def ForeignKey( # type: ignore # noqa CFQ002 namespace = dict( __type__=__type__, to=to, + to_pk_only=pk_only_model, through=None, alias=name, name=kwargs.pop("real_name", None), @@ -284,7 +297,6 @@ def ForeignKey( # type: ignore # noqa CFQ002 virtual=virtual, primary_key=False, index=False, - pydantic_only=False, default=None, server_default=None, onupdate=onupdate, @@ -352,6 +364,17 @@ class ForeignKeyField(BaseField): prefix = "to_" if self.self_reference else "" return self.through_relation_name or f"{prefix}{self.owner.get_name()}" + def _evaluate_forward_ref( + self, globalns: Any, localns: Any, is_through: bool = False + ) -> None: + target = "through" if is_through else "to" + target_obj = getattr(self, target) + if sys.version_info.minor <= 8: # pragma: no cover + evaluated = target_obj._evaluate(globalns, localns) + else: # pragma: no cover + evaluated = target_obj._evaluate(globalns, localns, set()) + setattr(self, target, evaluated) + def evaluate_forward_ref(self, globalns: Any, localns: Any) -> None: """ Evaluates the ForwardRef to actual Field based on global and local namespaces @@ -364,13 +387,12 @@ class ForeignKeyField(BaseField): :rtype: None """ if self.to.__class__ == ForwardRef: - self.to = evaluate_forwardref( - self.to, globalns, localns or None # type: ignore - ) + self._evaluate_forward_ref(globalns, localns) ( self.__type__, self.constraints, self.column_type, + self.to_pk_only, ) = populate_fk_params_based_on_to_model( to=self.to, nullable=self.nullable, @@ -444,12 +466,21 @@ class ForeignKeyField(BaseField): :return: (if needed) registered Model :rtype: Model """ - if len(value.keys()) == 1 and list(value.keys())[0] == self.to.Meta.pkname: + pk_only_model = None + keys = set(value.keys()) + own_keys = keys - self.to.extract_related_names() + if ( + len(own_keys) == 1 + and list(own_keys)[0] == self.to.ormar_config.pkname + and value.get(self.to.ormar_config.pkname) is not None + and not self.is_through + ): value["__pk_only__"] = True + pk_only_model = self.to_pk_only(**value) model = self.to(**value) if to_register: self.register_relation(model=model, child=child) - return model + return pk_only_model if pk_only_model is not None else model def _construct_model_from_pk( self, value: Any, child: "Model", to_register: bool @@ -472,11 +503,14 @@ class ForeignKeyField(BaseField): if self.to.pk_type() == uuid.UUID and isinstance(value, str): # pragma: nocover value = uuid.UUID(value) if not isinstance(value, self.to.pk_type()): - raise RelationshipInstanceError( - f"Relationship error - ForeignKey {self.to.__name__} " - f"is of type {self.to.pk_type()} " - f"while {type(value)} passed as a parameter." - ) + if isinstance(value, self.to_pk_only): + value = getattr(value, self.to.ormar_config.pkname) + else: + raise RelationshipInstanceError( + f"Relationship error - ForeignKey {self.to.__name__} " + f"is of type {self.to.pk_type()} " + f"while {type(value)} passed as a parameter." + ) model = create_dummy_instance(fk=self.to, pk=value) if to_register: self.register_relation(model=model, child=child) diff --git a/ormar/fields/many_to_many.py b/ormar/fields/many_to_many.py index 350fa3c..1a8af2d 100644 --- a/ormar/fields/many_to_many.py +++ b/ormar/fields/many_to_many.py @@ -1,9 +1,9 @@ -import sys from typing import ( + TYPE_CHECKING, Any, + ForwardRef, List, Optional, - TYPE_CHECKING, Tuple, Type, Union, @@ -11,21 +11,19 @@ from typing import ( overload, ) -from pydantic.typing import ForwardRef, evaluate_forwardref import ormar # noqa: I100 from ormar import ModelDefinitionError from ormar.fields import BaseField -from ormar.fields.foreign_key import ForeignKeyField, validate_not_allowed_fields +from ormar.fields.foreign_key import ( + ForeignKeyField, + create_dummy_model, + validate_not_allowed_fields, +) if TYPE_CHECKING: # pragma no cover from ormar.models import Model, T from ormar.relations.relation_proxy import RelationProxy - if sys.version_info < (3, 7): - ToType = Type["T"] - else: - ToType = Union[Type["T"], "ForwardRef"] - REF_PREFIX = "#/components/schemas/" @@ -36,7 +34,7 @@ def forbid_through_relations(through: Type["Model"]) -> None: :param through: through Model to be checked :type through: Type['Model] """ - if any(field.is_relation for field in through.Meta.model_fields.values()): + if any(field.is_relation for field in through.ormar_config.model_fields.values()): raise ModelDefinitionError( f"Through Models cannot have explicit relations " f"defined. Remove the relations from Model " @@ -46,7 +44,7 @@ def forbid_through_relations(through: Type["Model"]) -> None: def populate_m2m_params_based_on_to_model( to: Type["Model"], nullable: bool -) -> Tuple[Any, Any]: +) -> Tuple[Any, Any, Any]: """ Based on target to model to which relation leads to populates the type of the pydantic field to use and type of the target column field. @@ -58,14 +56,22 @@ def populate_m2m_params_based_on_to_model( :return: Tuple[List, Any] :rtype: tuple with target pydantic type and target col type """ - to_field = to.Meta.model_fields[to.Meta.pkname] + to_field = to.ormar_config.model_fields[to.ormar_config.pkname] + pk_only_model = create_dummy_model(to, to_field) + base_type = Union[ # type: ignore + to_field.__type__, # type: ignore + to, # type: ignore + pk_only_model, # type: ignore + List[to], # type: ignore + List[pk_only_model], # type: ignore + ] __type__ = ( - Union[to_field.__type__, to, List[to]] # type: ignore + base_type # type: ignore if not nullable - else Optional[Union[to_field.__type__, to, List[to]]] # type: ignore + else Optional[base_type] # type: ignore ) column_type = to_field.column_type - return __type__, column_type + return __type__, column_type, pk_only_model @overload @@ -79,10 +85,10 @@ def ManyToMany(to: ForwardRef, **kwargs: Any) -> "RelationProxy": # pragma: no def ManyToMany( # type: ignore - to: "ToType", - through: Optional["ToType"] = None, + to: Union[Type["T"], "ForwardRef"], + through: Optional[Union[Type["T"], "ForwardRef"]] = None, *, - name: str = None, + name: Optional[str] = None, unique: bool = False, virtual: bool = False, **kwargs: Any, @@ -129,7 +135,7 @@ def ManyToMany( # type: ignore forbid_through_relations(cast(Type["Model"], through)) validate_not_allowed_fields(kwargs) - + pk_only_model = None if to.__class__ == ForwardRef: __type__ = ( Union[to, List[to]] # type: ignore @@ -138,12 +144,13 @@ def ManyToMany( # type: ignore ) column_type = None else: - __type__, column_type = populate_m2m_params_based_on_to_model( + __type__, column_type, pk_only_model = populate_m2m_params_based_on_to_model( to=to, nullable=nullable # type: ignore ) namespace = dict( __type__=__type__, to=to, + to_pk_only=pk_only_model, through=through, alias=name, name=name, @@ -154,7 +161,6 @@ def ManyToMany( # type: ignore virtual=virtual, primary_key=False, index=False, - pydantic_only=False, default=None, server_default=None, owner=owner, @@ -173,7 +179,11 @@ def ManyToMany( # type: ignore return Field(**namespace) -class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationProtocol): +class ManyToManyField( # type: ignore + ForeignKeyField, + ormar.QuerySetProtocol, + ormar.RelationProtocol, +): """ Actual class returned from ManyToMany function call and stored in model_fields. """ @@ -194,7 +204,7 @@ class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationPro :rtype: str """ return ( - self.through.Meta.model_fields[ + self.through.ormar_config.model_fields[ self.default_source_field_name() ].related_name or self.name @@ -222,18 +232,19 @@ class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationPro :rtype: None """ if self.to.__class__ == ForwardRef: - self.to = evaluate_forwardref( - self.to, globalns, localns or None # type: ignore - ) - - (self.__type__, self.column_type) = populate_m2m_params_based_on_to_model( + self._evaluate_forward_ref(globalns, localns) + ( + self.__type__, + self.column_type, + pk_only_model, + ) = populate_m2m_params_based_on_to_model( to=self.to, nullable=self.nullable ) + self.to_pk_only = pk_only_model if self.through.__class__ == ForwardRef: - self.through = evaluate_forwardref( - self.through, globalns, localns or None # type: ignore - ) + self._evaluate_forward_ref(globalns, localns, is_through=True) + forbid_through_relations(self.through) def get_relation_name(self) -> str: @@ -265,15 +276,22 @@ class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationPro to_name = self.to.get_name(lower=False) class_name = f"{owner_name}{to_name}" table_name = f"{owner_name.lower()}s_{to_name.lower()}s" - new_meta_namespace = { - "tablename": table_name, - "database": self.owner.Meta.database, - "metadata": self.owner.Meta.metadata, + base_namespace = { + "__module__": self.owner.__module__, + "__qualname__": f"{self.owner.__qualname__}.{class_name}", } - new_meta = type("Meta", (), new_meta_namespace) + new_config = ormar.models.ormar_config.OrmarConfig( + tablename=table_name, + database=self.owner.ormar_config.database, + metadata=self.owner.ormar_config.metadata, + ) through_model = type( class_name, (ormar.Model,), - {"Meta": new_meta, "id": ormar.Integer(name="id", primary_key=True)}, + { + **base_namespace, + "ormar_config": new_config, + "id": ormar.Integer(name="id", primary_key=True), + }, ) self.through = cast(Type["Model"], through_model) diff --git a/ormar/fields/model_fields.py b/ormar/fields/model_fields.py index f2f4cfc..fa36163 100644 --- a/ormar/fields/model_fields.py +++ b/ormar/fields/model_fields.py @@ -1,8 +1,9 @@ import datetime import decimal import uuid -from enum import Enum as E, EnumMeta -from typing import Any, Optional, Set, TYPE_CHECKING, Type, TypeVar, Union, overload +from enum import Enum as E +from enum import EnumMeta +from typing import TYPE_CHECKING, Any, Optional, Type, TypeVar, Union, overload import pydantic import sqlalchemy @@ -23,7 +24,6 @@ def is_field_nullable( nullable: Optional[bool], default: Any, server_default: Any, - pydantic_only: Optional[bool], ) -> bool: """ Checks if the given field should be nullable/ optional based on parameters given. @@ -34,17 +34,11 @@ def is_field_nullable( :type default: Any :param server_default: function to be called as default by sql server :type server_default: Any - :param pydantic_only: flag if fields should not be included in the sql table - :type pydantic_only: Optional[bool] :return: result of the check :rtype: bool """ if nullable is None: - return ( - default is not None - or server_default is not None - or (pydantic_only is not None and pydantic_only) - ) + return default is not None or server_default is not None return nullable @@ -62,49 +56,6 @@ def is_auto_primary_key(primary_key: bool, autoincrement: bool) -> bool: return primary_key and autoincrement -def convert_choices_if_needed( - field_type: "Type", - choices: Set, - nullable: bool, - scale: int = None, - represent_as_str: bool = False, -) -> Set: - """ - Converts dates to isoformat as fastapi can check this condition in routes - and the fields are not yet parsed. - Converts enums to list of it's values. - Converts uuids to strings. - Converts decimal to float with given scale. - - :param field_type: type o the field - :type field_type: Type - :param choices: set of choices - :type choices: Set - :param scale: scale for decimals - :type scale: int - :param nullable: flag if field_nullable - :type nullable: bool - :param represent_as_str: flag for bytes fields - :type represent_as_str: bool - :param scale: scale for decimals - :type scale: int - :return: value, choices list - :rtype: Tuple[Any, Set] - """ - choices = {o.value if isinstance(o, E) else o for o in choices} - encoder = ormar.ENCODERS_MAP.get(field_type, lambda x: x) - if field_type == decimal.Decimal: - precision = scale - choices = {encoder(o, precision) for o in choices} - elif field_type == bytes: - choices = {encoder(o, represent_as_str) for o in choices} - elif encoder: - choices = {encoder(o) for o in choices} - if nullable: - choices.add(None) - return choices - - class ModelFieldFactory: """ Default field factory that construct Field classes and populated their values. @@ -121,7 +72,6 @@ class ModelFieldFactory: server_default = kwargs.pop("server_default", None) nullable = kwargs.pop("nullable", None) sql_nullable = kwargs.pop("sql_nullable", None) - pydantic_only = kwargs.pop("pydantic_only", False) primary_key = kwargs.pop("primary_key", False) autoincrement = kwargs.pop("autoincrement", False) @@ -133,7 +83,7 @@ class ModelFieldFactory: overwrite_pydantic_type = kwargs.pop("overwrite_pydantic_type", None) nullable = is_field_nullable( - nullable, default, server_default, pydantic_only + nullable, default, server_default ) or is_auto_primary_key(primary_key, autoincrement) sql_nullable = ( False @@ -141,23 +91,16 @@ class ModelFieldFactory: else (nullable if sql_nullable is None else sql_nullable) ) - choices = set(kwargs.pop("choices", [])) - if choices: - choices = convert_choices_if_needed( - field_type=cls._type, - choices=choices, - nullable=nullable, - scale=kwargs.get("scale", None), - represent_as_str=kwargs.get("represent_as_base64_str", False), - ) enum_class = kwargs.pop("enum_class", None) field_type = cls._type if enum_class is None else enum_class namespace = dict( __type__=field_type, - __pydantic_type__=overwrite_pydantic_type - if overwrite_pydantic_type is not None - else field_type, + __pydantic_type__=( + overwrite_pydantic_type + if overwrite_pydantic_type is not None + else field_type + ), __sample__=cls._sample, alias=kwargs.pop("name", None), name=None, @@ -165,15 +108,14 @@ class ModelFieldFactory: default=default, server_default=server_default, nullable=nullable, + annotation=field_type, sql_nullable=sql_nullable, index=kwargs.pop("index", False), unique=kwargs.pop("unique", False), - pydantic_only=pydantic_only, autoincrement=autoincrement, column_type=cls.get_column_type( **kwargs, sql_nullable=sql_nullable, enum_class=enum_class ), - choices=choices, encrypt_secret=encrypt_secret, encrypt_backend=encrypt_backend, encrypt_custom_backend=encrypt_custom_backend, @@ -216,8 +158,8 @@ class String(ModelFieldFactory, str): cls, *, max_length: int, - min_length: int = None, - regex: str = None, + min_length: Optional[int] = None, + regex: Optional[str] = None, **kwargs: Any ) -> BaseField: # type: ignore kwargs = { @@ -268,9 +210,9 @@ class Integer(ModelFieldFactory, int): def __new__( # type: ignore cls, *, - minimum: int = None, - maximum: int = None, - multiple_of: int = None, + minimum: Optional[int] = None, + maximum: Optional[int] = None, + multiple_of: Optional[int] = None, **kwargs: Any ) -> BaseField: autoincrement = kwargs.pop("autoincrement", None) @@ -349,9 +291,9 @@ class Float(ModelFieldFactory, float): def __new__( # type: ignore cls, *, - minimum: float = None, - maximum: float = None, - multiple_of: int = None, + minimum: Optional[float] = None, + maximum: Optional[float] = None, + multiple_of: Optional[int] = None, **kwargs: Any ) -> BaseField: kwargs = { @@ -528,20 +470,17 @@ if TYPE_CHECKING: # pragma: nocover # noqa: C901 @overload def LargeBinary( # type: ignore max_length: int, *, represent_as_base64_str: Literal[True], **kwargs: Any - ) -> str: - ... + ) -> str: ... @overload def LargeBinary( # type: ignore max_length: int, *, represent_as_base64_str: Literal[False], **kwargs: Any - ) -> bytes: - ... + ) -> bytes: ... @overload def LargeBinary( max_length: int, represent_as_base64_str: Literal[False] = ..., **kwargs: Any - ) -> bytes: - ... + ) -> bytes: ... def LargeBinary( max_length: int, represent_as_base64_str: bool = False, **kwargs: Any @@ -614,9 +553,9 @@ class BigInteger(Integer, int): def __new__( # type: ignore cls, *, - minimum: int = None, - maximum: int = None, - multiple_of: int = None, + minimum: Optional[int] = None, + maximum: Optional[int] = None, + multiple_of: Optional[int] = None, **kwargs: Any ) -> BaseField: autoincrement = kwargs.pop("autoincrement", None) @@ -662,9 +601,9 @@ class SmallInteger(Integer, int): def __new__( # type: ignore cls, *, - minimum: int = None, - maximum: int = None, - multiple_of: int = None, + minimum: Optional[int] = None, + maximum: Optional[int] = None, + multiple_of: Optional[int] = None, **kwargs: Any ) -> BaseField: autoincrement = kwargs.pop("autoincrement", None) @@ -710,13 +649,13 @@ class Decimal(ModelFieldFactory, decimal.Decimal): def __new__( # type: ignore # noqa CFQ002 cls, *, - minimum: float = None, - maximum: float = None, - multiple_of: int = None, - precision: int = None, - scale: int = None, - max_digits: int = None, - decimal_places: int = None, + minimum: Optional[float] = None, + maximum: Optional[float] = None, + multiple_of: Optional[int] = None, + precision: Optional[int] = None, + scale: Optional[int] = None, + max_digits: Optional[int] = None, + decimal_places: Optional[int] = None, **kwargs: Any ) -> BaseField: kwargs = { @@ -810,7 +749,6 @@ class UUID(ModelFieldFactory, uuid.UUID): if TYPE_CHECKING: # pragma: nocover - T = TypeVar("T", bound=E) def Enum(enum_class: Type[T], **kwargs: Any) -> T: @@ -829,7 +767,6 @@ else: def __new__( # type: ignore # noqa CFQ002 cls, *, enum_class: Type[E], **kwargs: Any ) -> BaseField: - kwargs = { **kwargs, **{ diff --git a/ormar/fields/parsers.py b/ormar/fields/parsers.py index c3d85fe..9b73304 100644 --- a/ormar/fields/parsers.py +++ b/ormar/fields/parsers.py @@ -5,7 +5,7 @@ import uuid from typing import Any, Callable, Dict, Optional, Union import pydantic -from pydantic.datetime_parse import parse_date, parse_datetime, parse_time +from pydantic_core import SchemaValidator, core_schema try: import orjson as json @@ -21,17 +21,23 @@ def encode_bool(value: bool) -> str: return "true" if value else "false" -def encode_decimal(value: decimal.Decimal, precision: int = None) -> float: - if precision: - return ( - round(float(value), precision) - if isinstance(value, decimal.Decimal) - else value +def encode_decimal(value: decimal.Decimal, precision: Optional[int] = None) -> float: + return ( + round(float(value), precision) if isinstance(value, decimal.Decimal) else value + ) + + +def encode_bytes(value: Union[str, bytes], represent_as_string: bool = False) -> str: + if represent_as_string: + value = ( + value if isinstance(value, str) else base64.b64encode(value).decode("utf-8") ) - return float(value) + else: + value = value if isinstance(value, str) else value.decode("utf-8") + return value -def encode_bytes(value: Union[str, bytes], represent_as_string: bool = False) -> bytes: +def decode_bytes(value: str, represent_as_string: bool = False) -> bytes: if represent_as_string: return value if isinstance(value, bytes) else base64.b64decode(value) return value if isinstance(value, bytes) else value.encode("utf-8") @@ -47,10 +53,10 @@ def encode_json(value: Any) -> Optional[str]: def re_dump_value(value: str) -> Union[str, bytes]: """ - Rw-dumps choices due to different string representation in orjson and json + Re-dumps value due to different string representation in orjson and json :param value: string to re-dump :type value: str - :return: re-dumped choices + :return: re-dumped value :rtype: List[str] """ try: @@ -72,11 +78,20 @@ ENCODERS_MAP: Dict[type, Callable] = { SQL_ENCODERS_MAP: Dict[type, Callable] = {bool: encode_bool, **ENCODERS_MAP} -DECODERS_MAP = { - bool: parse_bool, - datetime.datetime: parse_datetime, - datetime.date: parse_date, - datetime.time: parse_time, - pydantic.Json: json.loads, - decimal.Decimal: decimal.Decimal, +ADDITIONAL_PARAMETERS_MAP: Dict[type, str] = { + bytes: "represent_as_base64_str", + decimal.Decimal: "decimal_places", +} + + +DECODERS_MAP: Dict[type, Callable] = { + bool: parse_bool, + datetime.datetime: SchemaValidator(core_schema.datetime_schema()).validate_python, + datetime.date: SchemaValidator(core_schema.date_schema()).validate_python, + datetime.time: SchemaValidator(core_schema.time_schema()).validate_python, + pydantic.Json: json.loads, + decimal.Decimal: lambda x, precision: decimal.Decimal( + x, context=decimal.Context(prec=precision) + ), + bytes: decode_bytes, } diff --git a/ormar/fields/referential_actions.py b/ormar/fields/referential_actions.py index cde15ad..06e5cf5 100644 --- a/ormar/fields/referential_actions.py +++ b/ormar/fields/referential_actions.py @@ -2,7 +2,6 @@ Gathers all referential actions by ormar. """ - from enum import Enum diff --git a/ormar/fields/sqlalchemy_encrypted.py b/ormar/fields/sqlalchemy_encrypted.py index 88dc0af..1ad13f9 100644 --- a/ormar/fields/sqlalchemy_encrypted.py +++ b/ormar/fields/sqlalchemy_encrypted.py @@ -2,14 +2,14 @@ import abc import base64 from enum import Enum -from typing import Any, Callable, Optional, TYPE_CHECKING, Type, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type, Union import sqlalchemy.types as types -from pydantic.utils import lenient_issubclass from sqlalchemy.engine import Dialect import ormar # noqa: I100, I202 from ormar import ModelDefinitionError # noqa: I202, I100 +from ormar.fields.parsers import ADDITIONAL_PARAMETERS_MAP cryptography = None try: # pragma: nocover @@ -119,7 +119,7 @@ class EncryptedString(types.TypeDecorator): self, encrypt_secret: Union[str, Callable], encrypt_backend: EncryptBackends = EncryptBackends.FERNET, - encrypt_custom_backend: Type[EncryptBackend] = None, + encrypt_custom_backend: Optional[Type[EncryptBackend]] = None, **kwargs: Any, ) -> None: _field_type = kwargs.pop("_field_type") @@ -129,7 +129,11 @@ class EncryptedString(types.TypeDecorator): "In order to encrypt a column 'cryptography' is required!" ) backend = BACKENDS_MAP.get(encrypt_backend, encrypt_custom_backend) - if not backend or not lenient_issubclass(backend, EncryptBackend): + if ( + not backend + or not isinstance(backend, type) + or not issubclass(backend, EncryptBackend) + ): raise ModelDefinitionError("Wrong or no encrypt backend provided!") self.backend: EncryptBackend = backend() @@ -160,9 +164,14 @@ class EncryptedString(types.TypeDecorator): try: value = self._underlying_type.process_bind_param(value, dialect) except AttributeError: - encoder = ormar.SQL_ENCODERS_MAP.get(self.type_, None) - if encoder: - value = encoder(value) # type: ignore + encoder, additional_parameter = self._get_coder_type_and_params( + coders=ormar.SQL_ENCODERS_MAP + ) + if encoder is not None: + params = [value] + ( + [additional_parameter] if additional_parameter else [] + ) + value = encoder(*params) encrypted_value = self.backend.encrypt(value) return encrypted_value @@ -175,8 +184,24 @@ class EncryptedString(types.TypeDecorator): try: return self._underlying_type.process_result_value(decrypted_value, dialect) except AttributeError: - decoder = ormar.DECODERS_MAP.get(self.type_, None) - if decoder: - return decoder(decrypted_value) # type: ignore + decoder, additional_parameter = self._get_coder_type_and_params( + coders=ormar.DECODERS_MAP + ) + if decoder is not None: + params = [decrypted_value] + ( + [additional_parameter] if additional_parameter else [] + ) + return decoder(*params) # type: ignore return self._field_type.__type__(decrypted_value) # type: ignore + + def _get_coder_type_and_params( + self, coders: Dict[type, Callable] + ) -> Tuple[Optional[Callable], Optional[str]]: + coder = coders.get(self.type_, None) + additional_parameter: Optional[str] = None + if self.type_ in ADDITIONAL_PARAMETERS_MAP: + additional_parameter = getattr( + self._field_type, ADDITIONAL_PARAMETERS_MAP[self.type_] + ) + return coder, additional_parameter diff --git a/ormar/fields/through_field.py b/ormar/fields/through_field.py index a469ab3..6165816 100644 --- a/ormar/fields/through_field.py +++ b/ormar/fields/through_field.py @@ -1,13 +1,14 @@ import sys -from typing import Any, TYPE_CHECKING, Type, Union +from typing import TYPE_CHECKING, Any, Optional, Type, Union from ormar.fields.base import BaseField from ormar.fields.foreign_key import ForeignKeyField if TYPE_CHECKING: # pragma no cover - from ormar import Model from pydantic.typing import ForwardRef + from ormar import Model + if sys.version_info < (3, 7): ToType = Type[Model] else: @@ -15,7 +16,11 @@ if TYPE_CHECKING: # pragma no cover def Through( # noqa CFQ002 - to: "ToType", *, name: str = None, related_name: str = None, **kwargs: Any + to: "ToType", + *, + name: Optional[str] = None, + related_name: Optional[str] = None, + **kwargs: Any ) -> Any: """ Despite a name it's a function that returns constructed ThroughField. @@ -50,7 +55,6 @@ def Through( # noqa CFQ002 column_type=None, primary_key=False, index=False, - pydantic_only=False, default=None, server_default=None, is_relation=True, diff --git a/ormar/models/__init__.py b/ormar/models/__init__.py index 1bac952..fa7bf84 100644 --- a/ormar/models/__init__.py +++ b/ormar/models/__init__.py @@ -9,5 +9,14 @@ from ormar.models.model_row import ModelRow # noqa I100 from ormar.models.model import Model, T # noqa I100 from ormar.models.excludable import ExcludableItems # noqa I100 from ormar.models.utils import Extra # noqa I100 +from ormar.models.ormar_config import OrmarConfig # noqa I100 -__all__ = ["NewBaseModel", "Model", "ModelRow", "ExcludableItems", "T", "Extra"] +__all__ = [ + "NewBaseModel", + "Model", + "ModelRow", + "ExcludableItems", + "T", + "Extra", + "OrmarConfig", +] diff --git a/ormar/models/descriptors/__init__.py b/ormar/models/descriptors/__init__.py index 459e48d..58e29e9 100644 --- a/ormar/models/descriptors/__init__.py +++ b/ormar/models/descriptors/__init__.py @@ -2,7 +2,6 @@ from ormar.models.descriptors.descriptors import ( BytesDescriptor, JsonDescriptor, PkDescriptor, - PropertyDescriptor, PydanticDescriptor, RelationDescriptor, ) @@ -10,7 +9,6 @@ from ormar.models.descriptors.descriptors import ( __all__ = [ "PydanticDescriptor", "RelationDescriptor", - "PropertyDescriptor", "PkDescriptor", "JsonDescriptor", "BytesDescriptor", diff --git a/ormar/models/descriptors/descriptors.py b/ormar/models/descriptors/descriptors.py index 35f4194..40e96d7 100644 --- a/ormar/models/descriptors/descriptors.py +++ b/ormar/models/descriptors/descriptors.py @@ -1,7 +1,7 @@ import base64 -from typing import Any, TYPE_CHECKING, Type +from typing import TYPE_CHECKING, Any, Type -from ormar.fields.parsers import encode_json +from ormar.fields.parsers import decode_bytes, encode_json if TYPE_CHECKING: # pragma: no cover from ormar import Model @@ -53,7 +53,7 @@ class BytesDescriptor: def __get__(self, instance: "Model", owner: Type["Model"]) -> Any: value = instance.__dict__.get(self.name, None) - field = instance.Meta.model_fields[self.name] + field = instance.ormar_config.model_fields[self.name] if ( value is not None and field.represent_as_base64_str @@ -63,12 +63,11 @@ class BytesDescriptor: return value def __set__(self, instance: "Model", value: Any) -> None: - field = instance.Meta.model_fields[self.name] + field = instance.ormar_config.model_fields[self.name] if isinstance(value, str): - if field.represent_as_base64_str: - value = base64.b64decode(value) - else: - value = value.encode("utf-8") + value = decode_bytes( + value=value, represent_as_string=field.represent_as_base64_str + ) instance._internal_set(self.name, value) instance.set_save_status(False) @@ -107,36 +106,9 @@ class RelationDescriptor: return None # pragma no cover def __set__(self, instance: "Model", value: Any) -> None: - model = instance.Meta.model_fields[self.name].expand_relationship( + instance.ormar_config.model_fields[self.name].expand_relationship( value=value, child=instance ) - if isinstance(instance.__dict__.get(self.name), list): - # virtual foreign key or many to many - # TODO: Fix double items in dict, no effect on real action just ugly repr - instance.__dict__[self.name].append(model) - else: - # foreign key relation - instance.__dict__[self.name] = model + + if not isinstance(instance.__dict__.get(self.name), list): instance.set_save_status(False) - - -class PropertyDescriptor: - """ - Property descriptor handles methods decorated with @property_field decorator. - They are read only. - """ - - def __init__(self, name: str, function: Any) -> None: - self.name = name - self.function = function - - def __get__(self, instance: "Model", owner: Type["Model"]) -> Any: - if instance is None: - return self - if instance is not None and self.function is not None: - bound = self.function.__get__(instance, instance.__class__) - return bound() if callable(bound) else bound - - def __set__(self, instance: "Model", value: Any) -> None: # pragma: no cover - # kept here so it's a data-descriptor and precedes __dict__ lookup - pass diff --git a/ormar/models/excludable.py b/ormar/models/excludable.py index 59125c5..2759ad0 100644 --- a/ormar/models/excludable.py +++ b/ormar/models/excludable.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import Dict, List, Set, TYPE_CHECKING, Tuple, Type, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union from ormar.queryset.utils import get_relationship_alias_model_and_str @@ -186,7 +186,7 @@ class ExcludableItems: source_model: Type["Model"], model_cls: Type["Model"], is_exclude: bool, - related_items: List = None, + related_items: Optional[List] = None, alias: str = "", ) -> None: """ diff --git a/ormar/models/helpers/__init__.py b/ormar/models/helpers/__init__.py index 0d9e6ce..5d4c1de 100644 --- a/ormar/models/helpers/__init__.py +++ b/ormar/models/helpers/__init__.py @@ -1,43 +1,41 @@ from ormar.models.helpers.models import ( - check_required_meta_parameters, + check_required_config_parameters, + config_field_not_set, extract_annotations_and_default_vals, - meta_field_not_set, populate_default_options_values, ) from ormar.models.helpers.pydantic import ( get_potential_fields, get_pydantic_base_orm_config, - get_pydantic_field, merge_or_generate_pydantic_config, remove_excluded_parent_fields, ) from ormar.models.helpers.relations import ( alias_manager, + expand_reverse_relationships, register_relation_in_alias_manager, ) -from ormar.models.helpers.relations import expand_reverse_relationships from ormar.models.helpers.sqlalchemy import ( - populate_meta_sqlalchemy_table_if_required, - populate_meta_tablename_columns_and_pk, + populate_config_sqlalchemy_table_if_required, + populate_config_tablename_columns_and_pk, sqlalchemy_columns_from_model_fields, ) -from ormar.models.helpers.validation import populate_choices_validators +from ormar.models.helpers.validation import modify_schema_example __all__ = [ "expand_reverse_relationships", "extract_annotations_and_default_vals", - "populate_meta_tablename_columns_and_pk", - "populate_meta_sqlalchemy_table_if_required", + "populate_config_tablename_columns_and_pk", + "populate_config_sqlalchemy_table_if_required", "populate_default_options_values", "alias_manager", "register_relation_in_alias_manager", - "get_pydantic_field", "get_potential_fields", "get_pydantic_base_orm_config", "merge_or_generate_pydantic_config", - "check_required_meta_parameters", + "check_required_config_parameters", "sqlalchemy_columns_from_model_fields", - "populate_choices_validators", - "meta_field_not_set", + "config_field_not_set", "remove_excluded_parent_fields", + "modify_schema_example", ] diff --git a/ormar/models/helpers/models.py b/ormar/models/helpers/models.py index 5bc0a0f..e05ed16 100644 --- a/ormar/models/helpers/models.py +++ b/ormar/models/helpers/models.py @@ -1,12 +1,11 @@ import itertools import sqlite3 -from typing import Any, Dict, List, TYPE_CHECKING, Tuple, Type +from typing import TYPE_CHECKING, Any, Dict, ForwardRef, List, Tuple, Type import pydantic -from pydantic.typing import ForwardRef + import ormar # noqa: I100 from ormar.models.helpers.pydantic import populate_pydantic_default_values -from ormar.models.utils import Extra if TYPE_CHECKING: # pragma no cover from ormar import Model @@ -32,7 +31,7 @@ def populate_default_options_values( # noqa: CCR001 new_model: Type["Model"], model_fields: Dict ) -> None: """ - Sets all optional Meta values to it's defaults + Sets all optional OrmarConfig values to its defaults and set model_fields that were already previously extracted. Here should live all options that are not overwritten/set for all models. @@ -46,38 +45,19 @@ def populate_default_options_values( # noqa: CCR001 :param model_fields: dict of model fields :type model_fields: Union[Dict[str, type], Dict] """ - defaults = { - "queryset_class": ormar.QuerySet, - "constraints": [], - "model_fields": model_fields, - "abstract": False, - "extra": Extra.forbid, - "orders_by": [], - "exclude_parent_fields": [], - } - for key, value in defaults.items(): - if not hasattr(new_model.Meta, key): - setattr(new_model.Meta, key, value) - - if any( - is_field_an_forward_ref(field) for field in new_model.Meta.model_fields.values() - ): - new_model.Meta.requires_ref_update = True - else: - new_model.Meta.requires_ref_update = False + new_model.ormar_config.model_fields.update(model_fields) + if any(is_field_an_forward_ref(field) for field in model_fields.values()): + new_model.ormar_config.requires_ref_update = True new_model._json_fields = { - name - for name, field in new_model.Meta.model_fields.items() - if field.__type__ == pydantic.Json + name for name, field in model_fields.items() if field.__type__ == pydantic.Json } new_model._bytes_fields = { - name - for name, field in new_model.Meta.model_fields.items() - if field.__type__ == bytes + name for name, field in model_fields.items() if field.__type__ == bytes } new_model.__relation_map__ = None + new_model.__ormar_fields_validators__ = None class Connection(sqlite3.Connection): @@ -94,7 +74,7 @@ def substitue_backend_pool_for_sqlite(new_model: Type["Model"]) -> None: :param new_model: newly declared ormar Model :type new_model: Model class """ - backend = new_model.Meta.database._backend + backend = new_model.ormar_config.database._backend if ( backend._dialect.name == "sqlite" and "factory" not in backend._options ): # pragma: no cover @@ -103,7 +83,7 @@ def substitue_backend_pool_for_sqlite(new_model: Type["Model"]) -> None: backend._pool = old_pool.__class__(backend._database_url, **backend._options) -def check_required_meta_parameters(new_model: Type["Model"]) -> None: +def check_required_config_parameters(new_model: Type["Model"]) -> None: """ Verifies if ormar.Model has database and metadata set. @@ -112,20 +92,17 @@ def check_required_meta_parameters(new_model: Type["Model"]) -> None: :param new_model: newly declared ormar Model :type new_model: Model class """ - if not hasattr(new_model.Meta, "database"): - if not getattr(new_model.Meta, "abstract", False): - raise ormar.ModelDefinitionError( - f"{new_model.__name__} does not have database defined." - ) - - else: + if new_model.ormar_config.database is None and not new_model.ormar_config.abstract: + raise ormar.ModelDefinitionError( + f"{new_model.__name__} does not have database defined." + ) + elif not new_model.ormar_config.abstract: substitue_backend_pool_for_sqlite(new_model=new_model) - if not hasattr(new_model.Meta, "metadata"): - if not getattr(new_model.Meta, "abstract", False): - raise ormar.ModelDefinitionError( - f"{new_model.__name__} does not have metadata defined." - ) + if new_model.ormar_config.metadata is None and not new_model.ormar_config.abstract: + raise ormar.ModelDefinitionError( + f"{new_model.__name__} does not have metadata defined." + ) def extract_annotations_and_default_vals(attrs: Dict) -> Tuple[Dict, Dict]: @@ -176,9 +153,9 @@ def group_related_list(list_: List) -> Dict: return dict(sorted(result_dict.items(), key=lambda item: len(item[1]))) -def meta_field_not_set(model: Type["Model"], field_name: str) -> bool: +def config_field_not_set(model: Type["Model"], field_name: str) -> bool: """ - Checks if field with given name is already present in model.Meta. + Checks if field with given name is already present in model.OrmarConfig. Then check if it's set to something truthful (in practice meaning not None, as it's non or ormar Field only). @@ -189,4 +166,4 @@ def meta_field_not_set(model: Type["Model"], field_name: str) -> bool: :return: result of the check :rtype: bool """ - return not hasattr(model.Meta, field_name) or not getattr(model.Meta, field_name) + return not getattr(model.ormar_config, field_name) diff --git a/ormar/models/helpers/pydantic.py b/ormar/models/helpers/pydantic.py index 7b5fb82..27629b9 100644 --- a/ormar/models/helpers/pydantic.py +++ b/ormar/models/helpers/pydantic.py @@ -1,10 +1,9 @@ -import inspect from types import MappingProxyType -from typing import Dict, Optional, TYPE_CHECKING, Tuple, Type, Union +from typing import TYPE_CHECKING, Dict, Optional, Tuple, Type, Union import pydantic -from pydantic.fields import ModelField -from pydantic.utils import lenient_issubclass +from pydantic import ConfigDict +from pydantic.fields import FieldInfo from ormar.exceptions import ModelDefinitionError # noqa: I100, I202 from ormar.fields import BaseField @@ -30,35 +29,10 @@ def create_pydantic_field( :param model_field: relation field from which through model is extracted :type model_field: ManyToManyField class """ - model_field.through.__fields__[field_name] = ModelField( - name=field_name, - type_=model, - model_config=model.__config__, - required=False, - class_validators={}, - ) - - -def get_pydantic_field(field_name: str, model: Type["Model"]) -> "ModelField": - """ - Extracts field type and if it's required from Model model_fields by passed - field_name. Returns a pydantic field with type of field_name field type. - - :param field_name: field name to fetch from Model and name of pydantic field - :type field_name: str - :param model: type of field to register - :type model: Model class - :return: newly created pydantic field - :rtype: pydantic.ModelField - """ - type_ = model.Meta.model_fields[field_name].__type__ - return ModelField( - name=field_name, - type_=type_, # type: ignore - model_config=model.__config__, - required=not model.Meta.model_fields[field_name].nullable, - class_validators={}, + model_field.through.model_fields[field_name] = FieldInfo.from_annotated_attribute( + annotation=Optional[model], default=None # type: ignore ) + model_field.through.model_rebuild(force=True) def populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict]: @@ -107,23 +81,21 @@ def merge_or_generate_pydantic_config(attrs: Dict, name: str) -> None: :rtype: None """ - DefaultConfig = get_pydantic_base_orm_config() - if "Config" in attrs: - ProvidedConfig = attrs["Config"] - if not inspect.isclass(ProvidedConfig): + default_config = get_pydantic_base_orm_config() + if "model_config" in attrs: + provided_config = attrs["model_config"] + if not isinstance(provided_config, dict): raise ModelDefinitionError( - f"Config provided for class {name} has to be a class." + f"Config provided for class {name} has to be a dictionary." ) - class Config(ProvidedConfig, DefaultConfig): # type: ignore - pass - - attrs["Config"] = Config + config = {**default_config, **provided_config} + attrs["model_config"] = config else: - attrs["Config"] = DefaultConfig + attrs["model_config"] = default_config -def get_pydantic_base_orm_config() -> Type[pydantic.BaseConfig]: +def get_pydantic_base_orm_config() -> pydantic.ConfigDict: """ Returns empty pydantic Config with orm_mode set to True. @@ -131,11 +103,7 @@ def get_pydantic_base_orm_config() -> Type[pydantic.BaseConfig]: :rtype: pydantic Config """ - class Config(pydantic.BaseConfig): - orm_mode = True - validate_assignment = True - - return Config + return ConfigDict(validate_assignment=True, ser_json_bytes="base64") def get_potential_fields(attrs: Union[Dict, MappingProxyType]) -> Dict: @@ -150,7 +118,10 @@ def get_potential_fields(attrs: Union[Dict, MappingProxyType]) -> Dict: return { k: v for k, v in attrs.items() - if (lenient_issubclass(v, BaseField) or isinstance(v, BaseField)) + if ( + (isinstance(v, type) and issubclass(v, BaseField)) + or isinstance(v, BaseField) + ) } @@ -161,8 +132,11 @@ def remove_excluded_parent_fields(model: Type["Model"]) -> None: :param model: :type model: Type["Model"] """ - excludes = {*model.Meta.exclude_parent_fields} - {*model.Meta.model_fields.keys()} + excludes = {*model.ormar_config.exclude_parent_fields} - { + *model.ormar_config.model_fields.keys() + } if excludes: - model.__fields__ = { - k: v for k, v in model.__fields__.items() if k not in excludes + model.model_fields = { + k: v for k, v in model.model_fields.items() if k not in excludes } + model.model_rebuild(force=True) diff --git a/ormar/models/helpers/related_names_validation.py b/ormar/models/helpers/related_names_validation.py index 56497b2..d086ed0 100644 --- a/ormar/models/helpers/related_names_validation.py +++ b/ormar/models/helpers/related_names_validation.py @@ -1,6 +1,5 @@ -from typing import Dict, List, Optional, TYPE_CHECKING, Type +from typing import TYPE_CHECKING, Dict, ForwardRef, List, Optional, Type -from pydantic.typing import ForwardRef import ormar # noqa: I100 if TYPE_CHECKING: # pragma no cover diff --git a/ormar/models/helpers/relations.py b/ormar/models/helpers/relations.py index c6d1ba4..823efc6 100644 --- a/ormar/models/helpers/relations.py +++ b/ormar/models/helpers/relations.py @@ -1,4 +1,13 @@ -from typing import TYPE_CHECKING, Type, cast +import inspect +import warnings +from typing import TYPE_CHECKING, Any, List, Optional, Type, Union, cast + +from pydantic import BaseModel, create_model, field_serializer +from pydantic._internal._decorators import DecoratorInfos +from pydantic.fields import FieldInfo +from pydantic_core.core_schema import ( + SerializerFunctionWrapHandler, +) import ormar from ormar import ForeignKey, ManyToMany @@ -9,7 +18,7 @@ from ormar.relations import AliasManager if TYPE_CHECKING: # pragma no cover from ormar import Model - from ormar.fields import ManyToManyField, ForeignKeyField + from ormar.fields import ForeignKeyField, ManyToManyField alias_manager = AliasManager() @@ -82,7 +91,7 @@ def expand_reverse_relationships(model: Type["Model"]) -> None: :param model: model on which relation should be checked and registered :type model: Model class """ - model_fields = list(model.Meta.model_fields.values()) + model_fields = list(model.ormar_config.model_fields.values()) for model_field in model_fields: if model_field.is_relation and not model_field.has_unresolved_forward_refs(): model_field = cast("ForeignKeyField", model_field) @@ -101,9 +110,9 @@ def register_reverse_model_fields(model_field: "ForeignKeyField") -> None: :type model_field: relation Field """ related_name = model_field.get_related_name() - # TODO: Reverse relations does not register pydantic fields? + related_model_fields = model_field.to.ormar_config.model_fields if model_field.is_multi: - model_field.to.Meta.model_fields[related_name] = ManyToMany( # type: ignore + related_model_fields[related_name] = ManyToMany( # type: ignore model_field.owner, through=model_field.through, name=related_name, @@ -122,7 +131,7 @@ def register_reverse_model_fields(model_field: "ForeignKeyField") -> None: register_through_shortcut_fields(model_field=model_field) adjust_through_many_to_many_model(model_field=model_field) else: - model_field.to.Meta.model_fields[related_name] = ForeignKey( # type: ignore + related_model_fields[related_name] = ForeignKey( # type: ignore model_field.owner, real_name=related_name, virtual=True, @@ -133,9 +142,97 @@ def register_reverse_model_fields(model_field: "ForeignKeyField") -> None: skip_field=model_field.skip_reverse, ) if not model_field.skip_reverse: + field_type = related_model_fields[related_name].__type__ + field_type = replace_models_with_copy( + annotation=field_type, source_model_field=model_field.name + ) + if not model_field.is_multi: + field_type = Union[field_type, List[field_type], None] # type: ignore + model_field.to.model_fields[related_name] = FieldInfo.from_annotated_attribute( + annotation=field_type, default=None + ) + add_field_serializer_for_reverse_relations( + to_model=model_field.to, related_name=related_name + ) + model_field.to.model_rebuild(force=True) setattr(model_field.to, related_name, RelationDescriptor(name=related_name)) +def add_field_serializer_for_reverse_relations( + to_model: Type["Model"], related_name: str +) -> None: + def serialize( + self: "Model", children: List["Model"], handler: SerializerFunctionWrapHandler + ) -> Any: + """ + Serialize a list of nodes, handling circular references + by excluding the children. + """ + try: + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", message="Pydantic serializer warnings" + ) + return handler(children) + except ValueError as exc: + if not str(exc).startswith("Circular reference"): # pragma: no cover + raise exc + + result = [] + for child in children: + # If there is one circular ref dump all children as pk only + result.append({child.ormar_config.pkname: child.pk}) + return result + + decorator = field_serializer(related_name, mode="wrap", check_fields=False)( + serialize + ) + setattr(to_model, f"serialize_{related_name}", decorator) + DecoratorInfos.build(to_model) + + +def replace_models_with_copy( + annotation: Type, source_model_field: Optional[str] = None +) -> Any: + """ + Replaces all models in annotation with their copies to avoid circular references. + + :param annotation: annotation to replace models in + :type annotation: Type + :return: annotation with replaced models + :rtype: Type + """ + if inspect.isclass(annotation) and issubclass(annotation, ormar.Model): + return create_copy_to_avoid_circular_references(model=annotation) + elif hasattr(annotation, "__origin__") and annotation.__origin__ in {list, Union}: + if annotation.__origin__ == list: + return List[ # type: ignore + replace_models_with_copy( + annotation=annotation.__args__[0], + source_model_field=source_model_field, + ) + ] + elif annotation.__origin__ == Union: + args = annotation.__args__ + new_args = [ + replace_models_with_copy( + annotation=arg, source_model_field=source_model_field + ) + for arg in args + ] + return Union[tuple(new_args)] + else: + return annotation + + +def create_copy_to_avoid_circular_references(model: Type["Model"]) -> Type["BaseModel"]: + new_model = create_model( + model.__name__, + __base__=model, + ) + return new_model + + def register_through_shortcut_fields(model_field: "ManyToManyField") -> None: """ Registers m2m relation through shortcut on both ends of the relation. @@ -147,7 +244,7 @@ def register_through_shortcut_fields(model_field: "ManyToManyField") -> None: through_name = through_model.get_name(lower=True) related_name = model_field.get_related_name() - model_field.owner.Meta.model_fields[through_name] = Through( + model_field.owner.ormar_config.model_fields[through_name] = Through( through_model, real_name=through_name, virtual=True, @@ -156,7 +253,7 @@ def register_through_shortcut_fields(model_field: "ManyToManyField") -> None: nullable=True, ) - model_field.to.Meta.model_fields[through_name] = Through( + model_field.to.ormar_config.model_fields[through_name] = Through( through_model, real_name=through_name, virtual=True, @@ -209,10 +306,13 @@ def verify_related_name_dont_duplicate( :return: None :rtype: None """ - fk_field = model_field.to.Meta.model_fields.get(related_name) + fk_field = model_field.to.ormar_config.model_fields.get(related_name) if not fk_field: # pragma: no cover return - if fk_field.to != model_field.owner and fk_field.to.Meta != model_field.owner.Meta: + if ( + fk_field.to != model_field.owner + and fk_field.to.ormar_config != model_field.owner.ormar_config + ): raise ormar.ModelDefinitionError( f"Relation with related_name " f"'{related_name}' " @@ -237,8 +337,10 @@ def reverse_field_not_already_registered(model_field: "ForeignKeyField") -> bool :rtype: bool """ related_name = model_field.get_related_name() - check_result = related_name not in model_field.to.Meta.model_fields - check_result2 = model_field.owner.get_name() not in model_field.to.Meta.model_fields + check_result = related_name not in model_field.to.ormar_config.model_fields + check_result2 = ( + model_field.owner.get_name() not in model_field.to.ormar_config.model_fields + ) if not check_result: verify_related_name_dont_duplicate( diff --git a/ormar/models/helpers/sqlalchemy.py b/ormar/models/helpers/sqlalchemy.py index b0ade1d..1a4fd97 100644 --- a/ormar/models/helpers/sqlalchemy.py +++ b/ormar/models/helpers/sqlalchemy.py @@ -1,8 +1,7 @@ import logging -from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Type, Union +from typing import TYPE_CHECKING, Dict, ForwardRef, List, Optional, Tuple, Type, Union import sqlalchemy -from pydantic.typing import ForwardRef import ormar # noqa: I100, I202 from ormar.models.descriptors import RelationDescriptor @@ -12,8 +11,9 @@ from ormar.models.helpers.related_names_validation import ( ) if TYPE_CHECKING: # pragma no cover - from ormar import Model, ModelMeta, ManyToManyField, BaseField, ForeignKeyField + from ormar import BaseField, ForeignKeyField, ManyToManyField, Model from ormar.models import NewBaseModel + from ormar.models.ormar_config import OrmarConfig def adjust_through_many_to_many_model(model_field: "ManyToManyField") -> None: @@ -28,7 +28,7 @@ def adjust_through_many_to_many_model(model_field: "ManyToManyField") -> None: """ parent_name = model_field.default_target_field_name() child_name = model_field.default_source_field_name() - model_fields = model_field.through.Meta.model_fields + model_fields = model_field.through.ormar_config.model_fields model_fields[parent_name] = ormar.ForeignKey( # type: ignore model_field.to, real_name=parent_name, @@ -63,7 +63,8 @@ def create_and_append_m2m_fk( """ Registers sqlalchemy Column with sqlalchemy.ForeignKey leading to the model. - Newly created field is added to m2m relation through model Meta columns and table. + Newly created field is added to m2m relation + through model OrmarConfig columns and table. :param field_name: name of the column to create :type field_name: str @@ -72,8 +73,10 @@ def create_and_append_m2m_fk( :param model_field: field with ManyToMany relation :type model_field: ManyToManyField field """ - pk_alias = model.get_column_alias(model.Meta.pkname) - pk_column = next((col for col in model.Meta.columns if col.name == pk_alias), None) + pk_alias = model.get_column_alias(model.ormar_config.pkname) + pk_column = next( + (col for col in model.ormar_config.columns if col.name == pk_alias), None + ) if pk_column is None: # pragma: no cover raise ormar.ModelDefinitionError( "ManyToMany relation cannot lead to field without pk" @@ -82,15 +85,15 @@ def create_and_append_m2m_fk( field_name, pk_column.type, sqlalchemy.schema.ForeignKey( - model.Meta.tablename + "." + pk_alias, + model.ormar_config.tablename + "." + pk_alias, ondelete="CASCADE", onupdate="CASCADE", - name=f"fk_{model_field.through.Meta.tablename}_{model.Meta.tablename}" + name=f"fk_{model_field.through.ormar_config.tablename}_{model.ormar_config.tablename}" f"_{field_name}_{pk_alias}", ), ) - model_field.through.Meta.columns.append(column) - model_field.through.Meta.table.append_column(column) + model_field.through.ormar_config.columns.append(column) + model_field.through.ormar_config.table.append_column(column) def check_pk_column_validity( @@ -98,10 +101,9 @@ def check_pk_column_validity( ) -> Optional[str]: """ Receives the field marked as primary key and verifies if the pkname - was not already set (only one allowed per model) and if field is not marked - as pydantic_only as it needs to be a database field. + was not already set (only one allowed per model). - :raises ModelDefintionError: if pkname already set or field is pydantic_only + :raises ModelDefintionError: if pkname already set :param field_name: name of field :type field_name: str :param field: ormar.Field @@ -113,8 +115,6 @@ def check_pk_column_validity( """ if pkname is not None: raise ormar.ModelDefinitionError("Only one primary key column is allowed.") - if field.pydantic_only: - raise ormar.ModelDefinitionError("Primary key column cannot be pydantic only") return field_name @@ -132,11 +132,7 @@ def sqlalchemy_columns_from_model_fields( are leading to the same related model only one can have empty related_name param. Also related_names have to be unique. - Trigger validation of primary_key - only one and required pk can be set, - cannot be pydantic_only. - - Append fields to columns if it's not pydantic_only, - virtual ForeignKey or ManyToMany field. + Trigger validation of primary_key - only one and required pk can be set Sets `owner` on each model_field as reference to newly created Model. @@ -152,7 +148,7 @@ def sqlalchemy_columns_from_model_fields( if len(model_fields.keys()) == 0: model_fields["id"] = ormar.Integer(name="id", primary_key=True) logging.warning( - f"Table {new_model.Meta.tablename} had no fields so auto " + f"Table {new_model.ormar_config.tablename} had no fields so auto " "Integer primary key named `id` created." ) validate_related_names_in_relations(model_fields, new_model) @@ -166,11 +162,6 @@ def _process_fields( Helper method. Populates pkname and columns. - Trigger validation of primary_key - only one and required pk can be set, - cannot be pydantic_only. - - Append fields to columns if it's not pydantic_only, - virtual ForeignKey or ManyToMany field. Sets `owner` on each model_field as reference to newly created Model. @@ -215,17 +206,17 @@ def _is_db_field(field: "BaseField") -> bool: :return: result of the check :rtype: bool """ - return not field.pydantic_only and not field.virtual and not field.is_multi + return not field.virtual and not field.is_multi -def populate_meta_tablename_columns_and_pk( +def populate_config_tablename_columns_and_pk( name: str, new_model: Type["Model"] ) -> Type["Model"]: """ - Sets Model tablename if it's not already set in Meta. + Sets Model tablename if it's not already set in OrmarConfig. Default tablename if not present is class name lower + s (i.e. Bed becomes -> beds) - Checks if Model's Meta have pkname and columns set. + Checks if Model's OrmarConfig have pkname and columns set. If not calls the sqlalchemy_columns_from_model_fields to populate columns from ormar.fields definitions. @@ -236,77 +227,79 @@ def populate_meta_tablename_columns_and_pk( :type name: str :param new_model: currently constructed Model :type new_model: ormar.models.metaclass.ModelMetaclass - :return: Model with populated pkname and columns in Meta + :return: Model with populated pkname and columns in OrmarConfig :rtype: ormar.models.metaclass.ModelMetaclass """ tablename = name.lower() + "s" - new_model.Meta.tablename = ( - new_model.Meta.tablename if hasattr(new_model.Meta, "tablename") else tablename + new_model.ormar_config.tablename = ( + new_model.ormar_config.tablename + if new_model.ormar_config.tablename + else tablename ) pkname: Optional[str] - if hasattr(new_model.Meta, "columns"): - columns = new_model.Meta.columns - pkname = new_model.Meta.pkname + if new_model.ormar_config.columns: + columns = new_model.ormar_config.columns + pkname = new_model.ormar_config.pkname else: pkname, columns = sqlalchemy_columns_from_model_fields( - new_model.Meta.model_fields, new_model + new_model.ormar_config.model_fields, new_model ) if pkname is None: raise ormar.ModelDefinitionError("Table has to have a primary key.") - new_model.Meta.columns = columns - new_model.Meta.pkname = pkname - if not new_model.Meta.orders_by: - # by default we sort by pk name if other option not provided - new_model.Meta.orders_by.append(pkname) + new_model.ormar_config.columns = columns + new_model.ormar_config.pkname = pkname + if not new_model.ormar_config.orders_by: + # by default, we sort by pk name if other option not provided + new_model.ormar_config.orders_by.append(pkname) return new_model -def check_for_null_type_columns_from_forward_refs(meta: "ModelMeta") -> bool: +def check_for_null_type_columns_from_forward_refs(config: "OrmarConfig") -> bool: """ Check is any column is of NUllType() meaning it's empty column from ForwardRef - :param meta: Meta class of the Model without sqlalchemy table constructed - :type meta: Model class Meta + :param config: OrmarConfig of the Model without sqlalchemy table constructed + :type config: Model class OrmarConfig :return: result of the check :rtype: bool """ return not any( - isinstance(col.type, sqlalchemy.sql.sqltypes.NullType) for col in meta.columns + isinstance(col.type, sqlalchemy.sql.sqltypes.NullType) for col in config.columns ) -def populate_meta_sqlalchemy_table_if_required(meta: "ModelMeta") -> None: +def populate_config_sqlalchemy_table_if_required(config: "OrmarConfig") -> None: """ - Constructs sqlalchemy table out of columns and parameters set on Meta class. + Constructs sqlalchemy table out of columns and parameters set on OrmarConfig. It populates name, metadata, columns and constraints. - :param meta: Meta class of the Model without sqlalchemy table constructed - :type meta: Model class Meta + :param config: OrmarConfig of the Model without sqlalchemy table constructed + :type config: Model class OrmarConfig """ - if not hasattr(meta, "table") and check_for_null_type_columns_from_forward_refs( - meta + if config.table is None and check_for_null_type_columns_from_forward_refs( + config=config ): - set_constraint_names(meta=meta) + set_constraint_names(config=config) table = sqlalchemy.Table( - meta.tablename, meta.metadata, *meta.columns, *meta.constraints + config.tablename, config.metadata, *config.columns, *config.constraints ) - meta.table = table + config.table = table -def set_constraint_names(meta: "ModelMeta") -> None: +def set_constraint_names(config: "OrmarConfig") -> None: """ Populates the names on IndexColumns and UniqueColumns and CheckColumns constraints. - :param meta: Meta class of the Model without sqlalchemy table constructed - :type meta: Model class Meta + :param config: OrmarConfig of the Model without sqlalchemy table constructed + :type config: Model class OrmarConfig """ - for constraint in meta.constraints: + for constraint in config.constraints: if isinstance(constraint, sqlalchemy.UniqueConstraint) and not constraint.name: constraint.name = ( - f"uc_{meta.tablename}_" + f"uc_{config.tablename}_" f'{"_".join([str(col) for col in constraint._pending_colargs])}' ) elif ( @@ -314,12 +307,12 @@ def set_constraint_names(meta: "ModelMeta") -> None: and constraint.name == "TEMPORARY_NAME" ): constraint.name = ( - f"ix_{meta.tablename}_" + f"ix_{config.tablename}_" f'{"_".join([col for col in constraint._pending_colargs])}' ) elif isinstance(constraint, sqlalchemy.CheckConstraint) and not constraint.name: sql_condition: str = str(constraint.sqltext).replace(" ", "_") - constraint.name = f"check_{meta.tablename}_{sql_condition}" + constraint.name = f"check_{config.tablename}_{sql_condition}" def update_column_definition( @@ -335,7 +328,7 @@ def update_column_definition( :return: None :rtype: None """ - columns = model.Meta.columns + columns = model.ormar_config.columns for ind, column in enumerate(columns): if column.name == field.get_alias(): new_column = field.get_column(field.get_alias()) diff --git a/ormar/models/helpers/validation.py b/ormar/models/helpers/validation.py index d25837d..f284ce8 100644 --- a/ormar/models/helpers/validation.py +++ b/ormar/models/helpers/validation.py @@ -1,13 +1,13 @@ -import base64 import decimal import numbers from typing import ( + TYPE_CHECKING, Any, Callable, Dict, List, + Optional, Set, - TYPE_CHECKING, Type, Union, ) @@ -18,11 +18,8 @@ except ImportError: # pragma: no cover import json # type: ignore # noqa: F401 import pydantic -from pydantic.class_validators import make_generic_validator -from pydantic.fields import ModelField, SHAPE_LIST -import ormar # noqa: I100, I202 -from ormar.models.helpers.models import meta_field_not_set +from ormar.models.helpers.models import config_field_not_set from ormar.queryset.utils import translate_list_to_dict if TYPE_CHECKING: # pragma no cover @@ -30,72 +27,9 @@ if TYPE_CHECKING: # pragma no cover from ormar.fields import BaseField -def check_if_field_has_choices(field: "BaseField") -> bool: - """ - Checks if given field has choices populated. - A if it has one, a validator for this field needs to be attached. - - :param field: ormar field to check - :type field: BaseField - :return: result of the check - :rtype: bool - """ - return hasattr(field, "choices") and bool(field.choices) - - -def convert_value_if_needed(field: "BaseField", value: Any) -> Any: - """ - Converts dates to isoformat as fastapi can check this condition in routes - and the fields are not yet parsed. - Converts enums to list of it's values. - Converts uuids to strings. - Converts decimal to float with given scale. - - :param field: ormar field to check with choices - :type field: BaseField - :param value: current values of the model to verify - :type value: Any - :return: value, choices list - :rtype: Any - """ - encoder = ormar.ENCODERS_MAP.get(field.__type__, lambda x: x) - if field.__type__ == decimal.Decimal: - precision = field.scale # type: ignore - value = encoder(value, precision) - elif field.__type__ == bytes: - represent_as_string = field.represent_as_base64_str - value = encoder(value, represent_as_string) - elif encoder: - value = encoder(value) - return value - - -def generate_validator(ormar_field: "BaseField") -> Callable: - choices = ormar_field.choices - - def validate_choices(cls: type, value: Any, field: "ModelField") -> None: - """ - Validates if given value is in provided choices. - - :raises ValueError: If value is not in choices. - :param field:field to validate - :type field: BaseField - :param value: value of the field - :type value: Any - """ - adjusted_value = convert_value_if_needed(field=ormar_field, value=value) - if adjusted_value is not ormar.Undefined and adjusted_value not in choices: - raise ValueError( - f"{field.name}: '{adjusted_value}' " - f"not in allowed choices set:" - f" {choices}" - ) - return value - - return validate_choices - - -def generate_model_example(model: Type["Model"], relation_map: Dict = None) -> Dict: +def generate_model_example( + model: Type["Model"], relation_map: Optional[Dict] = None +) -> Dict: """ Generates example to be included in schema in fastapi. @@ -112,11 +46,11 @@ def generate_model_example(model: Type["Model"], relation_map: Dict = None) -> D if relation_map is not None else translate_list_to_dict(model._iterate_related_models()) ) - for name, field in model.Meta.model_fields.items(): + for name, field in model.ormar_config.model_fields.items(): populates_sample_fields_values( example=example, name=name, field=field, relation_map=relation_map ) - to_exclude = {name for name in model.Meta.model_fields} + to_exclude = {name for name in model.ormar_config.model_fields} pydantic_repr = generate_pydantic_example(pydantic_model=model, exclude=to_exclude) example.update(pydantic_repr) @@ -124,7 +58,10 @@ def generate_model_example(model: Type["Model"], relation_map: Dict = None) -> D def populates_sample_fields_values( - example: Dict[str, Any], name: str, field: "BaseField", relation_map: Dict = None + example: Dict[str, Any], + name: str, + field: "BaseField", + relation_map: Optional[Dict] = None, ) -> None: """ Iterates the field and sets fields to sample values @@ -168,7 +105,7 @@ def get_nested_model_example( def generate_pydantic_example( - pydantic_model: Type[pydantic.BaseModel], exclude: Set = None + pydantic_model: Type[pydantic.BaseModel], exclude: Optional[Set] = None ) -> Dict: """ Generates dict with example. @@ -182,14 +119,13 @@ def generate_pydantic_example( """ example: Dict[str, Any] = dict() exclude = exclude or set() - name_to_check = [name for name in pydantic_model.__fields__ if name not in exclude] + name_to_check = [ + name for name in pydantic_model.model_fields if name not in exclude + ] for name in name_to_check: - field = pydantic_model.__fields__[name] - type_ = field.type_ - if field.shape == SHAPE_LIST: - example[name] = [get_pydantic_example_repr(type_)] - else: - example[name] = get_pydantic_example_repr(type_) + field = pydantic_model.model_fields[name] + type_ = field.annotation + example[name] = get_pydantic_example_repr(type_) return example @@ -202,6 +138,8 @@ def get_pydantic_example_repr(type_: Any) -> Any: :return: representation to include in example :rtype: Any """ + if hasattr(type_, "__origin__"): + return generate_example_for_nested_types(type_) if issubclass(type_, (numbers.Number, decimal.Decimal)): return 0 if issubclass(type_, pydantic.BaseModel): @@ -209,6 +147,27 @@ def get_pydantic_example_repr(type_: Any) -> Any: return "string" +def generate_example_for_nested_types(type_: Any) -> Any: + """ + Process nested types like Union[X, Y] or List[X] + """ + if type_.__origin__ == Union: + return generate_example_for_union(type_=type_) + if type_.__origin__ == list: + return [get_pydantic_example_repr(type_.__args__[0])] + + +def generate_example_for_union(type_: Any) -> Any: + """ + Generates a pydantic example for Union[X, Y, ...]. + Note that Optional can also be set as Union[X, None] + """ + values = tuple( + get_pydantic_example_repr(x) for x in type_.__args__ if x is not type(None) + ) + return values[0] if len(values) == 1 else values + + def overwrite_example_and_description( schema: Dict[str, Any], model: Type["Model"] ) -> None: @@ -222,8 +181,6 @@ def overwrite_example_and_description( :type model: Type["Model"] """ schema["example"] = generate_model_example(model=model) - if "Main base class of ormar Model." in schema.get("description", ""): - schema["description"] = f"{model.__name__}" def overwrite_binary_format(schema: Dict[str, Any], model: Type["Model"]) -> None: @@ -239,42 +196,12 @@ def overwrite_binary_format(schema: Dict[str, Any], model: Type["Model"]) -> Non for field_id, prop in schema.get("properties", {}).items(): if ( field_id in model._bytes_fields - and model.Meta.model_fields[field_id].represent_as_base64_str + and model.ormar_config.model_fields[field_id].represent_as_base64_str ): prop["format"] = "base64" - if prop.get("enum"): - prop["enum"] = [ - base64.b64encode(choice).decode() for choice in prop.get("enum", []) - ] -def construct_modify_schema_function(fields_with_choices: List) -> Callable: - """ - Modifies the schema to include fields with choices validator. - Those fields will be displayed in schema as Enum types with available choices - values listed next to them. - - Note that schema extra has to be a function, otherwise it's called to soon - before all the relations are expanded. - - :param fields_with_choices: list of fields with choices validation - :type fields_with_choices: List - :return: callable that will be run by pydantic to modify the schema - :rtype: Callable - """ - - def schema_extra(schema: Dict[str, Any], model: Type["Model"]) -> None: - for field_id, prop in schema.get("properties", {}).items(): - if field_id in fields_with_choices: - prop["enum"] = list(model.Meta.model_fields[field_id].choices) - prop["description"] = prop.get("description", "") + "An enumeration." - overwrite_example_and_description(schema=schema, model=model) - overwrite_binary_format(schema=schema, model=model) - - return staticmethod(schema_extra) # type: ignore - - -def construct_schema_function_without_choices() -> Callable: +def construct_schema_function() -> Callable: """ Modifies model example and description if needed. @@ -292,28 +219,12 @@ def construct_schema_function_without_choices() -> Callable: return staticmethod(schema_extra) # type: ignore -def populate_choices_validators(model: Type["Model"]) -> None: # noqa CCR001 +def modify_schema_example(model: Type["Model"]) -> None: # noqa CCR001 """ - Checks if Model has any fields with choices set. - If yes it adds choices validation into pre root validators. + Modifies the schema example in openapi schema. :param model: newly constructed Model :type model: Model class """ - fields_with_choices = [] - if not meta_field_not_set(model=model, field_name="model_fields"): - if not hasattr(model, "_choices_fields"): - model._choices_fields = set() - for name, field in model.Meta.model_fields.items(): - if check_if_field_has_choices(field) and name not in model._choices_fields: - fields_with_choices.append(name) - validator = make_generic_validator(generate_validator(field)) - model.__fields__[name].validators.append(validator) - model._choices_fields.add(name) - - if fields_with_choices: - model.Config.schema_extra = construct_modify_schema_function( - fields_with_choices=fields_with_choices - ) - else: - model.Config.schema_extra = construct_schema_function_without_choices() + if not config_field_not_set(model=model, field_name="model_fields"): + model.model_config["json_schema_extra"] = construct_schema_function() diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 36a3e74..64dfbeb 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -1,60 +1,63 @@ +import copy +import sys +import warnings +from pathlib import Path from typing import ( + TYPE_CHECKING, Any, + Callable, Dict, List, Optional, Set, - TYPE_CHECKING, Tuple, Type, Union, cast, - Callable, ) -import databases import pydantic import sqlalchemy +from pydantic import field_serializer +from pydantic._internal._generics import PydanticGenericMetadata +from pydantic.fields import ComputedFieldInfo, FieldInfo +from pydantic_core.core_schema import SerializerFunctionWrapHandler from sqlalchemy.sql.schema import ColumnCollectionConstraint import ormar # noqa I100 import ormar.fields.constraints -from ormar.fields.constraints import UniqueColumns, IndexColumns, CheckColumns from ormar import ModelDefinitionError # noqa I100 from ormar.exceptions import ModelError from ormar.fields import BaseField +from ormar.fields.constraints import CheckColumns, IndexColumns, UniqueColumns from ormar.fields.foreign_key import ForeignKeyField from ormar.fields.many_to_many import ManyToManyField from ormar.models.descriptors import ( JsonDescriptor, PkDescriptor, - PropertyDescriptor, PydanticDescriptor, RelationDescriptor, ) from ormar.models.descriptors.descriptors import BytesDescriptor from ormar.models.helpers import ( - alias_manager, - check_required_meta_parameters, + check_required_config_parameters, + config_field_not_set, expand_reverse_relationships, extract_annotations_and_default_vals, get_potential_fields, - get_pydantic_field, merge_or_generate_pydantic_config, - meta_field_not_set, - populate_choices_validators, + modify_schema_example, + populate_config_sqlalchemy_table_if_required, + populate_config_tablename_columns_and_pk, populate_default_options_values, - populate_meta_sqlalchemy_table_if_required, - populate_meta_tablename_columns_and_pk, register_relation_in_alias_manager, remove_excluded_parent_fields, sqlalchemy_columns_from_model_fields, ) +from ormar.models.ormar_config import OrmarConfig from ormar.models.quick_access_views import quick_access_set -from ormar.models.utils import Extra from ormar.queryset import FieldAccessor, QuerySet -from ormar.relations.alias_manager import AliasManager -from ormar.signals import Signal, SignalEmitter +from ormar.signals import Signal if TYPE_CHECKING: # pragma no cover from ormar import Model @@ -64,32 +67,6 @@ CONFIG_KEY = "Config" PARSED_FIELDS_KEY = "__parsed_fields__" -class ModelMeta: - """ - Class used for type hinting. - Users can subclass this one for convenience but it's not required. - The only requirement is that ormar.Model has to have inner class with name Meta. - """ - - tablename: str - table: sqlalchemy.Table - metadata: sqlalchemy.MetaData - database: databases.Database - columns: List[sqlalchemy.Column] - constraints: List[ColumnCollectionConstraint] - pkname: str - model_fields: Dict[str, Union[BaseField, ForeignKeyField, ManyToManyField]] - alias_manager: AliasManager - property_fields: Set - signals: SignalEmitter - abstract: bool - requires_ref_update: bool - orders_by: List[str] - exclude_parent_fields: List[str] - extra: Extra - queryset_class: Type[QuerySet] - - def add_cached_properties(new_model: Type["Model"]) -> None: """ Sets cached properties for both pydantic and ormar models. @@ -108,15 +85,14 @@ def add_cached_properties(new_model: Type["Model"]) -> None: new_model._related_names = None new_model._through_names = None new_model._related_fields = None - new_model._pydantic_fields = {name for name in new_model.__fields__} new_model._json_fields = set() new_model._bytes_fields = set() def add_property_fields(new_model: Type["Model"], attrs: Dict) -> None: # noqa: CCR001 """ - Checks class namespace for properties or functions with __property_field__. - If attribute have __property_field__ it was decorated with @property_field. + Checks class namespace for properties or functions with computed_field. + If attribute have decorator_info it was decorated with @computed_field. Functions like this are exposed in dict() (therefore also fastapi result). Names of property fields are cached for quicker access / extraction. @@ -128,21 +104,22 @@ def add_property_fields(new_model: Type["Model"], attrs: Dict) -> None: # noqa: """ props = set() for var_name, value in attrs.items(): - if isinstance(value, property): - value = value.fget - field_config = getattr(value, "__property_field__", None) - if field_config: + if hasattr(value, "decorator_info") and isinstance( + value.decorator_info, ComputedFieldInfo + ): props.add(var_name) - if meta_field_not_set(model=new_model, field_name="property_fields"): - new_model.Meta.property_fields = props + if config_field_not_set(model=new_model, field_name="property_fields"): + new_model.ormar_config.property_fields = props else: - new_model.Meta.property_fields = new_model.Meta.property_fields.union(props) + new_model.ormar_config.property_fields = ( + new_model.ormar_config.property_fields.union(props) + ) def register_signals(new_model: Type["Model"]) -> None: # noqa: CCR001 """ - Registers on model's SignalEmmiter and sets pre defined signals. + Registers on model's SignalEmmiter and sets pre-defined signals. Predefined signals are (pre/post) + (save/update/delete). Signals are emitted in both model own methods and in selected queryset ones. @@ -150,8 +127,8 @@ def register_signals(new_model: Type["Model"]) -> None: # noqa: CCR001 :param new_model: newly constructed model :type new_model: Model class """ - if meta_field_not_set(model=new_model, field_name="signals"): - signals = SignalEmitter() + if config_field_not_set(model=new_model, field_name="signals"): + signals = new_model.ormar_config.signals signals.pre_save = Signal() signals.pre_update = Signal() signals.pre_delete = Signal() @@ -163,7 +140,6 @@ def register_signals(new_model: Type["Model"]) -> None: # noqa: CCR001 signals.pre_relation_remove = Signal() signals.post_relation_remove = Signal() signals.post_bulk_update = Signal() - new_model.Meta.signals = signals def verify_constraint_names( @@ -182,7 +158,9 @@ def verify_constraint_names( :type parent_value: List """ new_aliases = {x.name: x.get_alias() for x in model_fields.values()} - old_aliases = {x.name: x.get_alias() for x in base_class.Meta.model_fields.values()} + old_aliases = { + x.name: x.get_alias() for x in base_class.ormar_config.model_fields.values() + } old_aliases.update(new_aliases) constraints_columns = [x._pending_colargs for x in parent_value] for column_set in constraints_columns: @@ -224,11 +202,11 @@ def get_constraint_copy( return constructor(constraint) -def update_attrs_from_base_meta( # noqa: CCR001 +def update_attrs_from_base_config( # noqa: CCR001 base_class: "Model", attrs: Dict, model_fields: Dict ) -> None: """ - Updates Meta parameters in child from parent if needed. + Updates OrmarConfig parameters in child from parent if needed. :param base_class: one of the parent classes :type base_class: Model or model parent class @@ -240,9 +218,13 @@ def update_attrs_from_base_meta( # noqa: CCR001 params_to_update = ["metadata", "database", "constraints", "property_fields"] for param in params_to_update: - current_value = attrs.get("Meta", {}).__dict__.get(param, ormar.Undefined) + current_value = attrs.get("ormar_config", {}).__dict__.get( + param, ormar.Undefined + ) parent_value = ( - base_class.Meta.__dict__.get(param) if hasattr(base_class, "Meta") else None + base_class.ormar_config.__dict__.get(param) + if hasattr(base_class, "ormar_config") + else None ) if parent_value: if param == "constraints": @@ -255,7 +237,7 @@ def update_attrs_from_base_meta( # noqa: CCR001 if isinstance(current_value, list): current_value.extend(parent_value) else: - setattr(attrs["Meta"], param, parent_value) + setattr(attrs["ormar_config"], param, parent_value) def copy_and_replace_m2m_through_model( # noqa: CFQ002 @@ -264,7 +246,7 @@ def copy_and_replace_m2m_through_model( # noqa: CFQ002 table_name: str, parent_fields: Dict, attrs: Dict, - meta: ModelMeta, + ormar_config: OrmarConfig, base_class: Type["Model"], ) -> None: """ @@ -291,8 +273,8 @@ def copy_and_replace_m2m_through_model( # noqa: CFQ002 :type parent_fields: Dict :param attrs: new namespace for class being constructed :type attrs: Dict - :param meta: metaclass of currently created model - :type meta: ModelMeta + :param ormar_config: metaclass of currently created model + :type ormar_config: OrmarConfig """ Field: Type[BaseField] = type( # type: ignore field.__class__.__name__, (ManyToManyField, BaseField), {} @@ -306,32 +288,51 @@ def copy_and_replace_m2m_through_model( # noqa: CFQ002 field.owner = base_class field.create_default_through_model() through_class = field.through - new_meta: ormar.ModelMeta = type( # type: ignore - "Meta", (), dict(through_class.Meta.__dict__) + new_config = ormar.OrmarConfig( + tablename=through_class.ormar_config.tablename, + metadata=through_class.ormar_config.metadata, + database=through_class.ormar_config.database, + abstract=through_class.ormar_config.abstract, + exclude_parent_fields=through_class.ormar_config.exclude_parent_fields, + queryset_class=through_class.ormar_config.queryset_class, + extra=through_class.ormar_config.extra, + constraints=through_class.ormar_config.constraints, + order_by=through_class.ormar_config.orders_by, + ) + new_config.table = through_class.ormar_config.pkname + new_config.pkname = through_class.ormar_config.pkname + new_config.alias_manager = through_class.ormar_config.alias_manager + new_config.signals = through_class.ormar_config.signals + new_config.requires_ref_update = through_class.ormar_config.requires_ref_update + new_config.model_fields = copy.deepcopy(through_class.ormar_config.model_fields) + new_config.property_fields = copy.deepcopy( + through_class.ormar_config.property_fields ) copy_name = through_class.__name__ + attrs.get("__name__", "") - copy_through = type(copy_name, (ormar.Model,), {"Meta": new_meta}) - new_meta.tablename += "_" + meta.tablename + copy_through = cast( + Type[ormar.Model], type(copy_name, (ormar.Model,), {"ormar_config": new_config}) + ) # create new table with copied columns but remove foreign keys # they will be populated later in expanding reverse relation - if hasattr(new_meta, "table"): - del new_meta.table - new_meta.model_fields = { + # if hasattr(new_config, "table"): + new_config.tablename += "_" + ormar_config.tablename + new_config.table = None + new_config.model_fields = { name: field - for name, field in new_meta.model_fields.items() + for name, field in new_config.model_fields.items() if not field.is_relation } _, columns = sqlalchemy_columns_from_model_fields( - new_meta.model_fields, copy_through + new_config.model_fields, copy_through ) # type: ignore - new_meta.columns = columns - populate_meta_sqlalchemy_table_if_required(new_meta) + new_config.columns = columns + populate_config_sqlalchemy_table_if_required(config=new_config) copy_field.through = copy_through parent_fields[field_name] = copy_field - if through_class.Meta.table in through_class.Meta.metadata: - through_class.Meta.metadata.remove(through_class.Meta.table) + if through_class.ormar_config.table in through_class.ormar_config.metadata: + through_class.ormar_config.metadata.remove(through_class.ormar_config.table) def copy_data_from_parent_model( # noqa: CCR001 @@ -361,32 +362,32 @@ def copy_data_from_parent_model( # noqa: CCR001 :return: updated attrs and model_fields :rtype: Tuple[Dict, Dict] """ - if attrs.get("Meta"): - if model_fields and not base_class.Meta.abstract: # type: ignore + if attrs.get("ormar_config"): + if model_fields and not base_class.ormar_config.abstract: # type: ignore raise ModelDefinitionError( f"{curr_class.__name__} cannot inherit " f"from non abstract class {base_class.__name__}" ) - update_attrs_from_base_meta( + update_attrs_from_base_config( base_class=base_class, # type: ignore attrs=attrs, model_fields=model_fields, ) parent_fields: Dict = dict() - meta = attrs.get("Meta") - if not meta: # pragma: no cover + ormar_config = attrs.get("ormar_config") + if not ormar_config: # pragma: no cover raise ModelDefinitionError( - f"Model {curr_class.__name__} declared without Meta" + f"Model {curr_class.__name__} declared without ormar_config" ) table_name = ( - meta.tablename - if hasattr(meta, "tablename") and meta.tablename + ormar_config.tablename + if hasattr(ormar_config, "tablename") and ormar_config.tablename else attrs.get("__name__", "").lower() + "s" ) - for field_name, field in base_class.Meta.model_fields.items(): + for field_name, field in base_class.ormar_config.model_fields.items(): if ( - hasattr(meta, "exclude_parent_fields") - and field_name in meta.exclude_parent_fields + hasattr(ormar_config, "exclude_parent_fields") + and field_name in ormar_config.exclude_parent_fields ): continue if field.is_multi: @@ -397,7 +398,7 @@ def copy_data_from_parent_model( # noqa: CCR001 table_name=table_name, parent_fields=parent_fields, attrs=attrs, - meta=meta, + ormar_config=ormar_config, base_class=base_class, # type: ignore ) @@ -446,7 +447,7 @@ def extract_from_parents_definition( # noqa: CCR001 :return: updated attrs and model_fields :rtype: Tuple[Dict, Dict] """ - if hasattr(base_class, "Meta"): + if hasattr(base_class, "ormar_config"): base_class = cast(Type["Model"], base_class) return copy_data_from_parent_model( base_class=base_class, @@ -503,7 +504,7 @@ def update_attrs_and_fields( ) -> Dict: """ Updates __annotations__, values of model fields (so pydantic FieldInfos) - as well as model.Meta.model_fields definitions from parents. + as well as model.ormar_config.model_fields definitions from parents. :param attrs: new namespace for class being constructed :type attrs: Dict @@ -549,10 +550,42 @@ def add_field_descriptor( setattr(new_model, name, PydanticDescriptor(name=name)) -class ModelMetaclass(pydantic.main.ModelMetaclass): +def get_serializer() -> Callable: + def serialize( + self: "Model", + value: Optional["Model"], + handler: SerializerFunctionWrapHandler, + ) -> Any: + """ + Serialize a value if it's not expired weak reference. + """ + try: + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", message="Pydantic serializer warnings" + ) + return handler(value) + except ReferenceError: + return None + except ValueError as exc: + if not str(exc).startswith("Circular reference"): + raise exc + return {value.ormar_config.pkname: value.pk} if value else None + + return serialize + + +class ModelMetaclass(pydantic._internal._model_construction.ModelMetaclass): def __new__( # type: ignore # noqa: CCR001 - mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict - ) -> "ModelMetaclass": + mcs: "ModelMetaclass", + name: str, + bases: Any, + attrs: dict, + __pydantic_generic_metadata__: Union[PydanticGenericMetadata, None] = None, + __pydantic_reset_parent_namespace__: bool = True, + _create_model_module: Union[str, None] = None, + **kwargs, + ) -> type: """ Metaclass used by ormar Models that performs configuration and build of ormar Models. @@ -563,18 +596,19 @@ class ModelMetaclass(pydantic.main.ModelMetaclass): updates class namespace. Extracts settings and fields from parent classes. - Fetches methods decorated with @property_field decorator + Fetches methods decorated with @computed_field decorator to expose them later in dict(). Construct parent pydantic Metaclass/ Model. - If class has Meta class declared (so actual ormar Models) it also: + If class has ormar_config declared (so actual ormar Models) it also: * populate sqlalchemy columns, pkname and tables from model_fields * register reverse relationships on related models * registers all relations in alias manager that populates table_prefixes * exposes alias manager on each Model * creates QuerySet for each model and exposes it on a class + * sets custom serializers for relation models :param name: name of current class :type name: str @@ -593,63 +627,74 @@ class ModelMetaclass(pydantic.main.ModelMetaclass): attrs, model_fields = extract_from_parents_definition( base_class=base, curr_class=mcs, attrs=attrs, model_fields=model_fields ) - new_model = super().__new__(mcs, name, bases, attrs) # type: ignore + if "ormar_config" in attrs: + attrs["model_config"]["ignored_types"] = (OrmarConfig,) + attrs["model_config"]["from_attributes"] = True + for field_name, field in model_fields.items(): + if field.is_relation: + decorator = field_serializer( + field_name, mode="wrap", check_fields=False + )(get_serializer()) + attrs[f"serialize_{field_name}"] = decorator + + new_model = super().__new__( + mcs, # type: ignore + name, + bases, + attrs, + __pydantic_generic_metadata__=__pydantic_generic_metadata__, + __pydantic_reset_parent_namespace__=__pydantic_reset_parent_namespace__, + _create_model_module=_create_model_module, + **kwargs, + ) add_cached_properties(new_model) - if hasattr(new_model, "Meta"): + if hasattr(new_model, "ormar_config"): populate_default_options_values(new_model, model_fields) - check_required_meta_parameters(new_model) + check_required_config_parameters(new_model) add_property_fields(new_model, attrs) register_signals(new_model=new_model) - populate_choices_validators(new_model) + modify_schema_example(model=new_model) - if not new_model.Meta.abstract: - new_model = populate_meta_tablename_columns_and_pk(name, new_model) - populate_meta_sqlalchemy_table_if_required(new_model.Meta) + if not new_model.ormar_config.abstract: + new_model = populate_config_tablename_columns_and_pk(name, new_model) + populate_config_sqlalchemy_table_if_required(new_model.ormar_config) expand_reverse_relationships(new_model) - # TODO: iterate only related fields - for field_name, field in new_model.Meta.model_fields.items(): + for field_name, field in new_model.ormar_config.model_fields.items(): register_relation_in_alias_manager(field=field) add_field_descriptor( name=field_name, field=field, new_model=new_model ) if ( - new_model.Meta.pkname - and new_model.Meta.pkname not in attrs["__annotations__"] - and new_model.Meta.pkname not in new_model.__fields__ + new_model.ormar_config.pkname + and new_model.ormar_config.pkname not in attrs["__annotations__"] + and new_model.ormar_config.pkname not in new_model.model_fields ): - field_name = new_model.Meta.pkname - attrs["__annotations__"][field_name] = Optional[int] # type: ignore - attrs[field_name] = None - new_model.__fields__[field_name] = get_pydantic_field( - field_name=field_name, model=new_model + field_name = new_model.ormar_config.pkname + new_model.model_fields[field_name] = ( + FieldInfo.from_annotated_attribute( + Optional[int], # type: ignore + None, + ) ) - new_model.Meta.alias_manager = alias_manager + new_model.model_rebuild(force=True) - for item in new_model.Meta.property_fields: - function = getattr(new_model, item) - setattr( - new_model, - item, - PropertyDescriptor(name=item, function=function), - ) - - new_model.pk = PkDescriptor(name=new_model.Meta.pkname) + new_model.pk = PkDescriptor(name=new_model.ormar_config.pkname) remove_excluded_parent_fields(new_model) return new_model @property def objects(cls: Type["T"]) -> "QuerySet[T]": # type: ignore - if cls.Meta.requires_ref_update: + if cls.ormar_config.requires_ref_update: raise ModelError( f"Model {cls.get_name()} has not updated " f"ForwardRefs. \nBefore using the model you " f"need to call update_forward_refs()." ) - return cls.Meta.queryset_class(model_cls=cls) + return cls.ormar_config.queryset_class(model_cls=cls) def __getattr__(self, item: str) -> Any: """ @@ -661,10 +706,19 @@ class ModelMetaclass(pydantic.main.ModelMetaclass): :return: FieldAccessor for given field :rtype: FieldAccessor """ + # Ugly workaround for name shadowing warnings in pydantic + frame = sys._getframe(1) + file_name = Path(frame.f_code.co_filename) + if ( + frame.f_code.co_name == "collect_model_fields" + and file_name.name == "_fields.py" + and file_name.parent.parent.name == "pydantic" + ): + raise AttributeError() if item == "pk": - item = self.Meta.pkname - if item in object.__getattribute__(self, "Meta").model_fields: - field = self.Meta.model_fields.get(item) + item = self.ormar_config.pkname + if item in object.__getattribute__(self, "ormar_config").model_fields: + field = self.ormar_config.model_fields.get(item) if field.is_relation: return FieldAccessor( source_model=cast(Type["Model"], self), diff --git a/ormar/models/mixins/__init__.py b/ormar/models/mixins/__init__.py index e605ee8..edb5852 100644 --- a/ormar/models/mixins/__init__.py +++ b/ormar/models/mixins/__init__.py @@ -4,6 +4,7 @@ All mixins are combined into ModelTableProxy which is one of the parents of Mode The split into mixins was done to ease the maintainability of the proxy class, as it became quite complicated over time. """ + from ormar.models.mixins.alias_mixin import AliasMixin from ormar.models.mixins.excludable_mixin import ExcludableMixin from ormar.models.mixins.merge_mixin import MergeModelMixin diff --git a/ormar/models/mixins/alias_mixin.py b/ormar/models/mixins/alias_mixin.py index 2d98657..2f808cd 100644 --- a/ormar/models/mixins/alias_mixin.py +++ b/ormar/models/mixins/alias_mixin.py @@ -1,4 +1,4 @@ -from typing import Dict, TYPE_CHECKING +from typing import TYPE_CHECKING, Dict class AliasMixin: @@ -7,9 +7,9 @@ class AliasMixin: """ if TYPE_CHECKING: # pragma: no cover - from ormar import ModelMeta + from ormar.models.ormar_config import OrmarConfig - Meta: ModelMeta + ormar_config: OrmarConfig @classmethod def get_column_alias(cls, field_name: str) -> str: @@ -21,7 +21,7 @@ class AliasMixin: :return: alias (db name) if set, otherwise passed name :rtype: str """ - field = cls.Meta.model_fields.get(field_name) + field = cls.ormar_config.model_fields.get(field_name) return field.get_alias() if field is not None else field_name @classmethod @@ -34,7 +34,7 @@ class AliasMixin: :return: field name if set, otherwise passed alias (db name) :rtype: str """ - for field_name, field in cls.Meta.model_fields.items(): + for field_name, field in cls.ormar_config.model_fields.items(): if field.get_alias() == alias: return field_name return alias # if not found it's not an alias but actual name @@ -50,7 +50,7 @@ class AliasMixin: :return: dict with aliases and their values :rtype: Dict """ - for field_name, field in cls.Meta.model_fields.items(): + for field_name, field in cls.ormar_config.model_fields.items(): if field_name in new_kwargs: new_kwargs[field.get_alias()] = new_kwargs.pop(field_name) return new_kwargs @@ -66,7 +66,7 @@ class AliasMixin: :return: dict with fields names and their values :rtype: Dict """ - for field_name, field in cls.Meta.model_fields.items(): + for field_name, field in cls.ormar_config.model_fields.items(): if field.get_alias() and field.get_alias() in new_kwargs: new_kwargs[field_name] = new_kwargs.pop(field.get_alias()) return new_kwargs diff --git a/ormar/models/mixins/excludable_mixin.py b/ormar/models/mixins/excludable_mixin.py index d7019b7..b6151ad 100644 --- a/ormar/models/mixins/excludable_mixin.py +++ b/ormar/models/mixins/excludable_mixin.py @@ -1,13 +1,13 @@ from typing import ( + TYPE_CHECKING, AbstractSet, Any, Dict, List, Mapping, + Optional, Set, - TYPE_CHECKING, Type, - TypeVar, Union, cast, ) @@ -18,7 +18,6 @@ from ormar.models.mixins.relation_mixin import RelationMixin if TYPE_CHECKING: # pragma no cover from ormar import Model - T = TypeVar("T", bound=Model) IntStr = Union[int, str] AbstractSetIntStr = AbstractSet[IntStr] MappingIntStrAny = Mapping[IntStr, Any] @@ -35,7 +34,7 @@ class ExcludableMixin(RelationMixin): @staticmethod def get_child( - items: Union[Set, Dict, None], key: str = None + items: Union[Set, Dict, None], key: Optional[str] = None ) -> Union[Set, Dict, None]: """ Used to get nested dictionaries keys if they exists otherwise returns @@ -71,9 +70,9 @@ class ExcludableMixin(RelationMixin): :rtype: List[str] """ pk_alias = ( - model.get_column_alias(model.Meta.pkname) + model.get_column_alias(model.ormar_config.pkname) if use_alias - else model.Meta.pkname + else model.ormar_config.pkname ) if pk_alias not in columns: columns.append(pk_alias) @@ -113,11 +112,11 @@ class ExcludableMixin(RelationMixin): model_excludable = excludable.get(model_cls=model, alias=alias) # type: ignore columns = [ model.get_column_name_from_alias(col.name) if not use_alias else col.name - for col in model.Meta.table.columns + for col in model.ormar_config.table.columns ] field_names = [ model.get_column_name_from_alias(col.name) - for col in model.Meta.table.columns + for col in model.ormar_config.table.columns ] if model_excludable.include: columns = [ @@ -181,7 +180,7 @@ class ExcludableMixin(RelationMixin): :rtype: Set """ if exclude_primary_keys: - exclude.add(cls.Meta.pkname) + exclude.add(cls.ormar_config.pkname) if exclude_through_models: exclude = exclude.union(cls.extract_through_names()) return exclude @@ -219,6 +218,6 @@ class ExcludableMixin(RelationMixin): fields_to_exclude = fields_to_exclude.union( model_excludable.exclude.intersection(fields_names) ) - fields_to_exclude = fields_to_exclude - {cls.Meta.pkname} + fields_to_exclude = fields_to_exclude - {cls.ormar_config.pkname} return fields_to_exclude diff --git a/ormar/models/mixins/merge_mixin.py b/ormar/models/mixins/merge_mixin.py index 7ef2077..3555c34 100644 --- a/ormar/models/mixins/merge_mixin.py +++ b/ormar/models/mixins/merge_mixin.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Dict, List, Optional, cast import ormar from ormar.queryset.utils import translate_list_to_dict @@ -69,7 +69,7 @@ class MergeModelMixin: @classmethod def merge_two_instances( - cls, one: "Model", other: "Model", relation_map: Dict = None + cls, one: "Model", other: "Model", relation_map: Optional[Dict] = None ) -> "Model": """ Merges current (other) Model and previous one (one) and returns the current diff --git a/ormar/models/mixins/prefetch_mixin.py b/ormar/models/mixins/prefetch_mixin.py index b159850..61eb29d 100644 --- a/ormar/models/mixins/prefetch_mixin.py +++ b/ormar/models/mixins/prefetch_mixin.py @@ -1,4 +1,4 @@ -from typing import Callable, Dict, List, TYPE_CHECKING, Tuple, Type, cast +from typing import TYPE_CHECKING, Callable, Dict, List, Tuple, Type, cast from ormar.models.mixins.relation_mixin import RelationMixin @@ -38,15 +38,17 @@ class PrefetchQueryMixin(RelationMixin): :rtype: Tuple[Type[Model], str] """ if reverse: - field_name = parent_model.Meta.model_fields[related].get_related_name() - field = target_model.Meta.model_fields[field_name] + field_name = parent_model.ormar_config.model_fields[ + related + ].get_related_name() + field = target_model.ormar_config.model_fields[field_name] if field.is_multi: field = cast("ManyToManyField", field) field_name = field.default_target_field_name() - sub_field = field.through.Meta.model_fields[field_name] + sub_field = field.through.ormar_config.model_fields[field_name] return field.through, sub_field.get_alias() return target_model, field.get_alias() - target_field = target_model.get_column_alias(target_model.Meta.pkname) + target_field = target_model.get_column_alias(target_model.ormar_config.pkname) return target_model, target_field @staticmethod @@ -70,11 +72,11 @@ class PrefetchQueryMixin(RelationMixin): :rtype: """ if reverse: - column_name = parent_model.Meta.pkname + column_name = parent_model.ormar_config.pkname return ( parent_model.get_column_alias(column_name) if use_raw else column_name ) - column = parent_model.Meta.model_fields[related] + column = parent_model.ormar_config.model_fields[related] return column.get_alias() if use_raw else column.name @classmethod @@ -93,7 +95,7 @@ class PrefetchQueryMixin(RelationMixin): return cls.get_name() if target_field.virtual: return target_field.get_related_name() - return target_field.to.Meta.pkname + return target_field.to.ormar_config.pkname @classmethod def get_filtered_names_to_extract(cls, prefetch_dict: Dict) -> List: diff --git a/ormar/models/mixins/pydantic_mixin.py b/ormar/models/mixins/pydantic_mixin.py index 5d186bb..bd2e1a6 100644 --- a/ormar/models/mixins/pydantic_mixin.py +++ b/ormar/models/mixins/pydantic_mixin.py @@ -2,37 +2,44 @@ import copy import string from random import choices from typing import ( + TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, - TYPE_CHECKING, + Tuple, Type, Union, cast, ) import pydantic -from pydantic.fields import ModelField +from pydantic import BaseModel +from pydantic._internal._decorators import DecoratorInfos +from pydantic.fields import FieldInfo +from ormar.fields import BaseField, ForeignKeyField, ManyToManyField from ormar.models.mixins.relation_mixin import RelationMixin # noqa: I100, I202 from ormar.queryset.utils import translate_list_to_dict class PydanticMixin(RelationMixin): - __cache__: Dict[str, Type[pydantic.BaseModel]] = {} if TYPE_CHECKING: # pragma: no cover - __fields__: Dict[str, ModelField] + __pydantic_decorators__: DecoratorInfos + model_fields: Dict[str, FieldInfo] _skip_ellipsis: Callable _get_not_excluded_fields: Callable @classmethod def get_pydantic( - cls, *, include: Union[Set, Dict] = None, exclude: Union[Set, Dict] = None + cls, + *, + include: Union[Set, Dict, None] = None, + exclude: Union[Set, Dict, None] = None, ) -> Type[pydantic.BaseModel]: """ Returns a pydantic model out of ormar model. @@ -56,8 +63,8 @@ class PydanticMixin(RelationMixin): def _convert_ormar_to_pydantic( cls, relation_map: Dict[str, Any], - include: Union[Set, Dict] = None, - exclude: Union[Set, Dict] = None, + include: Union[Set, Dict, None] = None, + exclude: Union[Set, Dict, None] = None, ) -> Type[pydantic.BaseModel]: if include and isinstance(include, Set): include = translate_list_to_dict(include) @@ -66,10 +73,12 @@ class PydanticMixin(RelationMixin): fields_dict: Dict[str, Any] = dict() defaults: Dict[str, Any] = dict() fields_to_process = cls._get_not_excluded_fields( - fields={*cls.Meta.model_fields.keys()}, include=include, exclude=exclude + fields={*cls.ormar_config.model_fields.keys()}, + include=include, + exclude=exclude, ) fields_to_process.sort( - key=lambda x: list(cls.Meta.model_fields.keys()).index(x) + key=lambda x: list(cls.ormar_config.model_fields.keys()).index(x) ) cache_key = f"{cls.__name__}_{str(include)}_{str(exclude)}" @@ -105,51 +114,84 @@ class PydanticMixin(RelationMixin): exclude: Union[Set, Dict, None], relation_map: Dict[str, Any], ) -> Any: - field = cls.Meta.model_fields[name] + field = cls.ormar_config.model_fields[name] target: Any = None - if field.is_relation and name in relation_map: # type: ignore - target = field.to._convert_ormar_to_pydantic( - include=cls._skip_ellipsis(include, name), - exclude=cls._skip_ellipsis(exclude, name), - relation_map=cls._skip_ellipsis( - relation_map, name, default_return=dict() - ), + if field.is_relation and name in relation_map: + target, default = cls._determined_included_relation_field_type( + name=name, + field=field, + include=include, + exclude=exclude, + defaults=defaults, + relation_map=relation_map, ) - if field.is_multi or field.virtual: - target = List[target] # type: ignore elif not field.is_relation: - defaults[name] = cls.__fields__[name].field_info + defaults[name] = cls.model_fields[name].default target = field.__type__ if target is not None and field.nullable: target = Optional[target] return target + @classmethod + def _determined_included_relation_field_type( + cls, + name: str, + field: Union[BaseField, ForeignKeyField, ManyToManyField], + include: Union[Set, Dict, None], + exclude: Union[Set, Dict, None], + defaults: Dict, + relation_map: Dict[str, Any], + ) -> Tuple[Type[BaseModel], Dict]: + target = field.to._convert_ormar_to_pydantic( + include=cls._skip_ellipsis(include, name), + exclude=cls._skip_ellipsis(exclude, name), + relation_map=cls._skip_ellipsis(relation_map, name, default_return=dict()), + ) + if field.is_multi or field.virtual: + target = List[target] # type: ignore + if field.nullable: + defaults[name] = None + return target, defaults + @classmethod def _copy_field_validators(cls, model: Type[pydantic.BaseModel]) -> None: """ Copy field validators from ormar model to generated pydantic model. """ - for field_name, field in model.__fields__.items(): - if ( - field_name not in cls.__fields__ - or cls.Meta.model_fields[field_name].is_relation - ): - continue - validators = cls.__fields__[field_name].validators - already_attached = [ - validator.__wrapped__ for validator in field.validators # type: ignore - ] - validators_to_copy = [ - validator - for validator in validators - if validator.__wrapped__ not in already_attached # type: ignore - ] - field.validators.extend(copy.deepcopy(validators_to_copy)) - class_validators = cls.__fields__[field_name].class_validators - field.class_validators.update(copy.deepcopy(class_validators)) - field.pre_validators = copy.deepcopy( - cls.__fields__[field_name].pre_validators - ) - field.post_validators = copy.deepcopy( - cls.__fields__[field_name].post_validators - ) + filed_names = list(model.model_fields.keys()) + cls.copy_selected_validators_type( + model=model, fields=filed_names, validator_type="field_validators" + ) + cls.copy_selected_validators_type( + model=model, fields=filed_names, validator_type="validators" + ) + + class_validators = cls.__pydantic_decorators__.root_validators + model.__pydantic_decorators__.root_validators.update( + copy.deepcopy(class_validators) + ) + model_validators = cls.__pydantic_decorators__.model_validators + model.__pydantic_decorators__.model_validators.update( + copy.deepcopy(model_validators) + ) + model.model_rebuild(force=True) + + @classmethod + def copy_selected_validators_type( + cls, model: Type[pydantic.BaseModel], fields: List[str], validator_type: str + ) -> None: + """ + Copy field validators from ormar model to generated pydantic model. + """ + validators = getattr(cls.__pydantic_decorators__, validator_type) + for name, decorator in validators.items(): + if any(field_name in decorator.info.fields for field_name in fields): + copied_decorator = copy.deepcopy(decorator) + copied_decorator.info.fields = [ + field_name + for field_name in decorator.info.fields + if field_name in fields + ] + getattr(model.__pydantic_decorators__, validator_type)[ + name + ] = copied_decorator diff --git a/ormar/models/mixins/relation_mixin.py b/ormar/models/mixins/relation_mixin.py index 3f9521c..cf2a0af 100644 --- a/ormar/models/mixins/relation_mixin.py +++ b/ormar/models/mixins/relation_mixin.py @@ -1,4 +1,4 @@ -from typing import Callable, Dict, List, Optional, Set, TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Set, cast from ormar import BaseField, ForeignKeyField from ormar.models.traversible import NodeList @@ -10,9 +10,9 @@ class RelationMixin: """ if TYPE_CHECKING: # pragma no cover - from ormar import ModelMeta + from ormar.models.ormar_config import OrmarConfig - Meta: ModelMeta + ormar_config: OrmarConfig __relation_map__: Optional[List[str]] _related_names: Optional[Set] _through_names: Optional[Set] @@ -29,7 +29,9 @@ class RelationMixin: """ related_names = cls.extract_related_names() self_fields = { - name for name in cls.Meta.model_fields.keys() if name not in related_names + name + for name in cls.ormar_config.model_fields.keys() + if name not in related_names } return self_fields @@ -47,7 +49,9 @@ class RelationMixin: related_fields = [] for name in cls.extract_related_names().union(cls.extract_through_names()): - related_fields.append(cast("ForeignKeyField", cls.Meta.model_fields[name])) + related_fields.append( + cast("ForeignKeyField", cls.ormar_config.model_fields[name]) + ) cls._related_fields = related_fields return related_fields @@ -64,7 +68,7 @@ class RelationMixin: return cls._through_names related_names = set() - for name, field in cls.Meta.model_fields.items(): + for name, field in cls.ormar_config.model_fields.items(): if isinstance(field, BaseField) and field.is_through: related_names.add(name) @@ -84,7 +88,7 @@ class RelationMixin: return cls._related_names related_names = set() - for name, field in cls.Meta.model_fields.items(): + for name, field in cls.ormar_config.model_fields.items(): if ( isinstance(field, BaseField) and field.is_relation @@ -108,16 +112,16 @@ class RelationMixin: related_names = { name for name in related_names - if cls.Meta.model_fields[name].is_valid_uni_relation() + if cls.ormar_config.model_fields[name].is_valid_uni_relation() } return related_names @classmethod def _iterate_related_models( # noqa: CCR001 cls, - node_list: NodeList = None, - parsed_map: Dict = None, - source_relation: str = None, + node_list: Optional[NodeList] = None, + parsed_map: Optional[Dict] = None, + source_relation: Optional[str] = None, recurrent: bool = False, ) -> List[str]: """ @@ -139,7 +143,7 @@ class RelationMixin: processed_relations: List[str] = [] for relation in relations: if not current_node.visited(relation): - target_model = cls.Meta.model_fields[relation].to + target_model = cls.ormar_config.model_fields[relation].to node_list.add( node_class=target_model, relation_name=relation, diff --git a/ormar/models/mixins/save_mixin.py b/ormar/models/mixins/save_mixin.py index 46ef451..e759499 100644 --- a/ormar/models/mixins/save_mixin.py +++ b/ormar/models/mixins/save_mixin.py @@ -2,6 +2,7 @@ import base64 import uuid from enum import Enum from typing import ( + TYPE_CHECKING, Any, Callable, Collection, @@ -9,11 +10,11 @@ from typing import ( List, Optional, Set, - TYPE_CHECKING, cast, ) -import pydantic +from pydantic.plugin._schema_validator import create_schema_validator +from pydantic_core import CoreSchema, SchemaValidator import ormar # noqa: I100, I202 from ormar.exceptions import ModelPersistenceError @@ -31,11 +32,11 @@ class SavePrepareMixin(RelationMixin, AliasMixin): """ if TYPE_CHECKING: # pragma: nocover - _choices_fields: Optional[Set] _skip_ellipsis: Callable _json_fields: Set[str] _bytes_fields: Set[str] - __fields__: Dict[str, pydantic.fields.ModelField] + __pydantic_core_schema__: CoreSchema + __ormar_fields_validators__: Optional[Dict[str, SchemaValidator]] @classmethod def prepare_model_to_save(cls, new_kwargs: dict) -> dict: @@ -95,9 +96,7 @@ class SavePrepareMixin(RelationMixin, AliasMixin): :return: dictionary of model that is about to be saved :rtype: Dict[str, str] """ - ormar_fields = { - k for k, v in cls.Meta.model_fields.items() if not v.pydantic_only - } + ormar_fields = {k for k, v in cls.ormar_config.model_fields.items()} new_kwargs = {k: v for k, v in new_kwargs.items() if k in ormar_fields} return new_kwargs @@ -112,8 +111,8 @@ class SavePrepareMixin(RelationMixin, AliasMixin): :return: dictionary of model that is about to be saved :rtype: Dict[str, str] """ - pkname = cls.Meta.pkname - pk = cls.Meta.model_fields[pkname] + pkname = cls.ormar_config.pkname + pk = cls.ormar_config.model_fields[pkname] if new_kwargs.get(pkname, ormar.Undefined) is None and ( pk.nullable or pk.autoincrement ): @@ -131,7 +130,7 @@ class SavePrepareMixin(RelationMixin, AliasMixin): :return: dictionary of model that is about to be saved :rtype: Dict """ - for name, field in cls.Meta.model_fields.items(): + for name, field in cls.ormar_config.model_fields.items(): if field.__type__ == uuid.UUID and name in model_dict: parsers = {"string": lambda x: str(x), "hex": lambda x: "%.32x" % x.int} uuid_format = field.column_type.uuid_format @@ -153,8 +152,8 @@ class SavePrepareMixin(RelationMixin, AliasMixin): for field in cls.extract_related_names(): field_value = model_dict.get(field, None) if field_value is not None: - target_field = cls.Meta.model_fields[field] - target_pkname = target_field.to.Meta.pkname + target_field = cls.ormar_config.model_fields[field] + target_pkname = target_field.to.ormar_config.pkname if isinstance(field_value, ormar.Model): # pragma: no cover pk_value = getattr(field_value, target_pkname) if not pk_value: @@ -187,7 +186,7 @@ class SavePrepareMixin(RelationMixin, AliasMixin): """ bytes_base64_fields = { name - for name, field in cls.Meta.model_fields.items() + for name, field in cls.ormar_config.model_fields.items() if field.represent_as_base64_str } for key, value in model_dict.items(): @@ -227,12 +226,8 @@ class SavePrepareMixin(RelationMixin, AliasMixin): :return: dictionary of model that is about to be saved :rtype: Dict """ - for field_name, field in cls.Meta.model_fields.items(): - if ( - field_name not in new_kwargs - and field.has_default(use_server=False) - and not field.pydantic_only - ): + for field_name, field in cls.ormar_config.model_fields.items(): + if field_name not in new_kwargs and field.has_default(use_server=False): new_kwargs[field_name] = field.get_default() # clear fields with server_default set as None if ( @@ -243,7 +238,7 @@ class SavePrepareMixin(RelationMixin, AliasMixin): return new_kwargs @classmethod - def validate_choices(cls, new_kwargs: Dict) -> Dict: + def validate_enums(cls, new_kwargs: Dict) -> Dict: """ Receives dictionary of model that is about to be saved and validates the fields with choices set to see if the value is allowed. @@ -253,23 +248,47 @@ class SavePrepareMixin(RelationMixin, AliasMixin): :return: dictionary of model that is about to be saved :rtype: Dict """ - if not cls._choices_fields: - return new_kwargs - - fields_to_check = [ - field - for field in cls.Meta.model_fields.values() - if field.name in cls._choices_fields and field.name in new_kwargs - ] - for field in fields_to_check: - if new_kwargs[field.name] not in field.choices: - raise ValueError( - f"{field.name}: '{new_kwargs[field.name]}' " - f"not in allowed choices set:" - f" {field.choices}" - ) + validators = cls._build_individual_schema_validator() + for key, value in new_kwargs.items(): + if key in validators: + validators[key].validate_python(value) return new_kwargs + @classmethod + def _build_individual_schema_validator(cls) -> Any: + if cls.__ormar_fields_validators__ is not None: + return cls.__ormar_fields_validators__ + field_validators = {} + for key, field in cls._extract_pydantic_fields().items(): + if cls.__pydantic_core_schema__["type"] == "definitions": + schema = { + "type": "definitions", + "schema": field["schema"], + "definitions": cls.__pydantic_core_schema__["definitions"], + } + else: + schema = field["schema"] + field_validators[key] = create_schema_validator( + schema, cls, cls.__module__, cls.__qualname__, "BaseModel" + ) + cls.__ormar_fields_validators__ = field_validators + return cls.__ormar_fields_validators__ + + @classmethod + def _extract_pydantic_fields(cls) -> Any: + if cls.__pydantic_core_schema__["type"] == "model": + return cls.__pydantic_core_schema__["schema"]["fields"] + elif cls.__pydantic_core_schema__["type"] == "definitions": + main_schema = cls.__pydantic_core_schema__["schema"] + if "schema_ref" in main_schema: + reference_id = main_schema["schema_ref"] + return next( + ref + for ref in cls.__pydantic_core_schema__["definitions"] + if ref["ref"] == reference_id + )["schema"]["fields"] + return main_schema["schema"]["fields"] + @staticmethod async def _upsert_model( instance: "Model", @@ -329,12 +348,12 @@ class SavePrepareMixin(RelationMixin, AliasMixin): :param previous_model: previous model from which method came :type previous_model: Model """ - through_name = previous_model.Meta.model_fields[ + through_name = previous_model.ormar_config.model_fields[ relation_field.name ].through.get_name() through = getattr(instance, through_name) if through: - through_dict = through.dict(exclude=through.extract_related_names()) + through_dict = through.model_dump(exclude=through.extract_related_names()) else: through_dict = {} await getattr( diff --git a/ormar/models/model.py b/ormar/models/model.py index 7ff8e63..f527485 100644 --- a/ormar/models/model.py +++ b/ormar/models/model.py @@ -1,9 +1,8 @@ -from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING, TypeVar, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, TypeVar, Union import ormar.queryset # noqa I100 from ormar.exceptions import ModelPersistenceError, NoMatch from ormar.models import NewBaseModel # noqa I100 -from ormar.models.metaclass import ModelMeta from ormar.models.model_row import ModelRow from ormar.queryset.utils import subtract_dict, translate_list_to_dict @@ -11,17 +10,18 @@ T = TypeVar("T", bound="Model") if TYPE_CHECKING: # pragma: no cover from ormar import ForeignKeyField + from ormar.models.ormar_config import OrmarConfig class Model(ModelRow): __abstract__ = False if TYPE_CHECKING: # pragma nocover - Meta: ModelMeta + ormar_config: OrmarConfig def __repr__(self) -> str: # pragma nocover _repr = { k: getattr(self, k) - for k, v in self.Meta.model_fields.items() + for k, v in self.ormar_config.model_fields.items() if not v.skip_field } return f"{self.__class__.__name__}({str(_repr)})" @@ -40,8 +40,8 @@ class Model(ModelRow): force_save = kwargs.pop("__force_save__", False) if force_save: - expr = self.Meta.table.select().where(self.pk_column == self.pk) - row = await self.Meta.database.fetch_one(expr) + expr = self.ormar_config.table.select().where(self.pk_column == self.pk) + row = await self.ormar_config.database.fetch_one(expr) if not row: return await self.save() return await self.update(**kwargs) @@ -76,8 +76,11 @@ class Model(ModelRow): await self.signals.pre_save.send(sender=self.__class__, instance=self) self_fields = self._extract_model_db_fields() - if not self.pk and self.Meta.model_fields[self.Meta.pkname].autoincrement: - self_fields.pop(self.Meta.pkname, None) + if ( + not self.pk + and self.ormar_config.model_fields[self.ormar_config.pkname].autoincrement + ): + self_fields.pop(self.ormar_config.pkname, None) self_fields = self.populate_default_values(self_fields) self.update_from_dict( { @@ -88,18 +91,18 @@ class Model(ModelRow): ) self_fields = self.translate_columns_to_aliases(self_fields) - expr = self.Meta.table.insert() + expr = self.ormar_config.table.insert() expr = expr.values(**self_fields) - pk = await self.Meta.database.execute(expr) + pk = await self.ormar_config.database.execute(expr) if pk and isinstance(pk, self.pk_type()): - setattr(self, self.Meta.pkname, pk) + setattr(self, self.ormar_config.pkname, pk) self.set_save_status(True) # refresh server side defaults if any( field.server_default is not None - for name, field in self.Meta.model_fields.items() + for name, field in self.ormar_config.model_fields.items() if name not in self_fields ): await self.load() @@ -111,10 +114,10 @@ class Model(ModelRow): self, follow: bool = False, save_all: bool = False, - relation_map: Dict = None, - exclude: Union[Set, Dict] = None, + relation_map: Optional[Dict] = None, + exclude: Union[Set, Dict, None] = None, update_count: int = 0, - previous_model: "Model" = None, + previous_model: Optional["Model"] = None, relation_field: Optional["ForeignKeyField"] = None, ) -> int: """ @@ -210,7 +213,7 @@ class Model(ModelRow): return update_count - async def update(self: T, _columns: List[str] = None, **kwargs: Any) -> T: + async def update(self: T, _columns: Optional[List[str]] = None, **kwargs: Any) -> T: """ Performs update of Model instance in the database. Fields can be updated before or you can pass them as kwargs. @@ -240,14 +243,14 @@ class Model(ModelRow): sender=self.__class__, instance=self, passed_args=kwargs ) self_fields = self._extract_model_db_fields() - self_fields.pop(self.get_column_name_from_alias(self.Meta.pkname)) + self_fields.pop(self.get_column_name_from_alias(self.ormar_config.pkname)) if _columns: self_fields = {k: v for k, v in self_fields.items() if k in _columns} self_fields = self.translate_columns_to_aliases(self_fields) - expr = self.Meta.table.update().values(**self_fields) - expr = expr.where(self.pk_column == getattr(self, self.Meta.pkname)) + expr = self.ormar_config.table.update().values(**self_fields) + expr = expr.where(self.pk_column == getattr(self, self.ormar_config.pkname)) - await self.Meta.database.execute(expr) + await self.ormar_config.database.execute(expr) self.set_save_status(True) await self.signals.post_update.send(sender=self.__class__, instance=self) return self @@ -268,9 +271,9 @@ class Model(ModelRow): :rtype: int """ await self.signals.pre_delete.send(sender=self.__class__, instance=self) - expr = self.Meta.table.delete() - expr = expr.where(self.pk_column == (getattr(self, self.Meta.pkname))) - result = await self.Meta.database.execute(expr) + expr = self.ormar_config.table.delete() + expr = expr.where(self.pk_column == (getattr(self, self.ormar_config.pkname))) + result = await self.ormar_config.database.execute(expr) self.set_save_status(False) await self.signals.post_delete.send(sender=self.__class__, instance=self) return result @@ -286,8 +289,8 @@ class Model(ModelRow): :return: reloaded Model :rtype: Model """ - expr = self.Meta.table.select().where(self.pk_column == self.pk) - row = await self.Meta.database.fetch_one(expr) + expr = self.ormar_config.table.select().where(self.pk_column == self.pk) + row = await self.ormar_config.database.fetch_one(expr) if not row: # pragma nocover raise NoMatch("Instance was deleted from database and cannot be refreshed") kwargs = dict(row) @@ -299,8 +302,8 @@ class Model(ModelRow): async def load_all( self: T, follow: bool = False, - exclude: Union[List, str, Set, Dict] = None, - order_by: Union[List, str] = None, + exclude: Union[List, str, Set, Dict, None] = None, + order_by: Union[List, str, None] = None, ) -> T: """ Allow to refresh existing Models fields from database. @@ -341,5 +344,5 @@ class Model(ModelRow): queryset = queryset.order_by(order_by) instance = await queryset.select_related(relations).get(pk=self.pk) self._orm.clear() - self.update_from_dict(instance.dict()) + self.update_from_dict(instance.model_dump()) return self diff --git a/ormar/models/model_row.py b/ormar/models/model_row.py index e3cf6ec..db0d2ce 100644 --- a/ormar/models/model_row.py +++ b/ormar/models/model_row.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, Union, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union, cast try: from sqlalchemy.engine.result import ResultProxy # type: ignore @@ -21,13 +21,13 @@ class ModelRow(NewBaseModel): cls, row: ResultProxy, source_model: Type["Model"], - select_related: List = None, + select_related: Optional[List] = None, related_models: Any = None, - related_field: "ForeignKeyField" = None, - excludable: ExcludableItems = None, + related_field: Optional["ForeignKeyField"] = None, + excludable: Optional[ExcludableItems] = None, current_relation_str: str = "", proxy_source_model: Optional[Type["Model"]] = None, - used_prefixes: List[str] = None, + used_prefixes: Optional[List[str]] = None, ) -> Optional["Model"]: """ Model method to convert raw sql row from database into ormar.Model instance. @@ -97,7 +97,7 @@ class ModelRow(NewBaseModel): ) instance: Optional["Model"] = None - if item.get(cls.Meta.pkname, None) is not None: + if item.get(cls.ormar_config.pkname, None) is not None: item["__excluded__"] = cls.get_names_to_exclude( excludable=excludable, alias=table_prefix ) @@ -130,11 +130,11 @@ class ModelRow(NewBaseModel): previous_model = related_field.through else: previous_model = related_field.owner - table_prefix = cls.Meta.alias_manager.resolve_relation_alias( + table_prefix = cls.ormar_config.alias_manager.resolve_relation_alias( from_model=previous_model, relation_name=related_field.name ) if not table_prefix or table_prefix in used_prefixes: - manager = cls.Meta.alias_manager + manager = cls.ormar_config.alias_manager table_prefix = manager.resolve_relation_alias_after_complex( source_model=source_model, relation_str=current_relation_str, @@ -153,8 +153,8 @@ class ModelRow(NewBaseModel): excludable: ExcludableItems, table_prefix: str, used_prefixes: List[str], - current_relation_str: str = None, - proxy_source_model: Type["Model"] = None, + current_relation_str: Optional[str] = None, + proxy_source_model: Optional[Type["Model"]] = None, ) -> dict: """ Traverses structure of related models and populates the nested models @@ -186,7 +186,7 @@ class ModelRow(NewBaseModel): """ for related in related_models: - field = cls.Meta.model_fields[related] + field = cls.ormar_config.model_fields[related] field = cast("ForeignKeyField", field) model_cls = field.to model_excludable = excludable.get( @@ -281,7 +281,7 @@ class ModelRow(NewBaseModel): :param proxy_source_model: source model from which querysetproxy is constructed :type proxy_source_model: Type["Model"] """ - through_name = cls.Meta.model_fields[related].through.get_name() + through_name = cls.ormar_config.model_fields[related].through.get_name() through_child = cls._create_through_instance( row=row, related=related, through_name=through_name, excludable=excludable ) @@ -315,8 +315,8 @@ class ModelRow(NewBaseModel): :return: initialized through model without relation :rtype: "ModelRow" """ - model_cls = cls.Meta.model_fields[through_name].to - table_prefix = cls.Meta.alias_manager.resolve_relation_alias( + model_cls = cls.ormar_config.model_fields[through_name].to + table_prefix = cls.ormar_config.alias_manager.resolve_relation_alias( from_model=cls, relation_name=related ) # remove relations on through field @@ -372,7 +372,7 @@ class ModelRow(NewBaseModel): ) column_prefix = table_prefix + "_" if table_prefix else "" - for column in cls.Meta.table.columns: + for column in cls.ormar_config.table.columns: alias = cls.get_column_name_from_alias(column.name) if alias not in item and alias in selected_columns: prefixed_name = f"{column_prefix}{column.name}" diff --git a/ormar/models/newbasemodel.py b/ormar/models/newbasemodel.py index 51182bd..77abe47 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -2,16 +2,16 @@ import base64 import sys import warnings from typing import ( + TYPE_CHECKING, AbstractSet, Any, - Callable, Dict, List, + Literal, Mapping, MutableSequence, Optional, Set, - TYPE_CHECKING, Tuple, Type, TypeVar, @@ -19,33 +19,32 @@ from typing import ( cast, ) -import databases import pydantic import sqlalchemy -from pydantic import BaseModel - +import typing_extensions import ormar # noqa I100 from ormar.exceptions import ModelError, ModelPersistenceError from ormar.fields import BaseField from ormar.fields.foreign_key import ForeignKeyField -from ormar.fields.parsers import encode_json +from ormar.fields.parsers import decode_bytes, encode_json from ormar.models.helpers import register_relation_in_alias_manager from ormar.models.helpers.relations import expand_reverse_relationship from ormar.models.helpers.sqlalchemy import ( - populate_meta_sqlalchemy_table_if_required, + populate_config_sqlalchemy_table_if_required, update_column_definition, ) -from ormar.models.metaclass import ModelMeta, ModelMetaclass +from ormar.models.metaclass import ModelMetaclass from ormar.models.modelproxy import ModelTableProxy from ormar.models.utils import Extra from ormar.queryset.utils import translate_list_to_dict from ormar.relations.alias_manager import AliasManager from ormar.relations.relation import Relation from ormar.relations.relation_manager import RelationsManager +from ormar.warnings import OrmarDeprecatedSince020 if TYPE_CHECKING: # pragma no cover - from ormar.models import Model + from ormar.models import Model, OrmarConfig from ormar.signals import SignalEmitter T = TypeVar("T", bound="NewBaseModel") @@ -74,17 +73,12 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass "_pk_column", "__pk_only__", "__cached_hash__", + "__pydantic_extra__", + "__pydantic_fields_set__", ) if TYPE_CHECKING: # pragma no cover pk: Any - __model_fields__: Dict[str, BaseField] - __table__: sqlalchemy.Table - __pydantic_model__: Type[BaseModel] - __pkname__: str - __tablename__: str - __metadata__: sqlalchemy.MetaData - __database__: databases.Database __relation_map__: Optional[List[str]] __cached_hash__: Optional[int] _orm_relationship_manager: AliasManager @@ -94,12 +88,10 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass _related_names: Optional[Set] _through_names: Optional[Set] _related_names_hash: str - _choices_fields: Set - _pydantic_fields: Set _quick_access_fields: Set _json_fields: Set _bytes_fields: Set - Meta: ModelMeta + ormar_config: OrmarConfig # noinspection PyMissingConstructor def __init__(self, *args: Any, **kwargs: Any) -> None: # type: ignore @@ -117,7 +109,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass Json fields are automatically loaded/dumped if needed. - Models marked as abstract=True in internal Meta class cannot be initialized. + Models marked as abstract=True in internal OrmarConfig cannot be initialized. Accepts also special __pk_only__ flag that indicates that Model is constructed only with primary key value (so no other fields, it's a child model on other @@ -144,31 +136,23 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass new_kwargs, through_tmp_dict = self._process_kwargs(kwargs) if not pk_only: - values, fields_set, validation_error = pydantic.validate_model( - self, new_kwargs # type: ignore + self.__pydantic_validator__.validate_python( + new_kwargs, self_instance=self # type: ignore ) - if validation_error: - raise validation_error else: - fields_set = {self.Meta.pkname} + fields_set = {self.ormar_config.pkname} values = new_kwargs - - object.__setattr__(self, "__dict__", values) - object.__setattr__(self, "__fields_set__", fields_set) - + object.__setattr__(self, "__dict__", values) + object.__setattr__(self, "__pydantic_fields_set__", fields_set) # add back through fields new_kwargs.update(through_tmp_dict) - model_fields = object.__getattribute__(self, "Meta").model_fields + model_fields = object.__getattribute__(self, "ormar_config").model_fields # register the columns models after initialization for related in self.extract_related_names().union(self.extract_through_names()): model_fields[related].expand_relationship( new_kwargs.get(related), self, to_register=True ) - if hasattr(self, "_init_private_attributes"): - # introduced in pydantic 1.7 - self._init_private_attributes() - def __setattr__(self, name: str, value: Any) -> None: # noqa CCR001 """ Overwrites setattr in pydantic parent as otherwise descriptors are not called. @@ -189,7 +173,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass super().__setattr__(name, value) # In this case, the hash could have changed, so update it - if name == self.Meta.pkname or self.pk is None: + if name == self.ormar_config.pkname or self.pk is None: object.__setattr__(self, "__cached_hash__", None) new_hash = hash(self) @@ -198,20 +182,21 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass def __getattr__(self, item: str) -> Any: """ - Used only to silence mypy errors for Through models and reverse relations. - Not used in real life as in practice calls are intercepted - by RelationDescriptors + Used for private attributes of pydantic v2. :param item: name of attribute :type item: str :return: Any :rtype: Any """ - return super().__getattribute__(item) + # TODO: Check __pydantic_extra__ + if item == "__pydantic_extra__": + return None + return super().__getattr__(item) # type: ignore def __getstate__(self) -> Dict[Any, Any]: state = super().__getstate__() - self_dict = self.dict() + self_dict = self.model_dump() state["__dict__"].update(**self_dict) return state @@ -274,9 +259,9 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass :return: None :rtype: None """ - if self.Meta.abstract: + if self.ormar_config.abstract: raise ModelError(f"You cannot initialize abstract model {self.get_name()}") - if self.Meta.requires_ref_update: + if self.ormar_config.requires_ref_update: raise ModelError( f"Model {self.get_name()} has not updated " f"ForwardRefs. \nBefore using the model you " @@ -289,7 +274,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass Removes property_fields - Checks if field is in the model fields or pydatnic fields. + Checks if field is in the model fields or pydantic fields. Nullifies fields that should be excluded. @@ -300,9 +285,9 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass :return: modified kwargs :rtype: Tuple[Dict, Dict] """ - property_fields = self.Meta.property_fields - model_fields = self.Meta.model_fields - pydantic_fields = set(self.__fields__.keys()) + property_fields = self.ormar_config.property_fields + model_fields = self.ormar_config.model_fields + pydantic_fields = set(self.model_fields.keys()) # remove property fields for prop_filed in property_fields: @@ -310,7 +295,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass excluded: Set[str] = kwargs.pop("__excluded__", set()) if "pk" in kwargs: - kwargs[self.Meta.pkname] = kwargs.pop("pk") + kwargs[self.ormar_config.pkname] = kwargs.pop("pk") # extract through fields through_tmp_dict = dict() @@ -326,9 +311,13 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass k, self._convert_json( k, - model_fields[k].expand_relationship(v, self, to_register=False) - if k in model_fields - else (v if k in pydantic_fields else model_fields[k]), + ( + model_fields[k].expand_relationship( + v, self, to_register=False + ) + if k in model_fields + else (v if k in pydantic_fields else model_fields[k]) + ), ), ) for k, v in kwargs.items() @@ -360,7 +349,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass :return: dict without extra fields :rtype: Dict """ - if self.Meta.extra == Extra.ignore: + if self.ormar_config.extra == Extra.ignore: kwargs = { k: v for k, v in kwargs.items() @@ -432,22 +421,6 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass else: return hash(self) == other.__hash__() - def _copy_and_set_values( - self: "NewBaseModel", values: "DictStrAny", fields_set: "SetStr", *, deep: bool - ) -> "NewBaseModel": - """ - Overwrite related models values with dict representation to avoid infinite - recursion through related fields. - """ - self_dict = values - self_dict.update(self.dict(exclude_list=True)) - return cast( - "NewBaseModel", - super()._copy_and_set_values( - values=self_dict, fields_set=fields_set, deep=deep - ), - ) - @classmethod def get_name(cls, lower: bool = True) -> str: """ @@ -466,7 +439,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass @property def pk_column(self) -> sqlalchemy.Column: """ - Retrieves primary key sqlalchemy column from models Meta.table. + Retrieves primary key sqlalchemy column from models OrmarConfig.table. Each model has to have primary key. Only one primary key column is allowed. @@ -475,7 +448,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass """ if object.__getattribute__(self, "_pk_column") is not None: return object.__getattribute__(self, "_pk_column") - pk_columns = self.Meta.table.primary_key.columns.values() + pk_columns = self.ormar_config.table.primary_key.columns.values() pk_col = pk_columns[0] object.__setattr__(self, "_pk_column", pk_col) return pk_col @@ -487,19 +460,19 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass @property def signals(self) -> "SignalEmitter": - """Exposes signals from model Meta""" - return self.Meta.signals + """Exposes signals from model OrmarConfig""" + return self.ormar_config.signals @classmethod def pk_type(cls) -> Any: """Shortcut to models primary key field type""" - return cls.Meta.model_fields[cls.Meta.pkname].__type__ + return cls.ormar_config.model_fields[cls.ormar_config.pkname].__type__ @classmethod def db_backend_name(cls) -> str: """Shortcut to database dialect, cause some dialect require different treatment""" - return cls.Meta.database._backend._dialect.name + return cls.ormar_config.database._backend._dialect.name def remove(self, parent: "Model", name: str) -> None: """Removes child from relation with given name in RelationshipManager""" @@ -509,32 +482,6 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass """Sets value of the save status""" object.__setattr__(self, "_orm_saved", status) - @classmethod - def get_properties( - cls, include: Union[Set, Dict, None], exclude: Union[Set, Dict, None] - ) -> Set[str]: - """ - Returns a set of names of functions/fields decorated with - @property_field decorator. - - They are added to dictionary when called directly and therefore also are - present in fastapi responses. - - :param include: fields to include - :type include: Union[Set, Dict, None] - :param exclude: fields to exclude - :type exclude: Union[Set, Dict, None] - :return: set of property fields names - :rtype: Set[str] - """ - - props = cls.Meta.property_fields - if include: - props = {prop for prop in props if prop in include} - if exclude: - props = {prop for prop in props if prop not in exclude} - return props - @classmethod def update_forward_refs(cls, **localns: Any) -> None: """ @@ -544,7 +491,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass Expands relationships, register relation in alias manager and substitutes sqlalchemy columns with new ones with proper column type (null before). - Populates Meta table of the Model which is left empty before. + Populates OrmarConfig table of the Model which is left empty before. Sets self_reference flag on models that links to themselves. @@ -557,7 +504,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass """ globalns = sys.modules[cls.__module__].__dict__.copy() globalns.setdefault(cls.__name__, cls) - fields_to_check = cls.Meta.model_fields.copy() + fields_to_check = cls.ormar_config.model_fields.copy() for field in fields_to_check.values(): if field.has_unresolved_forward_refs(): field = cast(ForeignKeyField, field) @@ -569,9 +516,10 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass expand_reverse_relationship(model_field=field) register_relation_in_alias_manager(field=field) update_column_definition(model=cls, field=field) - populate_meta_sqlalchemy_table_if_required(meta=cls.Meta) - super().update_forward_refs(**localns) - cls.Meta.requires_ref_update = False + populate_config_sqlalchemy_table_if_required(config=cls.ormar_config) + # super().update_forward_refs(**localns) + cls.model_rebuild(force=True) + cls.ormar_config.requires_ref_update = False @staticmethod def _get_not_excluded_fields( @@ -626,26 +574,84 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass result = [] for model in models: try: - result.append( - model.dict( - relation_map=relation_map, + model_dict = model.model_dump( + relation_map=relation_map, + include=include, + exclude=exclude, + exclude_primary_keys=exclude_primary_keys, + exclude_through_models=exclude_through_models, + ) + if not exclude_through_models: + model.populate_through_models( + model=model, + model_dict=model_dict, include=include, exclude=exclude, - exclude_primary_keys=exclude_primary_keys, - exclude_through_models=exclude_through_models, + relation_map=relation_map, ) - ) + result.append(model_dict) except ReferenceError: # pragma no cover continue return result + @staticmethod + def populate_through_models( + model: "Model", + model_dict: Dict, + include: Union[Set, Dict], + exclude: Union[Set, Dict], + relation_map: Dict, + ) -> None: + """ + Populates through models with values from dict representation. + + :param model: model to populate through models + :type model: Model + :param model_dict: dict representation of the model + :type model_dict: Dict + :param include: fields to include + :type include: Dict + :param exclude: fields to exclude + :type exclude: Dict + :param relation_map: map of relations to follow to avoid circular refs + :type relation_map: Dict + :return: None + :rtype: None + """ + + include_dict = ( + translate_list_to_dict(include) + if (include and isinstance(include, Set)) + else include + ) + exclude_dict = ( + translate_list_to_dict(exclude) + if (exclude and isinstance(exclude, Set)) + else exclude + ) + models_to_populate = model._get_not_excluded_fields( + fields=model.extract_through_names(), + include=cast(Optional[Dict], include_dict), + exclude=cast(Optional[Dict], exclude_dict), + ) + through_fields_to_populate = [ + model.ormar_config.model_fields[through_model] + for through_model in models_to_populate + if model.ormar_config.model_fields[through_model].related_name + not in relation_map + ] + for through_field in through_fields_to_populate: + through_instance = getattr(model, through_field.name) + if through_instance: + model_dict[through_field.name] = through_instance.model_dump() + @classmethod def _skip_ellipsis( cls, items: Union[Set, Dict, None], key: str, default_return: Any = None ) -> Union[Set, Dict, None]: """ Helper to traverse the include/exclude dictionaries. - In dict() Ellipsis should be skipped as it indicates all fields required + In model_dump() Ellipsis should be skipped as it indicates all fields required and not the actual set/dict with fields names. :param items: current include/exclude value @@ -722,8 +728,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass exclude_through_models=exclude_through_models, ) elif nested_model is not None: - - dict_instance[field] = nested_model.dict( + model_dict = nested_model.model_dump( relation_map=self._skip_ellipsis( relation_map, field, default_return=dict() ), @@ -732,26 +737,78 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass exclude_primary_keys=exclude_primary_keys, exclude_through_models=exclude_through_models, ) + if not exclude_through_models: + nested_model.populate_through_models( + model=nested_model, + model_dict=model_dict, + include=self._convert_all( + self._skip_ellipsis(include, field) + ), + exclude=self._convert_all( + self._skip_ellipsis(exclude, field) + ), + relation_map=self._skip_ellipsis( + relation_map, field, default_return=dict() + ), + ) + dict_instance[field] = model_dict else: dict_instance[field] = None - except ReferenceError: + except ReferenceError: # pragma: no cover dict_instance[field] = None return dict_instance + @typing_extensions.deprecated( + "The `dict` method is deprecated; use `model_dump` instead.", + category=OrmarDeprecatedSince020, + ) def dict( # type: ignore # noqa A003 self, *, - include: Union[Set, Dict] = None, - exclude: Union[Set, Dict] = None, + include: Union[Set, Dict, None] = None, + exclude: Union[Set, Dict, None] = None, by_alias: bool = False, - skip_defaults: bool = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, exclude_primary_keys: bool = False, exclude_through_models: bool = False, exclude_list: bool = False, - relation_map: Dict = None, + relation_map: Optional[Dict] = None, + ) -> "DictStrAny": # noqa: A003 # pragma: no cover + warnings.warn( + "The `dict` method is deprecated; use `model_dump` instead.", + DeprecationWarning, + ) + return self.model_dump( + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + exclude_primary_keys=exclude_primary_keys, + exclude_through_models=exclude_through_models, + exclude_list=exclude_list, + relation_map=relation_map, + ) + + def model_dump( # type: ignore # noqa A003 + self, + *, + mode: Union[Literal["json", "python"], str] = "python", + include: Union[Set, Dict, None] = None, + exclude: Union[Set, Dict, None] = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_primary_keys: bool = False, + exclude_through_models: bool = False, + exclude_list: bool = False, + relation_map: Optional[Dict] = None, + round_trip: bool = False, + warnings: bool = True, ) -> "DictStrAny": # noqa: A003' """ @@ -760,7 +817,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass Nested models are also parsed to dictionaries. - Additionally fields decorated with @property_field are also added. + Additionally, fields decorated with @property_field are also added. :param exclude_through_models: flag to exclude through models from dict :type exclude_through_models: bool @@ -772,8 +829,6 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass :type exclude: Union[Set, Dict, None] :param by_alias: flag to get values by alias - passed to pydantic :type by_alias: bool - :param skip_defaults: flag to not set values - passed to pydantic - :type skip_defaults: bool :param exclude_unset: flag to exclude not set values - passed to pydantic :type exclude_unset: bool :param exclude_defaults: flag to exclude default values - passed to pydantic @@ -782,8 +837,16 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass :type exclude_none: bool :param exclude_list: flag to exclude lists of nested values models from dict :type exclude_list: bool - :param relation_map: map of the relations to follow to avoid circural deps + :param relation_map: map of the relations to follow to avoid circular deps :type relation_map: Dict + :param mode: The mode in which `to_python` should run. + If mode is 'json', the dictionary will only contain JSON serializable types. + If mode is 'python', the dictionary may contain any Python objects. + :type mode: str + :param round_trip: flag to enable serialization round-trip support + :type round_trip: bool + :param warnings: flag to log warnings for invalid fields + :type warnings: bool :return: :rtype: """ @@ -793,14 +856,16 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass exclude_primary_keys=exclude_primary_keys, exclude_through_models=exclude_through_models, ) - dict_instance = super().dict( + dict_instance = super().model_dump( + mode=mode, include=include, exclude=pydantic_exclude, by_alias=by_alias, - skip_defaults=skip_defaults, - exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, + exclude_unset=exclude_unset, exclude_none=exclude_none, + round_trip=round_trip, + warnings=False, ) dict_instance = { @@ -808,10 +873,12 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass for k, v in dict_instance.items() } - if include and isinstance(include, Set): - include = translate_list_to_dict(include) - if exclude and isinstance(exclude, Set): - exclude = translate_list_to_dict(exclude) + include_dict = ( + translate_list_to_dict(include) if isinstance(include, Set) else include + ) + exclude_dict = ( + translate_list_to_dict(exclude) if isinstance(exclude, Set) else exclude + ) relation_map = ( relation_map @@ -823,32 +890,57 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass dict_instance = self._extract_nested_models( relation_map=relation_map, dict_instance=dict_instance, - include=include, # type: ignore - exclude=exclude, # type: ignore + include=include_dict, + exclude=exclude_dict, exclude_primary_keys=exclude_primary_keys, exclude_through_models=exclude_through_models, exclude_list=exclude_list, ) - # include model properties as fields in dict - if object.__getattribute__(self, "Meta").property_fields: - props = self.get_properties(include=include, exclude=exclude) - if props: - dict_instance.update({prop: getattr(self, prop) for prop in props}) - return dict_instance + @typing_extensions.deprecated( + "The `json` method is deprecated; use `model_dump_json` instead.", + category=OrmarDeprecatedSince020, + ) def json( # type: ignore # noqa A003 self, *, - include: Union[Set, Dict] = None, - exclude: Union[Set, Dict] = None, + include: Union[Set, Dict, None] = None, + exclude: Union[Set, Dict, None] = None, + by_alias: bool = False, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_primary_keys: bool = False, + exclude_through_models: bool = False, + **dumps_kwargs: Any, + ) -> str: # pragma: no cover + warnings.warn( + "The `json` method is deprecated; use `model_dump_json` instead.", + DeprecationWarning, + ) + return self.model_dump_json( + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + exclude_primary_keys=exclude_primary_keys, + exclude_through_models=exclude_through_models, + **dumps_kwargs, + ) + + def model_dump_json( # type: ignore # noqa A003 + self, + *, + include: Union[Set, Dict, None] = None, + exclude: Union[Set, Dict, None] = None, by_alias: bool = False, - skip_defaults: bool = None, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, - encoder: Optional[Callable[[Any], Any]] = None, exclude_primary_keys: bool = False, exclude_through_models: bool = False, **dumps_kwargs: Any, @@ -860,15 +952,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass `encoder` is an optional function to supply as `default` to json.dumps(), other arguments as per `json.dumps()`. """ - if skip_defaults is not None: # pragma: no cover - warnings.warn( - f'{self.__class__.__name__}.json(): "skip_defaults" is deprecated ' - f'and replaced by "exclude_unset"', - DeprecationWarning, - ) - exclude_unset = skip_defaults - encoder = cast(Callable[[Any], Any], encoder or self.__json_encoder__) - data = self.dict( + data = self.model_dump( include=include, exclude=exclude, by_alias=by_alias, @@ -878,12 +962,24 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass exclude_primary_keys=exclude_primary_keys, exclude_through_models=exclude_through_models, ) - if self.__custom_root_type__: # pragma: no cover - data = data["__root__"] - return self.__config__.json_dumps(data, default=encoder, **dumps_kwargs) + return self.__pydantic_serializer__.to_json(data, warnings=False).decode() @classmethod + @typing_extensions.deprecated( + "The `construct` method is deprecated; use `model_construct` instead.", + category=OrmarDeprecatedSince020, + ) def construct( + cls: Type["T"], _fields_set: Union[Set[str], None] = None, **values: Any + ) -> "T": # pragma: no cover + warnings.warn( + "The `construct` method is deprecated; use `model_construct` instead.", + DeprecationWarning, + ) + return cls.model_construct(_fields_set=_fields_set, **values) + + @classmethod + def model_construct( cls: Type["T"], _fields_set: Optional["SetStr"] = None, **values: Any ) -> "T": own_values = { @@ -891,18 +987,52 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass } model = cls.__new__(cls) fields_values: Dict[str, Any] = {} - for name, field in cls.__fields__.items(): + for name, field in cls.model_fields.items(): if name in own_values: fields_values[name] = own_values[name] - elif not field.required: + elif not field.is_required(): fields_values[name] = field.get_default() fields_values.update(own_values) + + if _fields_set is None: + _fields_set = set(values.keys()) + + extra_allowed = cls.model_config.get("extra") == "allow" + if not extra_allowed: + fields_values.update(values) object.__setattr__(model, "__dict__", fields_values) model._initialize_internal_attributes() cls._construct_relations(model=model, values=values) - if _fields_set is None: - _fields_set = set(values.keys()) - object.__setattr__(model, "__fields_set__", _fields_set) + object.__setattr__(model, "__pydantic_fields_set__", _fields_set) + return cls._pydantic_model_construct_finalizer( + model=model, extra_allowed=extra_allowed, values=values + ) + + @classmethod + def _pydantic_model_construct_finalizer( + cls: Type["T"], model: "T", extra_allowed: bool, **values: Any + ) -> "T": + """ + Recreate pydantic model_construct logic here as we do not call super method. + """ + _extra: Union[Dict[str, Any], None] = None + if extra_allowed: # pragma: no cover + _extra = {} + for k, v in values.items(): + _extra[k] = v + + if not cls.__pydantic_root_model__: + object.__setattr__(model, "__pydantic_extra__", _extra) + + if cls.__pydantic_post_init__: # pragma: no cover + model.model_post_init(None) + elif not cls.__pydantic_root_model__: + # Note: if there are any private attributes, + # cls.__pydantic_post_init__ would exist + # Since it doesn't, that means that `__pydantic_private__` + # should be set to None + object.__setattr__(model, "__pydantic_private__", None) + return model @classmethod @@ -914,7 +1044,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass value_to_set = values[relation] if not isinstance(value_to_set, list): value_to_set = [value_to_set] - relation_field = cls.Meta.model_fields[relation] + relation_field = cls.ormar_config.model_fields[relation] relation_value = [ relation_field.expand_relationship(x, model, to_register=False) for x in value_to_set @@ -954,12 +1084,11 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass """ if column_name not in self._bytes_fields: return value - field = self.Meta.model_fields[column_name] - if not isinstance(value, bytes) and value is not None: - if field.represent_as_base64_str: - value = base64.b64decode(value) - else: - value = value.encode("utf-8") + field = self.ormar_config.model_fields[column_name] + if value is not None: + value = decode_bytes( + value=value, represent_as_string=field.represent_as_base64_str + ) return value def _convert_bytes_to_str(self, column_name: str, value: Any) -> Union[str, Dict]: @@ -975,7 +1104,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass """ if column_name not in self._bytes_fields: return value - field = self.Meta.model_fields[column_name] + field = self.ormar_config.model_fields[column_name] if ( value is not None and not isinstance(value, str) @@ -1021,16 +1150,15 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass :return: dictionary of fields names and values. :rtype: Dict """ - # TODO: Cache this dictionary? self_fields = self._extract_own_model_fields() self_fields = { k: v for k, v in self_fields.items() - if self.get_column_alias(k) in self.Meta.table.columns + if self.get_column_alias(k) in self.ormar_config.table.columns } for field in self._extract_db_related_names(): - relation_field = self.Meta.model_fields[field] - target_pk_name = relation_field.to.Meta.pkname + relation_field = self.ormar_config.model_fields[field] + target_pk_name = relation_field.to.ormar_config.pkname target_field = getattr(self, field) self_fields[field] = getattr(target_field, target_pk_name, None) if not relation_field.nullable and not self_fields[field]: diff --git a/ormar/models/ormar_config.py b/ormar/models/ormar_config.py new file mode 100644 index 0000000..e4941b1 --- /dev/null +++ b/ormar/models/ormar_config.py @@ -0,0 +1,85 @@ +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Type, Union + +import databases +import sqlalchemy +from sqlalchemy.sql.schema import ColumnCollectionConstraint + +from ormar.fields import BaseField, ForeignKeyField, ManyToManyField +from ormar.models.helpers import alias_manager +from ormar.models.utils import Extra +from ormar.queryset.queryset import QuerySet +from ormar.relations import AliasManager +from ormar.signals import SignalEmitter + + +class OrmarConfig: + + if TYPE_CHECKING: # pragma: no cover + pkname: str + metadata: sqlalchemy.MetaData + database: databases.Database + tablename: str + order_by: List[str] + abstract: bool + exclude_parent_fields: List[str] + constraints: List[ColumnCollectionConstraint] + + def __init__( + self, + metadata: Optional[sqlalchemy.MetaData] = None, + database: Optional[databases.Database] = None, + engine: Optional[sqlalchemy.engine.Engine] = None, + tablename: Optional[str] = None, + order_by: Optional[List[str]] = None, + abstract: bool = False, + exclude_parent_fields: Optional[List[str]] = None, + queryset_class: Type[QuerySet] = QuerySet, + extra: Extra = Extra.forbid, + constraints: Optional[List[ColumnCollectionConstraint]] = None, + ) -> None: + self.pkname = None # type: ignore + self.metadata = metadata + self.database = database # type: ignore + self.engine = engine # type: ignore + self.tablename = tablename # type: ignore + self.orders_by = order_by or [] + self.columns: List[sqlalchemy.Column] = [] + self.constraints = constraints or [] + self.model_fields: Dict[ + str, Union[BaseField, ForeignKeyField, ManyToManyField] + ] = {} + self.alias_manager: AliasManager = alias_manager + self.property_fields: Set = set() + self.signals: SignalEmitter = SignalEmitter() + self.abstract = abstract + self.requires_ref_update: bool = False + self.exclude_parent_fields = exclude_parent_fields or [] + self.extra = extra + self.queryset_class = queryset_class + self.table: sqlalchemy.Table = None + + def copy( + self, + metadata: Optional[sqlalchemy.MetaData] = None, + database: Optional[databases.Database] = None, + engine: Optional[sqlalchemy.engine.Engine] = None, + tablename: Optional[str] = None, + order_by: Optional[List[str]] = None, + abstract: Optional[bool] = None, + exclude_parent_fields: Optional[List[str]] = None, + queryset_class: Optional[Type[QuerySet]] = None, + extra: Optional[Extra] = None, + constraints: Optional[List[ColumnCollectionConstraint]] = None, + ) -> "OrmarConfig": + return OrmarConfig( + metadata=metadata or self.metadata, + database=database or self.database, + engine=engine or self.engine, + tablename=tablename, + order_by=order_by, + abstract=abstract or self.abstract, + exclude_parent_fields=exclude_parent_fields, + queryset_class=queryset_class or self.queryset_class, + extra=extra or self.extra, + constraints=constraints, + ) diff --git a/ormar/models/quick_access_views.py b/ormar/models/quick_access_views.py index a822375..17cadb7 100644 --- a/ormar/models/quick_access_views.py +++ b/ormar/models/quick_access_views.py @@ -2,14 +2,17 @@ Contains set of fields/methods etc names that are used to bypass the checks in NewBaseModel __getattribute__ calls to speed the calls. """ + quick_access_set = { "Config", - "Meta", + "model_config", + "model_fields", + "__cached_hash__", "__class__", "__config__", "__custom_root_type__", "__dict__", - "__fields__", + "model_fields", "__fields_set__", "__json_encoder__", "__pk_only__", @@ -18,7 +21,6 @@ quick_access_set = { "__private_attributes__", "__same__", "_calculate_keys", - "_choices_fields", "_convert_json", "_extract_db_related_names", "_extract_model_db_fields", diff --git a/ormar/models/traversible.py b/ormar/models/traversible.py index 3a90bb9..0bb3237 100644 --- a/ormar/models/traversible.py +++ b/ormar/models/traversible.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional, TYPE_CHECKING, Type +from typing import TYPE_CHECKING, Any, List, Optional, Type if TYPE_CHECKING: # pragma no cover from ormar.models.mixins.relation_mixin import RelationMixin @@ -18,8 +18,8 @@ class NodeList: def add( self, node_class: Type["RelationMixin"], - relation_name: str = None, - parent_node: "Node" = None, + relation_name: Optional[str] = None, + parent_node: Optional["Node"] = None, ) -> "Node": """ Adds new Node or returns the existing one @@ -50,7 +50,7 @@ class NodeList: self, node_class: Type["RelationMixin"], relation_name: Optional[str] = None, - parent_node: "Node" = None, + parent_node: Optional["Node"] = None, ) -> Optional["Node"]: """ Searches for existing node with given parameters @@ -78,8 +78,8 @@ class Node: def __init__( self, node_class: Type["RelationMixin"], - relation_name: str = None, - parent_node: "Node" = None, + relation_name: Optional[str] = None, + parent_node: Optional["Node"] = None, ) -> None: self.relation_name = relation_name self.node_class = node_class @@ -108,7 +108,7 @@ class Node: :return: result of the check :rtype: bool """ - target_model = self.node_class.Meta.model_fields[relation_name].to + target_model = self.node_class.ormar_config.model_fields[relation_name].to if self.parent_node: node = self while node.parent_node: diff --git a/ormar/protocols/queryset_protocol.py b/ormar/protocols/queryset_protocol.py index a2c1d58..fb8bcb9 100644 --- a/ormar/protocols/queryset_protocol.py +++ b/ormar/protocols/queryset_protocol.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Sequence, Set, TYPE_CHECKING, Tuple, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Set, Tuple, Union try: from typing import Protocol @@ -17,59 +17,44 @@ class QuerySetProtocol(Protocol): # pragma: nocover def exclude(self, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001 ... - def select_related(self, related: Union[List, str]) -> "QuerysetProxy": - ... + def select_related(self, related: Union[List, str]) -> "QuerysetProxy": ... - def prefetch_related(self, related: Union[List, str]) -> "QuerysetProxy": - ... + def prefetch_related(self, related: Union[List, str]) -> "QuerysetProxy": ... - async def exists(self) -> bool: - ... + async def exists(self) -> bool: ... - async def count(self, distinct: bool = True) -> int: - ... + async def count(self, distinct: bool = True) -> int: ... - async def clear(self) -> int: - ... + async def clear(self) -> int: ... - def limit(self, limit_count: int) -> "QuerysetProxy": - ... + def limit(self, limit_count: int) -> "QuerysetProxy": ... - def offset(self, offset: int) -> "QuerysetProxy": - ... + def offset(self, offset: int) -> "QuerysetProxy": ... - async def first(self, **kwargs: Any) -> "Model": - ... + async def first(self, **kwargs: Any) -> "Model": ... - async def get(self, **kwargs: Any) -> "Model": - ... + async def get(self, **kwargs: Any) -> "Model": ... async def all( # noqa: A003, A001 self, **kwargs: Any - ) -> Sequence[Optional["Model"]]: - ... + ) -> Sequence[Optional["Model"]]: ... - async def create(self, **kwargs: Any) -> "Model": - ... + async def create(self, **kwargs: Any) -> "Model": ... - async def update(self, each: bool = False, **kwargs: Any) -> int: - ... + async def update(self, each: bool = False, **kwargs: Any) -> int: ... async def get_or_create( self, _defaults: Optional[Dict[str, Any]] = None, **kwargs: Any, - ) -> Tuple["Model", bool]: - ... + ) -> Tuple["Model", bool]: ... - async def update_or_create(self, **kwargs: Any) -> "Model": - ... + async def update_or_create(self, **kwargs: Any) -> "Model": ... - def fields(self, columns: Union[List, str, Set, Dict]) -> "QuerysetProxy": - ... + def fields(self, columns: Union[List, str, Set, Dict]) -> "QuerysetProxy": ... - def exclude_fields(self, columns: Union[List, str, Set, Dict]) -> "QuerysetProxy": - ... + def exclude_fields( + self, columns: Union[List, str, Set, Dict] + ) -> "QuerysetProxy": ... - def order_by(self, columns: Union[List, str]) -> "QuerysetProxy": - ... + def order_by(self, columns: Union[List, str]) -> "QuerysetProxy": ... diff --git a/ormar/protocols/relation_protocol.py b/ormar/protocols/relation_protocol.py index b551b8d..4c44bca 100644 --- a/ormar/protocols/relation_protocol.py +++ b/ormar/protocols/relation_protocol.py @@ -10,8 +10,6 @@ if TYPE_CHECKING: # pragma: nocover class RelationProtocol(Protocol): # pragma: nocover - def add(self, child: "Model") -> None: - ... + def add(self, child: "Model") -> None: ... - def remove(self, child: Union["Model", Type["Model"]]) -> None: - ... + def remove(self, child: Union["Model", Type["Model"]]) -> None: ... diff --git a/ormar/queryset/__init__.py b/ormar/queryset/__init__.py index 32fbee4..02e4eec 100644 --- a/ormar/queryset/__init__.py +++ b/ormar/queryset/__init__.py @@ -1,13 +1,11 @@ """ Contains QuerySet and different Query classes to allow for constructing of sql queries. """ + from ormar.queryset.actions import FilterAction, OrderAction, SelectAction from ormar.queryset.clause import and_, or_ from ormar.queryset.field_accessor import FieldAccessor -from ormar.queryset.queries import FilterQuery -from ormar.queryset.queries import LimitQuery -from ormar.queryset.queries import OffsetQuery -from ormar.queryset.queries import OrderQuery +from ormar.queryset.queries import FilterQuery, LimitQuery, OffsetQuery, OrderQuery from ormar.queryset.queryset import QuerySet __all__ = [ diff --git a/ormar/queryset/actions/filter_action.py b/ormar/queryset/actions/filter_action.py index e393333..f5ab832 100644 --- a/ormar/queryset/actions/filter_action.py +++ b/ormar/queryset/actions/filter_action.py @@ -1,4 +1,4 @@ -from typing import Any, TYPE_CHECKING, Type +from typing import TYPE_CHECKING, Any, Type import sqlalchemy @@ -143,8 +143,10 @@ class FilterAction(QueryAction): else: filter_value = self.filter_value if self.table_prefix: - aliased_table = self.source_model.Meta.alias_manager.prefixed_table_name( - self.table_prefix, self.column.table + aliased_table = ( + self.source_model.ormar_config.alias_manager.prefixed_table_name( + self.table_prefix, self.column.table + ) ) aliased_column = getattr(aliased_table.c, self.column.name) else: diff --git a/ormar/queryset/actions/order_action.py b/ormar/queryset/actions/order_action.py index 05114bf..a77563c 100644 --- a/ormar/queryset/actions/order_action.py +++ b/ormar/queryset/actions/order_action.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Type +from typing import TYPE_CHECKING, Optional, Type import sqlalchemy from sqlalchemy import text @@ -20,7 +20,7 @@ class OrderAction(QueryAction): """ def __init__( - self, order_str: str, model_cls: Type["Model"], alias: str = None + self, order_str: str, model_cls: Type["Model"], alias: Optional[str] = None ) -> None: self.direction: str = "" super().__init__(query_str=order_str, model_cls=model_cls) @@ -36,8 +36,10 @@ class OrderAction(QueryAction): @property def is_postgres_bool(self) -> bool: - dialect = self.target_model.Meta.database._backend._dialect.name - field_type = self.target_model.Meta.model_fields[self.field_name].__type__ + dialect = self.target_model.ormar_config.database._backend._dialect.name + field_type = self.target_model.ormar_config.model_fields[ + self.field_name + ].__type__ return dialect == "postgresql" and field_type == bool def get_field_name_text(self) -> str: @@ -78,7 +80,7 @@ class OrderAction(QueryAction): :return: complied and escaped clause :rtype: sqlalchemy.sql.elements.TextClause """ - dialect = self.target_model.Meta.database._backend._dialect + dialect = self.target_model.ormar_config.database._backend._dialect quoter = dialect.identifier_preparer.quote prefix = f"{self.table_prefix}_" if self.table_prefix else "" table_name = self.table.name diff --git a/ormar/queryset/actions/query_action.py b/ormar/queryset/actions/query_action.py index 2c6ee84..b8bd651 100644 --- a/ormar/queryset/actions/query_action.py +++ b/ormar/queryset/actions/query_action.py @@ -1,5 +1,5 @@ import abc -from typing import Any, List, TYPE_CHECKING, Type +from typing import TYPE_CHECKING, Any, List, Type import sqlalchemy @@ -54,13 +54,13 @@ class QueryAction(abc.ABC): @property def table(self) -> sqlalchemy.Table: """Shortcut to sqlalchemy Table of filtered target model""" - return self.target_model.Meta.table + return self.target_model.ormar_config.table @property def column(self) -> sqlalchemy.Column: """Shortcut to sqlalchemy column of filtered target model""" aliased_name = self.target_model.get_column_alias(self.field_name) - return self.target_model.Meta.table.columns[aliased_name] + return self.target_model.ormar_config.table.columns[aliased_name] def update_select_related(self, select_related: List[str]) -> List[str]: """ diff --git a/ormar/queryset/actions/select_action.py b/ormar/queryset/actions/select_action.py index 92e5991..141b095 100644 --- a/ormar/queryset/actions/select_action.py +++ b/ormar/queryset/actions/select_action.py @@ -1,5 +1,5 @@ import decimal -from typing import Any, Callable, TYPE_CHECKING, Type +from typing import TYPE_CHECKING, Any, Callable, Optional, Type import sqlalchemy @@ -20,7 +20,7 @@ class SelectAction(QueryAction): """ def __init__( - self, select_str: str, model_cls: Type["Model"], alias: str = None + self, select_str: str, model_cls: Type["Model"], alias: Optional[str] = None ) -> None: super().__init__(query_str=select_str, model_cls=model_cls) if alias: # pragma: no cover @@ -36,7 +36,7 @@ class SelectAction(QueryAction): return self.get_target_field_type() in [int, float, decimal.Decimal] def get_target_field_type(self) -> Any: - return self.target_model.Meta.model_fields[self.field_name].__type__ + return self.target_model.ormar_config.model_fields[self.field_name].__type__ def get_text_clause(self) -> sqlalchemy.sql.expression.TextClause: alias = f"{self.table_prefix}_" if self.table_prefix else "" diff --git a/ormar/queryset/clause.py b/ormar/queryset/clause.py index 2a86d45..0000ac1 100644 --- a/ormar/queryset/clause.py +++ b/ormar/queryset/clause.py @@ -1,7 +1,7 @@ import itertools from dataclasses import dataclass from enum import Enum -from typing import Any, Generator, List, TYPE_CHECKING, Tuple, Type +from typing import TYPE_CHECKING, Any, Generator, List, Optional, Tuple, Type import sqlalchemy @@ -52,8 +52,8 @@ class FilterGroup: def resolve( self, model_cls: Type["Model"], - select_related: List = None, - filter_clauses: List = None, + select_related: Optional[List] = None, + filter_clauses: Optional[List] = None, ) -> Tuple[List[FilterAction], List[str]]: """ Resolves the FilterGroups actions to use proper target model, replace @@ -180,12 +180,11 @@ class QueryClause: def __init__( self, model_cls: Type["Model"], filter_clauses: List, select_related: List ) -> None: - self._select_related = select_related[:] self.filter_clauses = filter_clauses[:] self.model_cls = model_cls - self.table = self.model_cls.Meta.table + self.table = self.model_cls.ormar_config.table def prepare_filter( # noqa: A003 self, _own_only: bool = False, **kwargs: Any @@ -203,7 +202,9 @@ class QueryClause: :rtype: Tuple[List[sqlalchemy.sql.elements.TextClause], List[str]] """ if kwargs.get("pk"): - pk_name = self.model_cls.get_column_alias(self.model_cls.Meta.pkname) + pk_name = self.model_cls.get_column_alias( + self.model_cls.ormar_config.pkname + ) kwargs[pk_name] = kwargs.pop("pk") filter_clauses, select_related = self._populate_filter_clauses( @@ -262,7 +263,7 @@ class QueryClause: """ prefixes = self._parse_related_prefixes(select_related=select_related) - manager = self.model_cls.Meta.alias_manager + manager = self.model_cls.ormar_config.alias_manager filtered_prefixes = sorted(prefixes, key=lambda x: x.table_prefix) grouped = itertools.groupby(filtered_prefixes, key=lambda x: x.table_prefix) for _, group in grouped: @@ -320,7 +321,7 @@ class QueryClause: :param action: action to switch prefix in :type action: ormar.queryset.actions.filter_action.FilterAction """ - manager = self.model_cls.Meta.alias_manager + manager = self.model_cls.ormar_config.alias_manager new_alias = manager.resolve_relation_alias(self.model_cls, action.related_str) if "__" in action.related_str and new_alias: action.table_prefix = new_alias diff --git a/ormar/queryset/field_accessor.py b/ormar/queryset/field_accessor.py index b274837..3b66905 100644 --- a/ormar/queryset/field_accessor.py +++ b/ormar/queryset/field_accessor.py @@ -1,4 +1,4 @@ -from typing import Any, TYPE_CHECKING, Type, cast +from typing import TYPE_CHECKING, Any, Optional, Type, cast from ormar.queryset.actions import OrderAction from ormar.queryset.actions.filter_action import METHODS_TO_OPERATORS @@ -17,8 +17,8 @@ class FieldAccessor: def __init__( self, source_model: Type["Model"], - field: "BaseField" = None, - model: Type["Model"] = None, + field: Optional["BaseField"] = None, + model: Optional[Type["Model"]] = None, access_chain: str = "", ) -> None: self._source_model = source_model @@ -26,15 +26,6 @@ class FieldAccessor: self._model = model self._access_chain = access_chain - def __bool__(self) -> bool: - """ - Hack to avoid pydantic name check from parent model, returns false - - :return: False - :rtype: bool - """ - return False - def __getattr__(self, item: str) -> Any: """ Accessor return new accessor for each field and nested models. @@ -53,9 +44,10 @@ class FieldAccessor: if ( object.__getattribute__(self, "_model") - and item in object.__getattribute__(self, "_model").Meta.model_fields + and item + in object.__getattribute__(self, "_model").ormar_config.model_fields ): - field = cast("Model", self._model).Meta.model_fields[item] + field = cast("Model", self._model).ormar_config.model_fields[item] if field.is_relation: return FieldAccessor( source_model=self._source_model, diff --git a/ormar/queryset/join.py b/ormar/queryset/join.py index 29d0614..9d7e4d1 100644 --- a/ormar/queryset/join.py +++ b/ormar/queryset/join.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, TYPE_CHECKING, Tuple, Type, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, cast import sqlalchemy from sqlalchemy import text @@ -8,9 +8,9 @@ from ormar.exceptions import ModelDefinitionError, RelationshipInstanceError from ormar.relations import AliasManager if TYPE_CHECKING: # pragma no cover - from ormar import Model, ManyToManyField - from ormar.queryset import OrderAction + from ormar import ManyToManyField, Model from ormar.models.excludable import ExcludableItems + from ormar.queryset import OrderAction class SqlJoin: @@ -27,8 +27,8 @@ class SqlJoin: relation_str: str, related_models: Any = None, own_alias: str = "", - source_model: Type["Model"] = None, - already_sorted: Dict = None, + source_model: Optional[Type["Model"]] = None, + already_sorted: Optional[Dict] = None, ) -> None: self.relation_name = relation_name self.related_models = related_models or [] @@ -43,7 +43,9 @@ class SqlJoin: self.main_model = main_model self.own_alias = own_alias self.used_aliases = used_aliases - self.target_field = self.main_model.Meta.model_fields[self.relation_name] + self.target_field = self.main_model.ormar_config.model_fields[ + self.relation_name + ] self._next_model: Optional[Type["Model"]] = None self._next_alias: Optional[str] = None @@ -76,12 +78,12 @@ class SqlJoin: @property def alias_manager(self) -> AliasManager: """ - Shortcut for ormar's model AliasManager stored on Meta. + Shortcut for ormar's model AliasManager stored on OrmarConfig. - :return: alias manager from model's Meta + :return: alias manager from model's OrmarConfig :rtype: AliasManager """ - return self.main_model.Meta.alias_manager + return self.main_model.ormar_config.alias_manager @property def to_table(self) -> sqlalchemy.Table: @@ -90,9 +92,16 @@ class SqlJoin: :return: name of the target table :rtype: str """ - return self.next_model.Meta.table + return self.next_model.ormar_config.table - def _on_clause(self, previous_alias: str, from_table_name:str, from_column_name: str, to_table_name: str, to_column_name: str) -> text: + def _on_clause( + self, + previous_alias: str, + from_table_name: str, + from_column_name: str, + to_table_name: str, + to_column_name: str, + ) -> text: """ Receives aliases and names of both ends of the join and combines them into one text clause used in joins. @@ -110,13 +119,17 @@ class SqlJoin: :return: clause combining all strings :rtype: sqlalchemy.text """ - dialect = self.main_model.Meta.database._backend._dialect + dialect = self.main_model.ormar_config.database._backend._dialect quoter = dialect.identifier_preparer.quote - left_part = f"{quoter(f'{self.next_alias}_{to_table_name}')}.{quoter(to_column_name)}" + left_part = ( + f"{quoter(f'{self.next_alias}_{to_table_name}')}.{quoter(to_column_name)}" + ) if not previous_alias: right_part = f"{quoter(from_table_name)}.{quoter(from_column_name)}" else: - right_part = f"{quoter(f'{previous_alias}_{from_table_name}')}.{from_column_name}" + right_part = ( + f"{quoter(f'{previous_alias}_{from_table_name}')}.{from_column_name}" + ) return text(f"{left_part}={right_part}") @@ -235,7 +248,9 @@ class SqlJoin: self.relation_name = new_part self.own_alias = self.next_alias - self.target_field = self.next_model.Meta.model_fields[self.relation_name] + self.target_field = self.next_model.ormar_config.model_fields[ + self.relation_name + ] def _process_m2m_related_name_change(self, reverse: bool = False) -> str: """ @@ -281,7 +296,7 @@ class SqlJoin: on_clause = self._on_clause( previous_alias=self.own_alias, - from_table_name=self.target_field.owner.Meta.tablename, + from_table_name=self.target_field.owner.ormar_config.tablename, from_column_name=from_key, to_table_name=self.to_table.name, to_column_name=to_key, @@ -309,7 +324,7 @@ class SqlJoin: self.used_aliases.append(self.next_alias) def _set_default_primary_key_order_by(self) -> None: - for order_by in self.next_model.Meta.orders_by: + for order_by in self.next_model.ormar_config.orders_by: clause = ormar.OrderAction( order_str=order_by, model_cls=self.next_model, alias=self.next_alias ) @@ -399,16 +414,20 @@ class SqlJoin: """ if self.target_field.is_multi: to_key = self._process_m2m_related_name_change(reverse=True) - from_key = self.main_model.get_column_alias(self.main_model.Meta.pkname) + from_key = self.main_model.get_column_alias( + self.main_model.ormar_config.pkname + ) elif self.target_field.virtual: to_field = self.target_field.get_related_name() to_key = self.target_field.to.get_column_alias(to_field) - from_key = self.main_model.get_column_alias(self.main_model.Meta.pkname) + from_key = self.main_model.get_column_alias( + self.main_model.ormar_config.pkname + ) else: to_key = self.target_field.to.get_column_alias( - self.target_field.to.Meta.pkname + self.target_field.to.ormar_config.pkname ) from_key = self.main_model.get_column_alias(self.relation_name) diff --git a/ormar/queryset/queries/filter_query.py b/ormar/queryset/queries/filter_query.py index cb9b880..e6b7e82 100644 --- a/ormar/queryset/queries/filter_query.py +++ b/ormar/queryset/queries/filter_query.py @@ -1,6 +1,7 @@ from typing import List import sqlalchemy + from ormar.queryset.actions.filter_action import FilterAction diff --git a/ormar/queryset/queries/prefetch_query.py b/ormar/queryset/queries/prefetch_query.py index d30b9d7..66c2e47 100644 --- a/ormar/queryset/queries/prefetch_query.py +++ b/ormar/queryset/queries/prefetch_query.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Sequence, Set, TYPE_CHECKING, Tuple, Type, cast +from typing import TYPE_CHECKING, Dict, List, Sequence, Set, Tuple, Type, cast import ormar from ormar.queryset.clause import QueryClause @@ -7,9 +7,9 @@ from ormar.queryset.utils import extract_models_to_dict_of_lists, translate_list if TYPE_CHECKING: # pragma: no cover from ormar import Model - from ormar.fields import ForeignKeyField, BaseField - from ormar.queryset import OrderAction + from ormar.fields import BaseField, ForeignKeyField from ormar.models.excludable import ExcludableItems + from ormar.queryset import OrderAction def sort_models(models: List["Model"], orders_by: Dict) -> List["Model"]: @@ -96,9 +96,8 @@ class PrefetchQuery: select_related: List, orders_by: List["OrderAction"], ) -> None: - self.model = model_cls - self.database = self.model.Meta.database + self.database = self.model.ormar_config.database self._prefetch_related = prefetch_related self._select_related = select_related self.excludable = excludable @@ -279,7 +278,7 @@ class PrefetchQuery: ) for related in related_to_extract: - target_field = model.Meta.model_fields[related] + target_field = model.ormar_config.model_fields[related] target_field = cast("ForeignKeyField", target_field) target_model = target_field.to.get_name() model_id = model.get_relation_model_id(target_field=target_field) @@ -381,7 +380,7 @@ class PrefetchQuery: :return: None :rtype: None """ - target_field = target_model.Meta.model_fields[related] + target_field = target_model.ormar_config.model_fields[related] target_field = cast("ForeignKeyField", target_field) reverse = False if target_field.virtual or target_field.is_multi: @@ -473,14 +472,18 @@ class PrefetchQuery: select_related = [] query_target = target_model table_prefix = "" - exclude_prefix = target_field.to.Meta.alias_manager.resolve_relation_alias( - from_model=target_field.owner, relation_name=target_field.name + exclude_prefix = ( + target_field.to.ormar_config.alias_manager.resolve_relation_alias( + from_model=target_field.owner, relation_name=target_field.name + ) ) if target_field.is_multi: query_target = target_field.through select_related = [target_name] - table_prefix = target_field.to.Meta.alias_manager.resolve_relation_alias( - from_model=query_target, relation_name=target_name + table_prefix = ( + target_field.to.ormar_config.alias_manager.resolve_relation_alias( + from_model=query_target, relation_name=target_name + ) ) exclude_prefix = table_prefix self.already_extracted.setdefault(target_name, {})["prefix"] = table_prefix diff --git a/ormar/queryset/queries/query.py b/ormar/queryset/queries/query.py index de3821c..22c465e 100644 --- a/ormar/queryset/queries/query.py +++ b/ormar/queryset/queries/query.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Type, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type, Union import sqlalchemy from sqlalchemy import Table, text @@ -6,14 +6,14 @@ from sqlalchemy.sql import Join import ormar # noqa I100 from ormar.models.helpers.models import group_related_list -from ormar.queryset.queries import FilterQuery, LimitQuery, OffsetQuery, OrderQuery from ormar.queryset.actions.filter_action import FilterAction from ormar.queryset.join import SqlJoin +from ormar.queryset.queries import FilterQuery, LimitQuery, OffsetQuery, OrderQuery if TYPE_CHECKING: # pragma no cover from ormar import Model - from ormar.queryset import OrderAction from ormar.models.excludable import ExcludableItems + from ormar.queryset import OrderAction class Query: @@ -37,7 +37,7 @@ class Query: self.excludable = excludable self.model_cls = model_cls - self.table = self.model_cls.Meta.table + self.table = self.model_cls.ormar_config.table self.used_aliases: List[str] = [] @@ -75,10 +75,10 @@ class Query: def _apply_default_model_sorting(self) -> None: """ - Applies orders_by from model Meta class (if provided), if it was not provided - it was filled by metaclass so it's always there and falls back to pk column + Applies orders_by from model OrmarConfig (if provided), if it was not provided + it was filled by metaclass, so it's always there and falls back to pk column """ - for order_by in self.model_cls.Meta.orders_by: + for order_by in self.model_cls.ormar_config.orders_by: clause = ormar.OrderAction(order_str=order_by, model_cls=self.model_cls) self.sorted_orders[clause] = clause.get_text_clause() @@ -97,7 +97,7 @@ class Query: and self._select_related ) - def build_select_expression(self) -> Tuple[sqlalchemy.sql.select, List[str]]: + def build_select_expression(self) -> sqlalchemy.sql.select: """ Main entry point from outside (after proper initialization). @@ -113,7 +113,7 @@ class Query: self_related_fields = self.model_cls.own_table_columns( model=self.model_cls, excludable=self.excludable, use_alias=True ) - self.columns = self.model_cls.Meta.alias_manager.prefixed_columns( + self.columns = self.model_cls.ormar_config.alias_manager.prefixed_columns( "", self.table, self_related_fields ) self.apply_order_bys_for_primary_model() @@ -179,7 +179,7 @@ class Query: The condition is added to filters to filter out desired number of main model primary key values. Whole query is used to determine the values. """ - pk_alias = self.model_cls.get_column_alias(self.model_cls.Meta.pkname) + pk_alias = self.model_cls.get_column_alias(self.model_cls.ormar_config.pkname) pk_aliased_name = f"{self.table.name}.{pk_alias}" qry_text = sqlalchemy.text(f"{pk_aliased_name}") maxes = {} diff --git a/ormar/queryset/queryset.py b/ormar/queryset/queryset.py index c958729..0da47f5 100644 --- a/ormar/queryset/queryset.py +++ b/ormar/queryset/queryset.py @@ -1,19 +1,19 @@ import asyncio from typing import ( + TYPE_CHECKING, Any, + AsyncGenerator, Dict, Generic, List, Optional, Sequence, Set, - TYPE_CHECKING, Tuple, Type, TypeVar, Union, cast, - AsyncGenerator, ) import databases @@ -46,8 +46,8 @@ from ormar.queryset.reverse_alias_resolver import ReverseAliasResolver if TYPE_CHECKING: # pragma no cover from ormar import Model from ormar.models import T - from ormar.models.metaclass import ModelMeta from ormar.models.excludable import ExcludableItems + from ormar.models.ormar_config import OrmarConfig else: T = TypeVar("T", bound="Model") @@ -60,14 +60,14 @@ class QuerySet(Generic[T]): def __init__( # noqa CFQ002 self, model_cls: Optional[Type["T"]] = None, - filter_clauses: List = None, - exclude_clauses: List = None, - select_related: List = None, - limit_count: int = None, - offset: int = None, - excludable: "ExcludableItems" = None, - order_bys: List = None, - prefetch_related: List = None, + filter_clauses: Optional[List] = None, + exclude_clauses: Optional[List] = None, + select_related: Optional[List] = None, + limit_count: Optional[int] = None, + offset: Optional[int] = None, + excludable: Optional["ExcludableItems"] = None, + order_bys: Optional[List] = None, + prefetch_related: Optional[List] = None, limit_raw_sql: bool = False, proxy_source_model: Optional[Type["Model"]] = None, ) -> None: @@ -84,16 +84,16 @@ class QuerySet(Generic[T]): self.limit_sql_raw = limit_raw_sql @property - def model_meta(self) -> "ModelMeta": + def model_config(self) -> "OrmarConfig": """ - Shortcut to model class Meta set on QuerySet model. + Shortcut to model class OrmarConfig set on QuerySet model. - :return: Meta class of the model - :rtype: model Meta class + :return: OrmarConfig of the model + :rtype: model's OrmarConfig """ if not self.model_cls: # pragma nocover raise ValueError("Model class of QuerySet is not initialized") - return self.model_cls.Meta + return self.model_cls.ormar_config @property def model(self) -> Type["T"]: @@ -109,15 +109,15 @@ class QuerySet(Generic[T]): def rebuild_self( # noqa: CFQ002 self, - filter_clauses: List = None, - exclude_clauses: List = None, - select_related: List = None, - limit_count: int = None, - offset: int = None, - excludable: "ExcludableItems" = None, - order_bys: List = None, - prefetch_related: List = None, - limit_raw_sql: bool = None, + filter_clauses: Optional[List] = None, + exclude_clauses: Optional[List] = None, + select_related: Optional[List] = None, + limit_count: Optional[int] = None, + offset: Optional[int] = None, + excludable: Optional["ExcludableItems"] = None, + order_bys: Optional[List] = None, + prefetch_related: Optional[List] = None, + limit_raw_sql: Optional[bool] = None, proxy_source_model: Optional[Type["Model"]] = None, ) -> "QuerySet": """ @@ -247,25 +247,28 @@ class QuerySet(Generic[T]): @property def database(self) -> databases.Database: """ - Shortcut to models database from Meta class. + Shortcut to models database from OrmarConfig class. :return: database :rtype: databases.Database """ - return self.model_meta.database + return self.model_config.database @property def table(self) -> sqlalchemy.Table: """ - Shortcut to models table from Meta class. + Shortcut to models table from OrmarConfig. :return: database table :rtype: sqlalchemy.Table """ - return self.model_meta.table + return self.model_config.table def build_select_expression( - self, limit: int = None, offset: int = None, order_bys: List = None + self, + limit: Optional[int] = None, + offset: Optional[int] = None, + order_bys: Optional[List] = None, ) -> sqlalchemy.sql.select: """ Constructs the actual database query used in the QuerySet. @@ -575,9 +578,11 @@ class QuerySet(Generic[T]): columns = [columns] orders_by = [ - OrderAction(order_str=x, model_cls=self.model_cls) # type: ignore - if not isinstance(x, OrderAction) - else x + ( + OrderAction(order_str=x, model_cls=self.model_cls) # type: ignore + if not isinstance(x, OrderAction) + else x + ) for x in columns ] @@ -586,7 +591,7 @@ class QuerySet(Generic[T]): async def values( self, - fields: Union[List, str, Set, Dict] = None, + fields: Union[List, str, Set, Dict, None] = None, exclude_through: bool = False, _as_dict: bool = True, _flatten: bool = False, @@ -641,7 +646,7 @@ class QuerySet(Generic[T]): async def values_list( self, - fields: Union[List, str, Set, Dict] = None, + fields: Union[List, str, Set, Dict, None] = None, flatten: bool = False, exclude_through: bool = False, ) -> List: @@ -702,7 +707,7 @@ class QuerySet(Generic[T]): expr = self.build_select_expression().alias("subquery_for_count") expr = sqlalchemy.func.count().select().select_from(expr) if distinct: - pk_column_name = self.model.get_column_alias(self.model_meta.pkname) + pk_column_name = self.model.get_column_alias(self.model_config.pkname) expr_distinct = expr.group_by(pk_column_name).alias("subquery_for_group") expr = sqlalchemy.func.count().select().select_from(expr_distinct) return await self.database.fetch_val(expr) @@ -796,7 +801,7 @@ class QuerySet(Generic[T]): self.model.extract_related_names() ) updates = {k: v for k, v in kwargs.items() if k in self_fields} - updates = self.model.validate_choices(updates) + updates = self.model.validate_enums(updates) updates = self.model.translate_columns_to_aliases(updates) expr = FilterQuery(filter_clauses=self.filter_clauses).apply( @@ -855,7 +860,9 @@ class QuerySet(Generic[T]): query_offset = (page - 1) * page_size return self.rebuild_self(limit_count=limit_count, offset=query_offset) - def limit(self, limit_count: int, limit_raw_sql: bool = None) -> "QuerySet[T]": + def limit( + self, limit_count: int, limit_raw_sql: Optional[bool] = None + ) -> "QuerySet[T]": """ You can limit the results to desired number of parent models. @@ -872,7 +879,9 @@ class QuerySet(Generic[T]): limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql return self.rebuild_self(limit_count=limit_count, limit_raw_sql=limit_raw_sql) - def offset(self, offset: int, limit_raw_sql: bool = None) -> "QuerySet[T]": + def offset( + self, offset: int, limit_raw_sql: Optional[bool] = None + ) -> "QuerySet[T]": """ You can also offset the results by desired number of main models. @@ -908,7 +917,7 @@ class QuerySet(Generic[T]): order_bys=( [ OrderAction( - order_str=f"{self.model.Meta.pkname}", + order_str=f"{self.model.ormar_config.pkname}", model_cls=self.model_cls, # type: ignore ) ] @@ -970,7 +979,7 @@ class QuerySet(Generic[T]): order_bys=( [ OrderAction( - order_str=f"-{self.model.Meta.pkname}", + order_str=f"-{self.model.ormar_config.pkname}", model_cls=self.model_cls, # type: ignore ) ] @@ -1027,7 +1036,7 @@ class QuerySet(Generic[T]): :return: updated or created model :rtype: Model """ - pk_name = self.model_meta.pkname + pk_name = self.model_config.pkname if "pk" in kwargs: kwargs[pk_name] = kwargs.pop("pk") if pk_name not in kwargs or kwargs.get(pk_name) is None: @@ -1093,7 +1102,7 @@ class QuerySet(Generic[T]): rows: list = [] last_primary_key = None - pk_alias = self.model.get_column_alias(self.model_meta.pkname) + pk_alias = self.model.get_column_alias(self.model_config.pkname) async for row in self.database.iterate(query=expr): current_primary_key = row[pk_alias] @@ -1144,7 +1153,7 @@ class QuerySet(Generic[T]): ready_objects = [] for obj in objects: - ready_objects.append(obj.prepare_model_to_save(obj.dict())) + ready_objects.append(obj.prepare_model_to_save(obj.model_dump())) await asyncio.sleep(0) # Allow context switching to prevent blocking # don't use execute_many, as in databases it's executed in a loop @@ -1156,7 +1165,7 @@ class QuerySet(Generic[T]): obj.set_save_status(True) async def bulk_update( # noqa: CCR001 - self, objects: List["T"], columns: List[str] = None + self, objects: List["T"], columns: Optional[List[str]] = None ) -> None: """ Performs bulk update in one database session to speed up the process. @@ -1179,7 +1188,7 @@ class QuerySet(Generic[T]): raise ModelListEmptyError("Bulk update objects are empty!") ready_objects = [] - pk_name = self.model_meta.pkname + pk_name = self.model_config.pkname if not columns: columns = list( self.model.extract_db_own_fields().union( @@ -1193,7 +1202,7 @@ class QuerySet(Generic[T]): columns = [self.model.get_column_alias(k) for k in columns] for obj in objects: - new_kwargs = obj.dict() + new_kwargs = obj.model_dump() if new_kwargs.get(pk_name) is None: raise ModelPersistenceError( "You cannot update unsaved objects. " @@ -1205,9 +1214,9 @@ class QuerySet(Generic[T]): ) await asyncio.sleep(0) - pk_column = self.model_meta.table.c.get(self.model.get_column_alias(pk_name)) + pk_column = self.model_config.table.c.get(self.model.get_column_alias(pk_name)) pk_column_name = self.model.get_column_alias(pk_name) - table_columns = [c.name for c in self.model_meta.table.c] + table_columns = [c.name for c in self.model_config.table.c] expr = self.table.update().where( pk_column == bindparam("new_" + pk_column_name) ) @@ -1226,6 +1235,8 @@ class QuerySet(Generic[T]): for obj in objects: obj.set_save_status(True) - await cast(Type["Model"], self.model_cls).Meta.signals.post_bulk_update.send( + await cast( + Type["Model"], self.model_cls + ).ormar_config.signals.post_bulk_update.send( sender=self.model_cls, instances=objects # type: ignore ) diff --git a/ormar/queryset/reverse_alias_resolver.py b/ormar/queryset/reverse_alias_resolver.py index b9ed1c8..7cc4f8d 100644 --- a/ormar/queryset/reverse_alias_resolver.py +++ b/ormar/queryset/reverse_alias_resolver.py @@ -1,4 +1,4 @@ -from typing import Dict, List, TYPE_CHECKING, Type, cast +from typing import TYPE_CHECKING, Dict, List, Type, cast if TYPE_CHECKING: # pragma: no cover from ormar import ForeignKeyField, Model @@ -20,7 +20,9 @@ class ReverseAliasResolver: ) -> None: self.select_related = select_related self.model_cls = model_cls - self.reversed_aliases = self.model_cls.Meta.alias_manager.reversed_aliases + self.reversed_aliases = ( + self.model_cls.ormar_config.alias_manager.reversed_aliases + ) self.excludable = excludable self.exclude_through = exclude_through @@ -176,7 +178,7 @@ class ReverseAliasResolver: for relation in related_split: previous_related_str = f"{related_str}__" if related_str else "" new_related_str = previous_related_str + relation - field = model_cls.Meta.model_fields[relation] + field = model_cls.ormar_config.model_fields[relation] field = cast("ForeignKeyField", field) prefix_name = self._handle_through_fields_and_prefix( model_cls=model_cls, diff --git a/ormar/queryset/utils.py b/ormar/queryset/utils.py index 4ebe07c..97122d8 100644 --- a/ormar/queryset/utils.py +++ b/ormar/queryset/utils.py @@ -1,20 +1,20 @@ import collections.abc import copy from typing import ( + TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Set, - TYPE_CHECKING, Tuple, Type, Union, ) if TYPE_CHECKING: # pragma no cover - from ormar import Model, BaseField + from ormar import BaseField, Model def check_node_not_dict_or_not_last_node( @@ -216,7 +216,7 @@ def extract_nested_models( # noqa: CCR001 child = getattr(model, related) if not child: continue - target_model = model_type.Meta.model_fields[related].to + target_model = model_type.ormar_config.model_fields[related].to if isinstance(child, list): extracted.setdefault(target_model.get_name(), []).extend(child) if select_dict[related] is not Ellipsis: @@ -236,7 +236,7 @@ def extract_models_to_dict_of_lists( model_type: Type["Model"], models: Sequence["Model"], select_dict: Dict, - extracted: Dict = None, + extracted: Optional[Dict] = None, ) -> Dict: """ Receives a list of models and extracts all of the children and their children @@ -279,9 +279,9 @@ def get_relationship_alias_model_and_str( target_model = source_model previous_model = target_model previous_models = [target_model] - manager = target_model.Meta.alias_manager + manager = target_model.ormar_config.alias_manager for relation in related_parts[:]: - related_field = target_model.Meta.model_fields[relation] + related_field = target_model.ormar_config.model_fields[relation] if related_field.is_through: (previous_model, relation, is_through) = _process_through_field( @@ -331,7 +331,7 @@ def _process_through_field( """ is_through = True related_parts.remove(relation) - through_field = related_field.owner.Meta.model_fields[ + through_field = related_field.owner.ormar_config.model_fields[ related_field.related_name or "" ] if len(previous_models) > 1 and previous_models[-2] == through_field.to: diff --git a/ormar/relations/__init__.py b/ormar/relations/__init__.py index 4e4529b..15cc948 100644 --- a/ormar/relations/__init__.py +++ b/ormar/relations/__init__.py @@ -2,6 +2,7 @@ Package handles relations on models, returning related models on calls and exposing QuerySetProxy for m2m and reverse relations. """ + from ormar.relations.alias_manager import AliasManager from ormar.relations.relation import Relation, RelationType from ormar.relations.relation_manager import RelationsManager diff --git a/ormar/relations/alias_manager.py b/ormar/relations/alias_manager.py index c8bd1a6..0355f34 100644 --- a/ormar/relations/alias_manager.py +++ b/ormar/relations/alias_manager.py @@ -1,15 +1,15 @@ import string import uuid from random import choices -from typing import Any, Dict, List, TYPE_CHECKING, Type, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union import sqlalchemy from sqlalchemy import text if TYPE_CHECKING: # pragma: no cover from ormar import Model - from ormar.models import ModelRow from ormar.fields import ForeignKeyField + from ormar.models import ModelRow def get_table_alias() -> str: @@ -59,7 +59,7 @@ class AliasManager: @staticmethod def prefixed_columns( - alias: str, table: sqlalchemy.Table, fields: List = None + alias: str, table: sqlalchemy.Table, fields: Optional[List] = None ) -> List[text]: """ Creates a list of aliases sqlalchemy text clauses from @@ -106,7 +106,10 @@ class AliasManager: return self._prefixed_tables.setdefault(key, table.alias(full_alias)) def add_relation_type( - self, source_model: Type["Model"], relation_name: str, reverse_name: str = None + self, + source_model: Type["Model"], + relation_name: str, + reverse_name: Optional[str] = None, ) -> None: """ Registers the relations defined in ormar models. @@ -134,7 +137,7 @@ class AliasManager: if parent_key not in self._aliases_new: self.add_alias(parent_key) - to_field = source_model.Meta.model_fields[relation_name] + to_field = source_model.ormar_config.model_fields[relation_name] child_model = to_field.to child_key = f"{child_model.get_name()}_{reverse_name}" if child_key not in self._aliases_new: diff --git a/ormar/relations/querysetproxy.py b/ormar/relations/querysetproxy.py index e61f862..1a767aa 100644 --- a/ormar/relations/querysetproxy.py +++ b/ormar/relations/querysetproxy.py @@ -1,6 +1,7 @@ -from _weakref import CallableProxyType from typing import ( # noqa: I100, I201 + TYPE_CHECKING, Any, + AsyncGenerator, Dict, Generic, List, @@ -8,23 +9,23 @@ from typing import ( # noqa: I100, I201 Optional, Sequence, Set, - TYPE_CHECKING, Tuple, Type, TypeVar, Union, cast, - AsyncGenerator, ) +from _weakref import CallableProxyType + import ormar # noqa: I100, I202 from ormar.exceptions import ModelPersistenceError, NoMatch, QueryDefinitionError if TYPE_CHECKING: # pragma no cover - from ormar.relations import Relation + from ormar import OrderAction, RelationType from ormar.models import Model, T from ormar.queryset import QuerySet - from ormar import OrderAction, RelationType + from ormar.relations import Relation else: T = TypeVar("T", bound="Model") @@ -43,17 +44,17 @@ class QuerysetProxy(Generic[T]): relation: "Relation", to: Type["T"], type_: "RelationType", - qryset: "QuerySet[T]" = None, + qryset: Optional["QuerySet[T]"] = None, ) -> None: self.relation: "Relation" = relation self._queryset: Optional["QuerySet[T]"] = qryset self.type_: "RelationType" = type_ self._owner: Union[CallableProxyType, "Model"] = self.relation.manager.owner - self.related_field_name = self._owner.Meta.model_fields[ + self.related_field_name = self._owner.ormar_config.model_fields[ self.relation.field_name ].get_related_name() self.to: Type[T] = to - self.related_field = to.Meta.model_fields[self.related_field_name] + self.related_field = to.ormar_config.model_fields[self.related_field_name] self.owner_pk_value = self._owner.pk self.through_model_name = ( self.related_field.through.get_name() @@ -286,7 +287,9 @@ class QuerysetProxy(Generic[T]): return await queryset.delete(**kwargs) # type: ignore async def values( - self, fields: Union[List, str, Set, Dict] = None, exclude_through: bool = False + self, + fields: Union[List, str, Set, Dict, None] = None, + exclude_through: bool = False, ) -> List: """ Return a list of dictionaries with column values in order of the fields @@ -308,7 +311,7 @@ class QuerysetProxy(Generic[T]): async def values_list( self, - fields: Union[List, str, Set, Dict] = None, + fields: Union[List, str, Set, Dict, None] = None, flatten: bool = False, exclude_through: bool = False, ) -> List: @@ -551,7 +554,7 @@ class QuerysetProxy(Generic[T]): :return: updated or created model :rtype: Model """ - pk_name = self.queryset.model_meta.pkname + pk_name = self.queryset.model_config.pkname if "pk" in kwargs: kwargs[pk_name] = kwargs.pop("pk") if pk_name not in kwargs or kwargs.get(pk_name) is None: diff --git a/ormar/relations/relation.py b/ormar/relations/relation.py index d73d627..7c2724d 100644 --- a/ormar/relations/relation.py +++ b/ormar/relations/relation.py @@ -1,10 +1,10 @@ from enum import Enum from typing import ( + TYPE_CHECKING, Generic, List, Optional, Set, - TYPE_CHECKING, Type, TypeVar, Union, @@ -16,8 +16,8 @@ from ormar.exceptions import RelationshipInstanceError # noqa I100 from ormar.relations.relation_proxy import RelationProxy if TYPE_CHECKING: # pragma no cover - from ormar.relations import RelationsManager from ormar.models import Model, NewBaseModel, T + from ormar.relations import RelationsManager else: T = TypeVar("T", bound="Model") @@ -48,7 +48,7 @@ class Relation(Generic[T]): type_: RelationType, field_name: str, to: Type["T"], - through: Type["Model"] = None, + through: Optional[Type["Model"]] = None, ) -> None: """ Initialize the Relation and keep the related models either as instances of @@ -162,9 +162,17 @@ class Relation(Generic[T]): rel = rel or [] if not isinstance(rel, list): rel = [rel] - rel.append(child) + self._populate_owner_side_dict(rel=rel, child=child) self._owner.__dict__[relation_name] = rel + def _populate_owner_side_dict(self, rel: List["Model"], child: "Model") -> None: + try: + if child not in rel: + rel.append(child) + except ReferenceError: + rel.clear() + rel.append(child) + def remove(self, child: Union["NewBaseModel", Type["NewBaseModel"]]) -> None: """ Removes child Model from relation, either sets None as related model or removes diff --git a/ormar/relations/relation_manager.py b/ormar/relations/relation_manager.py index e188bda..368661c 100644 --- a/ormar/relations/relation_manager.py +++ b/ormar/relations/relation_manager.py @@ -1,12 +1,12 @@ -from typing import Dict, List, Optional, Sequence, TYPE_CHECKING, Type, Union +from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Type, Union from weakref import proxy from ormar.relations.relation import Relation, RelationType from ormar.relations.utils import get_relations_sides_and_names if TYPE_CHECKING: # pragma no cover - from ormar.models import NewBaseModel, Model - from ormar.fields import ForeignKeyField, BaseField + from ormar.fields import BaseField, ForeignKeyField + from ormar.models import Model, NewBaseModel class RelationsManager: @@ -16,7 +16,7 @@ class RelationsManager: def __init__( self, - related_fields: List["ForeignKeyField"] = None, + related_fields: Optional[List["ForeignKeyField"]] = None, owner: Optional["Model"] = None, ) -> None: self.owner = proxy(owner) @@ -120,7 +120,7 @@ class RelationsManager: :param name: name of the relation :type name: str """ - relation_name = item.Meta.model_fields[name].get_related_name() + relation_name = item.ormar_config.model_fields[name].get_related_name() item._orm.remove(name, parent) parent._orm.remove(relation_name, item) diff --git a/ormar/relations/relation_proxy.py b/ormar/relations/relation_proxy.py index 0169308..7efaf41 100644 --- a/ormar/relations/relation_proxy.py +++ b/ormar/relations/relation_proxy.py @@ -1,14 +1,15 @@ from typing import ( + TYPE_CHECKING, Any, Dict, Generic, List, Optional, - TYPE_CHECKING, Set, Type, TypeVar, ) + from typing_extensions import SupportsIndex import ormar @@ -18,8 +19,8 @@ from ormar.relations.querysetproxy import QuerysetProxy if TYPE_CHECKING: # pragma no cover from ormar import Model, RelationType from ormar.models import T - from ormar.relations import Relation from ormar.queryset import QuerySet + from ormar.relations import Relation else: T = TypeVar("T", bound="Model") @@ -71,7 +72,7 @@ class RelationProxy(Generic[T], List[T]): """ if self._related_field_name: return self._related_field_name - owner_field = self._owner.Meta.model_fields[self.field_name] + owner_field = self._owner.ormar_config.model_fields[self.field_name] self._related_field_name = owner_field.get_related_name() return self._related_field_name @@ -245,7 +246,7 @@ class RelationProxy(Generic[T], List[T]): :rtype: QuerySet """ related_field_name = self.related_field_name - pkname = self._owner.get_column_alias(self._owner.Meta.pkname) + pkname = self._owner.get_column_alias(self._owner.ormar_config.pkname) self._check_if_model_saved() kwargs = {f"{related_field_name}__{pkname}": self._owner.pk} queryset = ( diff --git a/ormar/signals/__init__.py b/ormar/signals/__init__.py index 127fc2f..74a642c 100644 --- a/ormar/signals/__init__.py +++ b/ormar/signals/__init__.py @@ -1,7 +1,8 @@ """ -Signals and SignalEmitter that gathers the signals on models Meta. +Signals and SignalEmitter that gathers the signals on models OrmarConfig. Used to signal receivers functions about events, i.e. post_save, pre_delete etc. """ + from ormar.signals.signal import Signal, SignalEmitter __all__ = ["Signal", "SignalEmitter"] diff --git a/ormar/signals/signal.py b/ormar/signals/signal.py index 868b494..ee952d6 100644 --- a/ormar/signals/signal.py +++ b/ormar/signals/signal.py @@ -1,6 +1,6 @@ import asyncio import inspect -from typing import Any, Callable, Dict, TYPE_CHECKING, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Type, Union from ormar.exceptions import SignalDefinitionError diff --git a/ormar/warnings.py b/ormar/warnings.py new file mode 100644 index 0000000..318fd3c --- /dev/null +++ b/ormar/warnings.py @@ -0,0 +1,51 @@ +# Adopted from pydantic +from typing import Optional, Tuple + + +class OrmarDeprecationWarning(DeprecationWarning): + """A Pydantic specific deprecation warning. + + This warning is raised when using deprecated functionality in Ormar. + It provides information on when the deprecation was introduced and + the expected version in which the corresponding functionality will be removed. + + Attributes: + message: Description of the warning + since: Ormar version in what the deprecation was introduced + expected_removal: Ormar version in what the functionality will be removed + """ + + message: str + since: Tuple[int, int] + expected_removal: Tuple[int, int] + + def __init__( + self, + message: str, + *args: object, + since: Tuple[int, int], + expected_removal: Optional[Tuple[int, int]] = None, + ) -> None: # pragma: no cover + super().__init__(message, *args) + self.message = message.rstrip(".") + self.since = since + self.expected_removal = ( + expected_removal if expected_removal is not None else (since[0] + 1, 0) + ) + + def __str__(self) -> str: # pragma: no cover + message = ( + f"{self.message}. Deprecated in Ormar V{self.since[0]}.{self.since[1]}" + f" to be removed in V{self.expected_removal[0]}.{self.expected_removal[1]}." + ) + if self.since == (0, 20): + message += " See Ormar V0.20 Migration Guide at https://collerek.github.io/ormar/migration/" + return message + + +class OrmarDeprecatedSince020(OrmarDeprecationWarning): + """A specific `OrmarDeprecationWarning` subclass defining + functionality deprecated since Ormar 0.20.""" + + def __init__(self, message: str, *args: object) -> None: # pragma: no cover + super().__init__(message, *args, since=(0, 20), expected_removal=(0, 30)) diff --git a/poetry.lock b/poetry.lock index a74efc9..7752038 100644 --- a/poetry.lock +++ b/poetry.lock @@ -50,35 +50,47 @@ files = [ {file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"}, ] -[package.dependencies] -typing_extensions = {version = ">=4.0", markers = "python_version < \"3.8\""} - [package.extras] dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"] docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] [[package]] -name = "anyio" -version = "3.7.1" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "dev" +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] [package.dependencies] -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} + +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "asgi-lifespan" @@ -96,19 +108,20 @@ files = [ sniffio = "*" [[package]] -name = "astpretty" -version = "2.1.0" -description = "Pretty print the output of python stdlib `ast.parse`." +name = "astunparse" +version = "1.6.3" +description = "An AST unparser for Python" category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = "*" files = [ - {file = "astpretty-2.1.0-py2.py3-none-any.whl", hash = "sha256:f81f14b5636f7af81fadb1e3c09ca7702ce4615500d9cc6d6829befb2dec2e3c"}, - {file = "astpretty-2.1.0.tar.gz", hash = "sha256:8a801fcda604ec741f010bb36d7cbadc3ec8a182ea6fb83e20ab663463e75ff6"}, + {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, + {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, ] -[package.extras] -typed = ["typed-ast"] +[package.dependencies] +six = ">=1.6.1,<2.0" +wheel = ">=0.23.0,<1.0" [[package]] name = "async-timeout" @@ -122,9 +135,6 @@ files = [ {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] -[package.dependencies] -typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} - [[package]] name = "asyncpg" version = "0.28.0" @@ -175,111 +185,58 @@ files = [ {file = "asyncpg-0.28.0.tar.gz", hash = "sha256:7252cdc3acb2f52feaa3664280d3bcd78a46bd6c10bfd681acfffefa1120e278"}, ] -[package.dependencies] -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} - [package.extras] docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] test = ["flake8 (>=5.0,<6.0)", "uvloop (>=0.15.3)"] -[[package]] -name = "attrs" -version = "23.1.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, -] - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] - [[package]] name = "babel" -version = "2.13.1" +version = "2.14.0" description = "Internationalization utilities" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"}, - {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, ] [package.dependencies] pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} -setuptools = {version = "*", markers = "python_version >= \"3.12\""} [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] -[[package]] -name = "bandit" -version = "1.7.5" -description = "Security oriented static analyser for python code." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "bandit-1.7.5-py3-none-any.whl", hash = "sha256:75665181dc1e0096369112541a056c59d1c5f66f9bb74a8d686c3c362b83f549"}, - {file = "bandit-1.7.5.tar.gz", hash = "sha256:bdfc739baa03b880c2d15d0431b31c658ffc348e907fe197e54e0389dd59e11e"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} -GitPython = ">=1.0.1" -PyYAML = ">=5.3.1" -rich = "*" -stevedore = ">=1.20.0" - -[package.extras] -test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "tomli (>=1.1.0)"] -toml = ["tomli (>=1.1.0)"] -yaml = ["PyYAML"] - [[package]] name = "black" -version = "23.3.0" +version = "24.3.0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, + {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, + {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, + {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, + {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, + {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, + {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, + {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, + {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, + {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, + {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, + {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, + {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, + {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, + {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, + {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, + {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, + {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, + {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, + {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, + {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, + {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, + {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, ] [package.dependencies] @@ -289,111 +246,86 @@ packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] -[[package]] -name = "cached-property" -version = "1.5.2" -description = "A decorator for caching properties in classes." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, - {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, -] - [[package]] name = "certifi" -version = "2023.7.22" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] name = "cffi" -version = "1.15.1" +version = "1.16.0" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, ] [package.dependencies] @@ -401,14 +333,14 @@ pycparser = "*" [[package]] name = "cfgv" -version = "3.3.1" +version = "3.4.0" description = "Validate configuration and produce human readable error messages." category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8" files = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] @@ -525,7 +457,6 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "codecov" @@ -543,20 +474,6 @@ files = [ coverage = "*" requests = ">=2.7.9" -[[package]] -name = "cognitive-complexity" -version = "1.3.0" -description = "Library to calculate Python functions cognitive complexity via code" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "cognitive_complexity-1.3.0.tar.gz", hash = "sha256:a0cfbd47dee0b19f4056f892389f501694b205c3af69fb703cc744541e03dde5"}, -] - -[package.dependencies] -setuptools = "*" - [[package]] name = "colorama" version = "0.4.6" @@ -571,72 +488,64 @@ files = [ [[package]] name = "coverage" -version = "7.2.7" +version = "7.4.4" description = "Code coverage measurement for Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, + {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, + {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, + {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, + {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, ] [package.dependencies] @@ -693,14 +602,14 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "databases" -version = "0.8.0" +version = "0.7.0" description = "Async database support for Python." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "databases-0.8.0-py3-none-any.whl", hash = "sha256:0ceb7fd5c740d846e1f4f58c0256d780a6786841ec8e624a21f1eb1b51a9093d"}, - {file = "databases-0.8.0.tar.gz", hash = "sha256:6544d82e9926f233d694ec29cd018403444c7fb6e863af881a8304d1ff5cfb90"}, + {file = "databases-0.7.0-py3-none-any.whl", hash = "sha256:cf5da4b8a3e3cd038c459529725ebb64931cbbb7a091102664f20ef8f6cefd0d"}, + {file = "databases-0.7.0.tar.gz", hash = "sha256:ea2d419d3d2eb80595b7ceb8f282056f080af62efe2fb9bcd83562f93ec4b674"}, ] [package.dependencies] @@ -730,26 +639,26 @@ files = [ [[package]] name = "distlib" -version = "0.3.7" +version = "0.3.8" description = "Distribution utilities" category = "dev" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -757,222 +666,40 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.101.1" +version = "0.109.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "fastapi-0.101.1-py3-none-any.whl", hash = "sha256:aef5f8676eb1b8389952e1fe734abe20f04b71f6936afcc53b320ba79b686a4b"}, - {file = "fastapi-0.101.1.tar.gz", hash = "sha256:7b32000d14ca9992f7461117b81e4ef9ff0c07936af641b4fe40e67d5f9d63cb"}, + {file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"}, + {file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"}, ] [package.dependencies] pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.27.0,<0.28.0" -typing-extensions = ">=4.5.0" +starlette = ">=0.36.3,<0.37.0" +typing-extensions = ">=4.8.0" [package.extras] -all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "filelock" -version = "3.12.2" +version = "3.13.1" description = "A platform independent file lock." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, - {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, ] [package.extras] -docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "flake8" -version = "3.9.2" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, -] - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" - -[[package]] -name = "flake8-bandit" -version = "3.0.0" -description = "Automated security testing with bandit and flake8." -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "flake8_bandit-3.0.0-py2.py3-none-any.whl", hash = "sha256:61b617f4f7cdaa0e2b1e6bf7b68afb2b619a227bb3e3ae00dd36c213bd17900a"}, - {file = "flake8_bandit-3.0.0.tar.gz", hash = "sha256:54d19427e6a8d50322a7b02e1841c0a7c22d856975f3459803320e0e18e2d6a1"}, -] - -[package.dependencies] -bandit = ">=1.7.3" -flake8 = "*" -flake8-polyfill = "*" -pycodestyle = "*" - -[[package]] -name = "flake8-black" -version = "0.3.6" -description = "flake8 plugin to call black as a code style validator" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8-black-0.3.6.tar.gz", hash = "sha256:0dfbca3274777792a5bcb2af887a4cad72c72d0e86c94e08e3a3de151bb41c34"}, - {file = "flake8_black-0.3.6-py3-none-any.whl", hash = "sha256:fe8ea2eca98d8a504f22040d9117347f6b367458366952862ac3586e7d4eeaca"}, -] - -[package.dependencies] -black = ">=22.1.0" -flake8 = ">=3" -tomli = {version = "*", markers = "python_version < \"3.11\""} - -[package.extras] -develop = ["build", "twine"] - -[[package]] -name = "flake8-bugbear" -version = "23.3.12" -description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8-bugbear-23.3.12.tar.gz", hash = "sha256:e3e7f74c8a49ad3794a7183353026dabd68c74030d5f46571f84c1fb0eb79363"}, - {file = "flake8_bugbear-23.3.12-py3-none-any.whl", hash = "sha256:beb5c7efcd7ccc2039ef66a77bb8db925e7be3531ff1cb4d0b7030d0e2113d72"}, -] - -[package.dependencies] -attrs = ">=19.2.0" -flake8 = ">=3.0.0" - -[package.extras] -dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"] - -[[package]] -name = "flake8-builtins" -version = "2.1.0" -description = "Check for python builtins being used as variables or parameters." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8-builtins-2.1.0.tar.gz", hash = "sha256:12ff1ee96dd4e1f3141141ee6c45a5c7d3b3c440d0949e9b8d345c42b39c51d4"}, - {file = "flake8_builtins-2.1.0-py3-none-any.whl", hash = "sha256:469e8f03d6d0edf4b1e62b6d5a97dce4598592c8a13ec8f0952e7a185eba50a1"}, -] - -[package.dependencies] -flake8 = "*" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "flake8-cognitive-complexity" -version = "0.1.0" -description = "An extension for flake8 that validates cognitive functions complexity" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "flake8_cognitive_complexity-0.1.0.tar.gz", hash = "sha256:f202df054e4f6ff182b659c261922b9c684628a47beb19cb0973c50d6a7831c1"}, -] - -[package.dependencies] -cognitive_complexity = "*" -setuptools = "*" - -[[package]] -name = "flake8-expression-complexity" -version = "0.0.11" -description = "A flake8 extension that checks expressions complexity" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8_expression_complexity-0.0.11-py3-none-any.whl", hash = "sha256:b56bac37f7dd5d3d102a7111c89f6579c2cbd897b868147794c9ed12aadc627c"}, - {file = "flake8_expression_complexity-0.0.11.tar.gz", hash = "sha256:4dd8909fecbc20f53814cdcef9d0b04f61532764278d9b6e8026686812e96631"}, -] - -[package.dependencies] -astpretty = "*" -flake8 = "*" - -[[package]] -name = "flake8-functions" -version = "0.0.8" -description = "A flake8 extension that checks functions" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "flake8_functions-0.0.8-py3-none-any.whl", hash = "sha256:e1a88aa634d1aff6973f8c9dd64f30ab2beaac661e52eea96929ccc7ee7f64df"}, - {file = "flake8_functions-0.0.8.tar.gz", hash = "sha256:5446626673a9faecbf389fb411b90bdc87b002c387b72dc097b208e7a58f2a1c"}, -] - -[package.dependencies] -mr-proper = "*" -setuptools = "*" - -[[package]] -name = "flake8-import-order" -version = "0.18.2" -description = "Flake8 and pylama plugin that checks the ordering of import statements." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "flake8-import-order-0.18.2.tar.gz", hash = "sha256:e23941f892da3e0c09d711babbb0c73bc735242e9b216b726616758a920d900e"}, - {file = "flake8_import_order-0.18.2-py2.py3-none-any.whl", hash = "sha256:82ed59f1083b629b030ee9d3928d9e06b6213eb196fe745b3a7d4af2168130df"}, -] - -[package.dependencies] -pycodestyle = "*" -setuptools = "*" - -[[package]] -name = "flake8-polyfill" -version = "1.0.2" -description = "Polyfill package for Flake8 plugins" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, - {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, -] - -[package.dependencies] -flake8 = "*" - -[[package]] -name = "flake8-variables-names" -version = "0.0.6" -description = "A flake8 extension that helps to make more readable variables names" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8_variables_names-0.0.6-py3-none-any.whl", hash = "sha256:4aff935d54b3f7afcd026b4dae55029877bd05a7c507b294b45bc7bf577d7b47"}, - {file = "flake8_variables_names-0.0.6.tar.gz", hash = "sha256:292c50e4813d632aa3adcd02c185e7bb583f5fc8ebe02e70f13c958bfe46ad91"}, -] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] [[package]] name = "ghp-import" @@ -992,125 +719,92 @@ python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] -[[package]] -name = "gitdb" -version = "4.0.11" -description = "Git Object Database" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, - {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, -] - -[package.dependencies] -smmap = ">=3.0.1,<6" - -[[package]] -name = "gitpython" -version = "3.1.41" -description = "GitPython is a Python library used to interact with Git repositories" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "GitPython-3.1.41-py3-none-any.whl", hash = "sha256:c36b6634d069b3f719610175020a9aed919421c87552185b085e04fbbdb10b7c"}, - {file = "GitPython-3.1.41.tar.gz", hash = "sha256:ed66e624884f76df22c8e16066d567aaa5a37d5b5fa19db2c6df6f7156db9048"}, -] - -[package.dependencies] -gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} - -[package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "sumtypes"] - [[package]] name = "greenlet" -version = "3.0.1" +version = "3.0.3" description = "Lightweight in-process concurrent programming" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "greenlet-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63"}, - {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e"}, - {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846"}, - {file = "greenlet-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9"}, - {file = "greenlet-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234"}, - {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884"}, - {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94"}, - {file = "greenlet-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c"}, - {file = "greenlet-3.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5"}, - {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d"}, - {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445"}, - {file = "greenlet-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de"}, - {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166"}, - {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36"}, - {file = "greenlet-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1"}, - {file = "greenlet-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8"}, - {file = "greenlet-3.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9"}, - {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e"}, - {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a"}, - {file = "greenlet-3.0.1-cp38-cp38-win32.whl", hash = "sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd"}, - {file = "greenlet-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6"}, - {file = "greenlet-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d"}, - {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8"}, - {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546"}, - {file = "greenlet-3.0.1-cp39-cp39-win32.whl", hash = "sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57"}, - {file = "greenlet-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619"}, - {file = "greenlet-3.0.1.tar.gz", hash = "sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b"}, + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, ] [package.extras] -docs = ["Sphinx"] +docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] [[package]] name = "griffe" -version = "0.30.1" +version = "0.42.1" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "griffe-0.30.1-py3-none-any.whl", hash = "sha256:b2f3df6952995a6bebe19f797189d67aba7c860755d3d21cc80f64d076d0154c"}, - {file = "griffe-0.30.1.tar.gz", hash = "sha256:007cc11acd20becf1bb8f826419a52b9d403bbad9d8c8535699f5440ddc0a109"}, + {file = "griffe-0.42.1-py3-none-any.whl", hash = "sha256:7e805e35617601355edcac0d3511cedc1ed0cb1f7645e2d336ae4b05bbae7b3b"}, + {file = "griffe-0.42.1.tar.gz", hash = "sha256:57046131384043ed078692b85d86b76568a686266cc036b9b56b704466f803ce"}, ] [package.dependencies] -cached-property = {version = "*", markers = "python_version < \"3.8\""} +astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} colorama = ">=0.4" [[package]] @@ -1125,9 +819,6 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - [[package]] name = "httpcore" version = "0.17.3" @@ -1176,14 +867,14 @@ socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "identify" -version = "2.5.24" +version = "2.5.35" description = "File identification library for Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, - {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, ] [package.extras] @@ -1191,55 +882,54 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] name = "importlib-metadata" -version = "6.7.0" +version = "7.1.0" description = "Read metadata from Python packages" -category = "main" +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, - {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "importlib-resources" -version = "5.12.0" +version = "6.4.0" description = "Read resources from Python packages" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, - {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, + {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, + {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] [[package]] name = "iniconfig" @@ -1273,131 +963,91 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markdown" -version = "3.4.4" +version = "3.6" description = "Python implementation of John Gruber's Markdown." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, - {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, ] [package.dependencies] importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} [package.extras] -docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] -[[package]] -name = "markdown-it-py" -version = "2.2.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, - {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" -typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - [[package]] name = "markupsafe" -version = "2.1.3" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, -] - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -1463,7 +1113,6 @@ pathspec = ">=0.11.1" platformdirs = ">=2.2.0" pyyaml = ">=5.1" pyyaml-env-tag = ">=0.1" -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""} watchdog = ">=2.0" [package.extras] @@ -1472,18 +1121,19 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp [[package]] name = "mkdocs-autorefs" -version = "0.4.1" +version = "1.0.1" description = "Automatically link across pages in MkDocs." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, - {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, + {file = "mkdocs_autorefs-1.0.1-py3-none-any.whl", hash = "sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570"}, + {file = "mkdocs_autorefs-1.0.1.tar.gz", hash = "sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971"}, ] [package.dependencies] Markdown = ">=3.3" +markupsafe = ">=2.0.1" mkdocs = ">=1.1" [[package]] @@ -1518,39 +1168,39 @@ mkdocs = ">=1.0.3" [[package]] name = "mkdocs-material" -version = "9.2.7" +version = "9.2.8" description = "Documentation that simply works" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "mkdocs_material-9.2.7-py3-none-any.whl", hash = "sha256:92e4160d191cc76121fed14ab9f14638e43a6da0f2e9d7a9194d377f0a4e7f18"}, - {file = "mkdocs_material-9.2.7.tar.gz", hash = "sha256:b44da35b0d98cd762d09ef74f1ddce5b6d6e35c13f13beb0c9d82a629e5f229e"}, + {file = "mkdocs_material-9.2.8-py3-none-any.whl", hash = "sha256:6bc8524f8047a4f060d6ab0925b9d7cb61b3b5e6d5ca8a8e8085f8bfdeca1b71"}, + {file = "mkdocs_material-9.2.8.tar.gz", hash = "sha256:ec839dc5eaf42d8525acd1d6420fd0a0583671a4f98a9b3ff7897ae8628dbc2d"}, ] [package.dependencies] -babel = ">=2.10,<3.0" +babel = ">=2.12,<3.0" colorama = ">=0.4,<1.0" -jinja2 = ">=3.0,<4.0" -markdown = ">=3.2,<4.0" +jinja2 = ">=3.1,<4.0" +markdown = ">=3.4,<4.0" mkdocs = ">=1.5,<2.0" mkdocs-material-extensions = ">=1.1,<2.0" paginate = ">=0.5,<1.0" pygments = ">=2.16,<3.0" -pymdown-extensions = ">=10.2,<11.0" -regex = ">=2022.4,<2023.0" -requests = ">=2.26,<3.0" +pymdown-extensions = ">=10.3,<11.0" +regex = ">=2023.8,<2024.0" +requests = ">=2.31,<3.0" [[package]] name = "mkdocs-material-extensions" -version = "1.2" +version = "1.3.1" description = "Extension pack for Python Markdown and MkDocs Material." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mkdocs_material_extensions-1.2-py3-none-any.whl", hash = "sha256:c767bd6d6305f6420a50f0b541b0c9966d52068839af97029be14443849fb8a1"}, - {file = "mkdocs_material_extensions-1.2.tar.gz", hash = "sha256:27e2d1ed2d031426a6e10d5ea06989d67e90bb02acd588bc5673106b5ee5eedf"}, + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, ] [[package]] @@ -1598,81 +1248,66 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "1.1.2" +version = "1.8.0" description = "A Python handler for mkdocstrings." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mkdocstrings_python-1.1.2-py3-none-any.whl", hash = "sha256:c2b652a850fec8e85034a9cdb3b45f8ad1a558686edc20ed1f40b4e17e62070f"}, - {file = "mkdocstrings_python-1.1.2.tar.gz", hash = "sha256:f28bdcacb9bcdb44b6942a5642c1ea8b36870614d33e29e3c923e204a8d8ed61"}, + {file = "mkdocstrings_python-1.8.0-py3-none-any.whl", hash = "sha256:4209970cc90bec194568682a535848a8d8489516c6ed4adbe58bbc67b699ca9d"}, + {file = "mkdocstrings_python-1.8.0.tar.gz", hash = "sha256:1488bddf50ee42c07d9a488dddc197f8e8999c2899687043ec5dd1643d057192"}, ] [package.dependencies] -griffe = ">=0.24" +griffe = ">=0.37" mkdocstrings = ">=0.20" -[[package]] -name = "mr-proper" -version = "0.0.7" -description = "Static Python code analyzer, that tries to check if functions in code are pure or not and why." -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "mr_proper-0.0.7-py3-none-any.whl", hash = "sha256:74a1b60240c46f10ba518707ef72811a01e5c270da0a78b5dd2dd923d99fdb14"}, - {file = "mr_proper-0.0.7.tar.gz", hash = "sha256:03b517b19e617537f711ce418b125e5f2efd82ec881539cdee83195c78c14a02"}, -] - -[package.dependencies] -click = ">=7.1.2" -setuptools = "*" -stdlib-list = ">=0.5.0" -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} - [[package]] name = "mypy" -version = "0.982" +version = "1.9.0" description = "Optional static typing for Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mypy-0.982-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5"}, - {file = "mypy-0.982-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3"}, - {file = "mypy-0.982-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c"}, - {file = "mypy-0.982-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6"}, - {file = "mypy-0.982-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046"}, - {file = "mypy-0.982-cp310-cp310-win_amd64.whl", hash = "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e"}, - {file = "mypy-0.982-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20"}, - {file = "mypy-0.982-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947"}, - {file = "mypy-0.982-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40"}, - {file = "mypy-0.982-cp37-cp37m-win_amd64.whl", hash = "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1"}, - {file = "mypy-0.982-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24"}, - {file = "mypy-0.982-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e"}, - {file = "mypy-0.982-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda"}, - {file = "mypy-0.982-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206"}, - {file = "mypy-0.982-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763"}, - {file = "mypy-0.982-cp38-cp38-win_amd64.whl", hash = "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2"}, - {file = "mypy-0.982-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8"}, - {file = "mypy-0.982-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146"}, - {file = "mypy-0.982-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc"}, - {file = "mypy-0.982-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b"}, - {file = "mypy-0.982-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a"}, - {file = "mypy-0.982-cp39-cp39-win_amd64.whl", hash = "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795"}, - {file = "mypy-0.982-py3-none-any.whl", hash = "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d"}, - {file = "mypy-0.982.tar.gz", hash = "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, + {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, + {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, + {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, + {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, + {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, + {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, + {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, + {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, + {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, + {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, + {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, + {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, + {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, + {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, + {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, + {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, + {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, ] [package.dependencies] -mypy-extensions = ">=0.4.3" +mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} -typing-extensions = ">=3.10" +typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<2)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] @@ -1689,19 +1324,21 @@ files = [ [[package]] name = "mysqlclient" -version = "2.1.1" +version = "2.2.4" description = "Python interface to MySQL" category = "main" optional = true -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "mysqlclient-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c1ed71bd6244993b526113cca3df66428609f90e4652f37eb51c33496d478b37"}, - {file = "mysqlclient-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:c812b67e90082a840efb82a8978369e6e69fc62ce1bda4ca8f3084a9d862308b"}, - {file = "mysqlclient-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0d1cd3a5a4d28c222fa199002810e8146cffd821410b67851af4cc80aeccd97c"}, - {file = "mysqlclient-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b355c8b5a7d58f2e909acdbb050858390ee1b0e13672ae759e5e784110022994"}, - {file = "mysqlclient-2.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:996924f3483fd36a34a5812210c69e71dea5a3d5978d01199b78b7f6d485c855"}, - {file = "mysqlclient-2.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:dea88c8d3f5a5d9293dfe7f087c16dd350ceb175f2f6631c9cf4caf3e19b7a96"}, - {file = "mysqlclient-2.1.1.tar.gz", hash = "sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782"}, + {file = "mysqlclient-2.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac44777eab0a66c14cb0d38965572f762e193ec2e5c0723bcd11319cc5b693c5"}, + {file = "mysqlclient-2.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:329e4eec086a2336fe3541f1ce095d87a6f169d1cc8ba7b04ac68bcb234c9711"}, + {file = "mysqlclient-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:e1ebe3f41d152d7cb7c265349fdb7f1eca86ccb0ca24a90036cde48e00ceb2ab"}, + {file = "mysqlclient-2.2.4-cp38-cp38-win_amd64.whl", hash = "sha256:3c318755e06df599338dad7625f884b8a71fcf322a9939ef78c9b3db93e1de7a"}, + {file = "mysqlclient-2.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:9d4c015480c4a6b2b1602eccd9846103fc70606244788d04aa14b31c4bd1f0e2"}, + {file = "mysqlclient-2.2.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d43987bb9626096a302ca6ddcdd81feaeca65ced1d5fe892a6a66b808326aa54"}, + {file = "mysqlclient-2.2.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4e80dcad884dd6e14949ac6daf769123223a52a6805345608bf49cdaf7bc8b3a"}, + {file = "mysqlclient-2.2.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9d3310295cb682232cadc28abd172f406c718b9ada41d2371259098ae37779d3"}, + {file = "mysqlclient-2.2.4.tar.gz", hash = "sha256:33bc9fb3464e7d7c10b1eaf7336c5ff8f2a3d3b88bab432116ad2490beb3bf41"}, ] [[package]] @@ -1733,84 +1370,74 @@ setuptools = "*" [[package]] name = "orjson" -version = "3.9.7" +version = "3.9.15" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" category = "main" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "orjson-3.9.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6df858e37c321cefbf27fe7ece30a950bcc3a75618a804a0dcef7ed9dd9c92d"}, - {file = "orjson-3.9.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5198633137780d78b86bb54dafaaa9baea698b4f059456cd4554ab7009619221"}, - {file = "orjson-3.9.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e736815b30f7e3c9044ec06a98ee59e217a833227e10eb157f44071faddd7c5"}, - {file = "orjson-3.9.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a19e4074bc98793458b4b3ba35a9a1d132179345e60e152a1bb48c538ab863c4"}, - {file = "orjson-3.9.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80acafe396ab689a326ab0d80f8cc61dec0dd2c5dca5b4b3825e7b1e0132c101"}, - {file = "orjson-3.9.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:355efdbbf0cecc3bd9b12589b8f8e9f03c813a115efa53f8dc2a523bfdb01334"}, - {file = "orjson-3.9.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3aab72d2cef7f1dd6104c89b0b4d6b416b0db5ca87cc2fac5f79c5601f549cc2"}, - {file = "orjson-3.9.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36b1df2e4095368ee388190687cb1b8557c67bc38400a942a1a77713580b50ae"}, - {file = "orjson-3.9.7-cp310-none-win32.whl", hash = "sha256:e94b7b31aa0d65f5b7c72dd8f8227dbd3e30354b99e7a9af096d967a77f2a580"}, - {file = "orjson-3.9.7-cp310-none-win_amd64.whl", hash = "sha256:82720ab0cf5bb436bbd97a319ac529aee06077ff7e61cab57cee04a596c4f9b4"}, - {file = "orjson-3.9.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1f8b47650f90e298b78ecf4df003f66f54acdba6a0f763cc4df1eab048fe3738"}, - {file = "orjson-3.9.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f738fee63eb263530efd4d2e9c76316c1f47b3bbf38c1bf45ae9625feed0395e"}, - {file = "orjson-3.9.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38e34c3a21ed41a7dbd5349e24c3725be5416641fdeedf8f56fcbab6d981c900"}, - {file = "orjson-3.9.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21a3344163be3b2c7e22cef14fa5abe957a892b2ea0525ee86ad8186921b6cf0"}, - {file = "orjson-3.9.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23be6b22aab83f440b62a6f5975bcabeecb672bc627face6a83bc7aeb495dc7e"}, - {file = "orjson-3.9.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5205ec0dfab1887dd383597012199f5175035e782cdb013c542187d280ca443"}, - {file = "orjson-3.9.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8769806ea0b45d7bf75cad253fba9ac6700b7050ebb19337ff6b4e9060f963fa"}, - {file = "orjson-3.9.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f9e01239abea2f52a429fe9d95c96df95f078f0172489d691b4a848ace54a476"}, - {file = "orjson-3.9.7-cp311-none-win32.whl", hash = "sha256:8bdb6c911dae5fbf110fe4f5cba578437526334df381b3554b6ab7f626e5eeca"}, - {file = "orjson-3.9.7-cp311-none-win_amd64.whl", hash = "sha256:9d62c583b5110e6a5cf5169ab616aa4ec71f2c0c30f833306f9e378cf51b6c86"}, - {file = "orjson-3.9.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1c3cee5c23979deb8d1b82dc4cc49be59cccc0547999dbe9adb434bb7af11cf7"}, - {file = "orjson-3.9.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a347d7b43cb609e780ff8d7b3107d4bcb5b6fd09c2702aa7bdf52f15ed09fa09"}, - {file = "orjson-3.9.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:154fd67216c2ca38a2edb4089584504fbb6c0694b518b9020ad35ecc97252bb9"}, - {file = "orjson-3.9.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ea3e63e61b4b0beeb08508458bdff2daca7a321468d3c4b320a758a2f554d31"}, - {file = "orjson-3.9.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb0b0b2476f357eb2975ff040ef23978137aa674cd86204cfd15d2d17318588"}, - {file = "orjson-3.9.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b9a20a03576c6b7022926f614ac5a6b0914486825eac89196adf3267c6489d"}, - {file = "orjson-3.9.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:915e22c93e7b7b636240c5a79da5f6e4e84988d699656c8e27f2ac4c95b8dcc0"}, - {file = "orjson-3.9.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f26fb3e8e3e2ee405c947ff44a3e384e8fa1843bc35830fe6f3d9a95a1147b6e"}, - {file = "orjson-3.9.7-cp312-none-win_amd64.whl", hash = "sha256:d8692948cada6ee21f33db5e23460f71c8010d6dfcfe293c9b96737600a7df78"}, - {file = "orjson-3.9.7-cp37-cp37m-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7bab596678d29ad969a524823c4e828929a90c09e91cc438e0ad79b37ce41166"}, - {file = "orjson-3.9.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63ef3d371ea0b7239ace284cab9cd00d9c92b73119a7c274b437adb09bda35e6"}, - {file = "orjson-3.9.7-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f8fcf696bbbc584c0c7ed4adb92fd2ad7d153a50258842787bc1524e50d7081"}, - {file = "orjson-3.9.7-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90fe73a1f0321265126cbba13677dcceb367d926c7a65807bd80916af4c17047"}, - {file = "orjson-3.9.7-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45a47f41b6c3beeb31ac5cf0ff7524987cfcce0a10c43156eb3ee8d92d92bf22"}, - {file = "orjson-3.9.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a2937f528c84e64be20cb80e70cea76a6dfb74b628a04dab130679d4454395c"}, - {file = "orjson-3.9.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b4fb306c96e04c5863d52ba8d65137917a3d999059c11e659eba7b75a69167bd"}, - {file = "orjson-3.9.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:410aa9d34ad1089898f3db461b7b744d0efcf9252a9415bbdf23540d4f67589f"}, - {file = "orjson-3.9.7-cp37-none-win32.whl", hash = "sha256:26ffb398de58247ff7bde895fe30817a036f967b0ad0e1cf2b54bda5f8dcfdd9"}, - {file = "orjson-3.9.7-cp37-none-win_amd64.whl", hash = "sha256:bcb9a60ed2101af2af450318cd89c6b8313e9f8df4e8fb12b657b2e97227cf08"}, - {file = "orjson-3.9.7-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5da9032dac184b2ae2da4bce423edff7db34bfd936ebd7d4207ea45840f03905"}, - {file = "orjson-3.9.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7951af8f2998045c656ba8062e8edf5e83fd82b912534ab1de1345de08a41d2b"}, - {file = "orjson-3.9.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8e59650292aa3a8ea78073fc84184538783966528e442a1b9ed653aa282edcf"}, - {file = "orjson-3.9.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9274ba499e7dfb8a651ee876d80386b481336d3868cba29af839370514e4dce0"}, - {file = "orjson-3.9.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca1706e8b8b565e934c142db6a9592e6401dc430e4b067a97781a997070c5378"}, - {file = "orjson-3.9.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cc275cf6dcb1a248e1876cdefd3f9b5f01063854acdfd687ec360cd3c9712a"}, - {file = "orjson-3.9.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:11c10f31f2c2056585f89d8229a56013bc2fe5de51e095ebc71868d070a8dd81"}, - {file = "orjson-3.9.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cf334ce1d2fadd1bf3e5e9bf15e58e0c42b26eb6590875ce65bd877d917a58aa"}, - {file = "orjson-3.9.7-cp38-none-win32.whl", hash = "sha256:76a0fc023910d8a8ab64daed8d31d608446d2d77c6474b616b34537aa7b79c7f"}, - {file = "orjson-3.9.7-cp38-none-win_amd64.whl", hash = "sha256:7a34a199d89d82d1897fd4a47820eb50947eec9cda5fd73f4578ff692a912f89"}, - {file = "orjson-3.9.7-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e7e7f44e091b93eb39db88bb0cb765db09b7a7f64aea2f35e7d86cbf47046c65"}, - {file = "orjson-3.9.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d647b2a9c45a23a84c3e70e19d120011cba5f56131d185c1b78685457320bb"}, - {file = "orjson-3.9.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0eb850a87e900a9c484150c414e21af53a6125a13f6e378cf4cc11ae86c8f9c5"}, - {file = "orjson-3.9.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f4b0042d8388ac85b8330b65406c84c3229420a05068445c13ca28cc222f1f7"}, - {file = "orjson-3.9.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd3e7aae977c723cc1dbb82f97babdb5e5fbce109630fbabb2ea5053523c89d3"}, - {file = "orjson-3.9.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c616b796358a70b1f675a24628e4823b67d9e376df2703e893da58247458956"}, - {file = "orjson-3.9.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3ba725cf5cf87d2d2d988d39c6a2a8b6fc983d78ff71bc728b0be54c869c884"}, - {file = "orjson-3.9.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4891d4c934f88b6c29b56395dfc7014ebf7e10b9e22ffd9877784e16c6b2064f"}, - {file = "orjson-3.9.7-cp39-none-win32.whl", hash = "sha256:14d3fb6cd1040a4a4a530b28e8085131ed94ebc90d72793c59a713de34b60838"}, - {file = "orjson-3.9.7-cp39-none-win_amd64.whl", hash = "sha256:9ef82157bbcecd75d6296d5d8b2d792242afcd064eb1ac573f8847b52e58f677"}, - {file = "orjson-3.9.7.tar.gz", hash = "sha256:85e39198f78e2f7e054d296395f6c96f5e02892337746ef5b6a1bf3ed5910142"}, + {file = "orjson-3.9.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d61f7ce4727a9fa7680cd6f3986b0e2c732639f46a5e0156e550e35258aa313a"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4feeb41882e8aa17634b589533baafdceb387e01e117b1ec65534ec724023d04"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbbeb3c9b2edb5fd044b2a070f127a0ac456ffd079cb82746fc84af01ef021a4"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b66bcc5670e8a6b78f0313bcb74774c8291f6f8aeef10fe70e910b8040f3ab75"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2973474811db7b35c30248d1129c64fd2bdf40d57d84beed2a9a379a6f57d0ab"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe41b6f72f52d3da4db524c8653e46243c8c92df826ab5ffaece2dba9cccd58"}, + {file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4228aace81781cc9d05a3ec3a6d2673a1ad0d8725b4e915f1089803e9efd2b99"}, + {file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f7b65bfaf69493c73423ce9db66cfe9138b2f9ef62897486417a8fcb0a92bfe"}, + {file = "orjson-3.9.15-cp310-none-win32.whl", hash = "sha256:2d99e3c4c13a7b0fb3792cc04c2829c9db07838fb6973e578b85c1745e7d0ce7"}, + {file = "orjson-3.9.15-cp310-none-win_amd64.whl", hash = "sha256:b725da33e6e58e4a5d27958568484aa766e825e93aa20c26c91168be58e08cbb"}, + {file = "orjson-3.9.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c8e8fe01e435005d4421f183038fc70ca85d2c1e490f51fb972db92af6e047c2"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87f1097acb569dde17f246faa268759a71a2cb8c96dd392cd25c668b104cad2f"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff0f9913d82e1d1fadbd976424c316fbc4d9c525c81d047bbdd16bd27dd98cfc"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8055ec598605b0077e29652ccfe9372247474375e0e3f5775c91d9434e12d6b1"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6768a327ea1ba44c9114dba5fdda4a214bdb70129065cd0807eb5f010bfcbb5"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12365576039b1a5a47df01aadb353b68223da413e2e7f98c02403061aad34bde"}, + {file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71c6b009d431b3839d7c14c3af86788b3cfac41e969e3e1c22f8a6ea13139404"}, + {file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e18668f1bd39e69b7fed19fa7cd1cd110a121ec25439328b5c89934e6d30d357"}, + {file = "orjson-3.9.15-cp311-none-win32.whl", hash = "sha256:62482873e0289cf7313461009bf62ac8b2e54bc6f00c6fabcde785709231a5d7"}, + {file = "orjson-3.9.15-cp311-none-win_amd64.whl", hash = "sha256:b3d336ed75d17c7b1af233a6561cf421dee41d9204aa3cfcc6c9c65cd5bb69a8"}, + {file = "orjson-3.9.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:82425dd5c7bd3adfe4e94c78e27e2fa02971750c2b7ffba648b0f5d5cc016a73"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c51378d4a8255b2e7c1e5cc430644f0939539deddfa77f6fac7b56a9784160a"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae4e06be04dc00618247c4ae3f7c3e561d5bc19ab6941427f6d3722a0875ef7"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcef128f970bb63ecf9a65f7beafd9b55e3aaf0efc271a4154050fc15cdb386e"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b72758f3ffc36ca566ba98a8e7f4f373b6c17c646ff8ad9b21ad10c29186f00d"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c57bc7b946cf2efa67ac55766e41764b66d40cbd9489041e637c1304400494"}, + {file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:946c3a1ef25338e78107fba746f299f926db408d34553b4754e90a7de1d44068"}, + {file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2f256d03957075fcb5923410058982aea85455d035607486ccb847f095442bda"}, + {file = "orjson-3.9.15-cp312-none-win_amd64.whl", hash = "sha256:5bb399e1b49db120653a31463b4a7b27cf2fbfe60469546baf681d1b39f4edf2"}, + {file = "orjson-3.9.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b17f0f14a9c0ba55ff6279a922d1932e24b13fc218a3e968ecdbf791b3682b25"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f6cbd8e6e446fb7e4ed5bac4661a29e43f38aeecbf60c4b900b825a353276a1"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76bc6356d07c1d9f4b782813094d0caf1703b729d876ab6a676f3aaa9a47e37c"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdfa97090e2d6f73dced247a2f2d8004ac6449df6568f30e7fa1a045767c69a6"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7413070a3e927e4207d00bd65f42d1b780fb0d32d7b1d951f6dc6ade318e1b5a"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cf1596680ac1f01839dba32d496136bdd5d8ffb858c280fa82bbfeb173bdd40"}, + {file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:809d653c155e2cc4fd39ad69c08fdff7f4016c355ae4b88905219d3579e31eb7"}, + {file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:920fa5a0c5175ab14b9c78f6f820b75804fb4984423ee4c4f1e6d748f8b22bc1"}, + {file = "orjson-3.9.15-cp38-none-win32.whl", hash = "sha256:2b5c0f532905e60cf22a511120e3719b85d9c25d0e1c2a8abb20c4dede3b05a5"}, + {file = "orjson-3.9.15-cp38-none-win_amd64.whl", hash = "sha256:67384f588f7f8daf040114337d34a5188346e3fae6c38b6a19a2fe8c663a2f9b"}, + {file = "orjson-3.9.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6fc2fe4647927070df3d93f561d7e588a38865ea0040027662e3e541d592811e"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34cbcd216e7af5270f2ffa63a963346845eb71e174ea530867b7443892d77180"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f541587f5c558abd93cb0de491ce99a9ef8d1ae29dd6ab4dbb5a13281ae04cbd"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92255879280ef9c3c0bcb327c5a1b8ed694c290d61a6a532458264f887f052cb"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a1f57fb601c426635fcae9ddbe90dfc1ed42245eb4c75e4960440cac667262"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ede0bde16cc6e9b96633df1631fbcd66491d1063667f260a4f2386a098393790"}, + {file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e88b97ef13910e5f87bcbc4dd7979a7de9ba8702b54d3204ac587e83639c0c2b"}, + {file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57d5d8cf9c27f7ef6bc56a5925c7fbc76b61288ab674eb352c26ac780caa5b10"}, + {file = "orjson-3.9.15-cp39-none-win32.whl", hash = "sha256:001f4eb0ecd8e9ebd295722d0cbedf0748680fb9998d3993abaed2f40587257a"}, + {file = "orjson-3.9.15-cp39-none-win_amd64.whl", hash = "sha256:ea0b183a5fe6b2b45f3b854b0d19c4e932d6f5934ae1f723b07cf9560edd4ec7"}, + {file = "orjson-3.9.15.tar.gz", hash = "sha256:95cae920959d772f30ab36d3b25f83bb0f3be671e986c72ce22f8fa700dae061"}, ] [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -1826,62 +1453,44 @@ files = [ [[package]] name = "pathspec" -version = "0.11.2" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, -] - -[[package]] -name = "pbr" -version = "6.0.0" -description = "Python Build Reasonableness" -category = "dev" -optional = false -python-versions = ">=2.6" -files = [ - {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, - {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "platformdirs" -version = "3.11.0" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""} - [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" -version = "1.2.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] @@ -1901,7 +1510,6 @@ files = [ [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" @@ -2000,18 +1608,6 @@ files = [ {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, ] -[[package]] -name = "pycodestyle" -version = "2.7.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, -] - [[package]] name = "pycparser" version = "2.21" @@ -2026,98 +1622,190 @@ files = [ [[package]] name = "pydantic" -version = "1.10.11" -description = "Data validation and settings management using python type hints" +version = "2.5.3" +description = "Data validation using Python type hints" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ff44c5e89315b15ff1f7fdaf9853770b810936d6b01a7bcecaa227d2f8fe444f"}, - {file = "pydantic-1.10.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c098d4ab5e2d5b3984d3cb2527e2d6099d3de85630c8934efcfdc348a9760e"}, - {file = "pydantic-1.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16928fdc9cb273c6af00d9d5045434c39afba5f42325fb990add2c241402d151"}, - {file = "pydantic-1.10.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0588788a9a85f3e5e9ebca14211a496409cb3deca5b6971ff37c556d581854e7"}, - {file = "pydantic-1.10.11-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9baf78b31da2dc3d3f346ef18e58ec5f12f5aaa17ac517e2ffd026a92a87588"}, - {file = "pydantic-1.10.11-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:373c0840f5c2b5b1ccadd9286782852b901055998136287828731868027a724f"}, - {file = "pydantic-1.10.11-cp310-cp310-win_amd64.whl", hash = "sha256:c3339a46bbe6013ef7bdd2844679bfe500347ac5742cd4019a88312aa58a9847"}, - {file = "pydantic-1.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:08a6c32e1c3809fbc49debb96bf833164f3438b3696abf0fbeceb417d123e6eb"}, - {file = "pydantic-1.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a451ccab49971af043ec4e0d207cbc8cbe53dbf148ef9f19599024076fe9c25b"}, - {file = "pydantic-1.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b02d24f7b2b365fed586ed73582c20f353a4c50e4be9ba2c57ab96f8091ddae"}, - {file = "pydantic-1.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f34739a89260dfa420aa3cbd069fbcc794b25bbe5c0a214f8fb29e363484b66"}, - {file = "pydantic-1.10.11-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e297897eb4bebde985f72a46a7552a7556a3dd11e7f76acda0c1093e3dbcf216"}, - {file = "pydantic-1.10.11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d185819a7a059550ecb85d5134e7d40f2565f3dd94cfd870132c5f91a89cf58c"}, - {file = "pydantic-1.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:4400015f15c9b464c9db2d5d951b6a780102cfa5870f2c036d37c23b56f7fc1b"}, - {file = "pydantic-1.10.11-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2417de68290434461a266271fc57274a138510dca19982336639484c73a07af6"}, - {file = "pydantic-1.10.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:331c031ba1554b974c98679bd0780d89670d6fd6f53f5d70b10bdc9addee1713"}, - {file = "pydantic-1.10.11-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8268a735a14c308923e8958363e3a3404f6834bb98c11f5ab43251a4e410170c"}, - {file = "pydantic-1.10.11-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:44e51ba599c3ef227e168424e220cd3e544288c57829520dc90ea9cb190c3248"}, - {file = "pydantic-1.10.11-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d7781f1d13b19700b7949c5a639c764a077cbbdd4322ed505b449d3ca8edcb36"}, - {file = "pydantic-1.10.11-cp37-cp37m-win_amd64.whl", hash = "sha256:7522a7666157aa22b812ce14c827574ddccc94f361237ca6ea8bb0d5c38f1629"}, - {file = "pydantic-1.10.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc64eab9b19cd794a380179ac0e6752335e9555d214cfcb755820333c0784cb3"}, - {file = "pydantic-1.10.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8dc77064471780262b6a68fe67e013298d130414d5aaf9b562c33987dbd2cf4f"}, - {file = "pydantic-1.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe429898f2c9dd209bd0632a606bddc06f8bce081bbd03d1c775a45886e2c1cb"}, - {file = "pydantic-1.10.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:192c608ad002a748e4a0bed2ddbcd98f9b56df50a7c24d9a931a8c5dd053bd3d"}, - {file = "pydantic-1.10.11-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ef55392ec4bb5721f4ded1096241e4b7151ba6d50a50a80a2526c854f42e6a2f"}, - {file = "pydantic-1.10.11-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:41e0bb6efe86281623abbeeb0be64eab740c865388ee934cd3e6a358784aca6e"}, - {file = "pydantic-1.10.11-cp38-cp38-win_amd64.whl", hash = "sha256:265a60da42f9f27e0b1014eab8acd3e53bd0bad5c5b4884e98a55f8f596b2c19"}, - {file = "pydantic-1.10.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:469adf96c8e2c2bbfa655fc7735a2a82f4c543d9fee97bd113a7fb509bf5e622"}, - {file = "pydantic-1.10.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6cbfbd010b14c8a905a7b10f9fe090068d1744d46f9e0c021db28daeb8b6de1"}, - {file = "pydantic-1.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abade85268cc92dff86d6effcd917893130f0ff516f3d637f50dadc22ae93999"}, - {file = "pydantic-1.10.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9738b0f2e6c70f44ee0de53f2089d6002b10c33264abee07bdb5c7f03038303"}, - {file = "pydantic-1.10.11-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:787cf23e5a0cde753f2eabac1b2e73ae3844eb873fd1f5bdbff3048d8dbb7604"}, - {file = "pydantic-1.10.11-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:174899023337b9fc685ac8adaa7b047050616136ccd30e9070627c1aaab53a13"}, - {file = "pydantic-1.10.11-cp39-cp39-win_amd64.whl", hash = "sha256:1954f8778489a04b245a1e7b8b22a9d3ea8ef49337285693cf6959e4b757535e"}, - {file = "pydantic-1.10.11-py3-none-any.whl", hash = "sha256:008c5e266c8aada206d0627a011504e14268a62091450210eda7c07fabe6963e"}, - {file = "pydantic-1.10.11.tar.gz", hash = "sha256:f66d479cf7eb331372c470614be6511eae96f1f120344c25f3f9bb59fb1b5528"}, + {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, + {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.14.6" +typing-extensions = ">=4.6.1" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] [[package]] -name = "pyflakes" -version = "2.3.1" -description = "passive checker of Python programs" +name = "pydantic-core" +version = "2.14.6" +description = "" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, + {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, + {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, + {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, + {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, + {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, + {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, + {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, + {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, + {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, + {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, + {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, + {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, + {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, + {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, + {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-extra-types" +version = "2.6.0" +description = "Extra Pydantic types." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" files = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, + {file = "pydantic_extra_types-2.6.0-py3-none-any.whl", hash = "sha256:d291d521c2e2bf2e6f11971caf8d639518124ae26a76d2e712599e98c4ef2b2b"}, + {file = "pydantic_extra_types-2.6.0.tar.gz", hash = "sha256:e9a93cfb245158462acb76621785219f80ad112303a0a7784d2ada65e6ed6cba"}, ] +[package.dependencies] +pydantic = ">=2.5.2" + +[package.extras] +all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23)", "python-ulid (>=1,<2)", "python-ulid (>=1,<3)"] + [[package]] name = "pygments" -version = "2.16.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.2.1" +version = "10.7.1" description = "Extension pack for Python Markdown." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.2.1-py3-none-any.whl", hash = "sha256:bded105eb8d93f88f2f821f00108cb70cef1269db6a40128c09c5f48bfc60ea4"}, - {file = "pymdown_extensions-10.2.1.tar.gz", hash = "sha256:d0c534b4a5725a4be7ccef25d65a4c97dba58b54ad7c813babf0eb5ba9c81591"}, + {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, + {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, ] [package.dependencies] -markdown = ">=3.2" +markdown = ">=3.5" pyyaml = "*" [package.extras] @@ -2141,14 +1829,14 @@ rsa = ["cryptography"] [[package]] name = "pyparsing" -version = "3.1.1" +version = "3.1.2" description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "dev" optional = false python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, - {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, ] [package.extras] @@ -2169,7 +1857,6 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" @@ -2192,7 +1879,6 @@ files = [ [package.dependencies] pytest = ">=7.0.0" -typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] @@ -2221,25 +1907,25 @@ histogram = ["pygal", "pygaljs"] [[package]] name = "pytest-codspeed" -version = "2.2.0" +version = "2.2.1" description = "Pytest plugin to create CodSpeed benchmarks" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest_codspeed-2.2.0-py3-none-any.whl", hash = "sha256:5da48b842fc465926d122dd15bb86e86af5d9f0c53ec1b7c736e9a9aed558c13"}, - {file = "pytest_codspeed-2.2.0.tar.gz", hash = "sha256:665003fc20117b64a98d16ffd1008f5bd6bf3b1e9af142b98c00abff7f626bbd"}, + {file = "pytest_codspeed-2.2.1-py3-none-any.whl", hash = "sha256:aad08033015f3e6c8c14c8bf0eca475921a9b088e92c98b626bf8af8f516471e"}, + {file = "pytest_codspeed-2.2.1.tar.gz", hash = "sha256:0adc24baf01c64a6ca0a0b83b3cd704351708997e09ec086b7776c32227d4e0a"}, ] [package.dependencies] -cffi = ">=1.15.1,<1.16.0" -filelock = ">=3.12.2,<3.13.0" +cffi = ">=1.15.1" +filelock = ">=3.12.2" pytest = ">=3.8" -setuptools = {version = ">=67.8.0,<67.9.0", markers = "python_full_version >= \"3.12.0b1\""} +setuptools = {version = "*", markers = "python_full_version >= \"3.12.0\""} [package.extras] compat = ["pytest-benchmark (>=4.0.0,<4.1.0)", "pytest-xdist (>=2.0.0,<2.1.0)"] -lint = ["black (>=23.3.0,<23.4.0)", "isort (>=5.12.0,<5.13.0)", "mypy (>=1.3.0,<1.4.0)", "ruff (>=0.0.275,<0.1.0)"] +lint = ["mypy (>=1.3.0,<1.4.0)", "ruff (>=0.3.3,<0.4.0)"] test = ["pytest (>=7.0,<8.0)", "pytest-cov (>=4.0.0,<4.1.0)"] [[package]] @@ -2263,14 +1949,14 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -2278,14 +1964,14 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2023.3.post1" +version = "2024.1" description = "World timezone definitions, modern and historical" category = "dev" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] [[package]] @@ -2355,100 +2041,105 @@ pyyaml = "*" [[package]] name = "regex" -version = "2022.10.31" +version = "2023.12.25" description = "Alternative regular expression module, to replace re." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, - {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, - {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, - {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, - {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, - {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, - {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, - {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, - {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, - {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, - {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, - {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, - {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, - {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, - {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, - {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, - {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, - {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, - {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, - {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, - {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, - {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, - {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, - {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, - {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, - {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, - {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, - {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, - {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, - {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, - {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, - {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, - {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, - {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, - {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, + {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, + {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, + {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, + {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, + {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, + {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, + {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, + {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, + {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, + {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, + {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, + {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, + {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, + {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, + {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, + {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, + {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, + {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, + {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, + {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, + {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, + {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, + {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, + {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, + {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, + {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, + {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, + {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, + {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, + {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, + {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, ] [[package]] @@ -2474,41 +2165,48 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] -name = "rich" -version = "13.6.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "dev" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, - {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "setuptools" -version = "67.8.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" +name = "ruff" +version = "0.0.275" +description = "An extremely fast Python linter, written in Rust." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.8.0-py3-none-any.whl", hash = "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f"}, - {file = "setuptools-67.8.0.tar.gz", hash = "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"}, + {file = "ruff-0.0.275-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:5e6554a072e7ce81eb6f0bec1cebd3dcb0e358652c0f4900d7d630d61691e914"}, + {file = "ruff-0.0.275-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:1cc599022fe5ffb143a965b8d659eb64161ab8ab4433d208777eab018a1aab67"}, + {file = "ruff-0.0.275-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5206fc1cd8c1c1deadd2e6360c0dbcd690f1c845da588ca9d32e4a764a402c60"}, + {file = "ruff-0.0.275-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c4e6468da26f77b90cae35319d310999f471a8c352998e9b39937a23750149e"}, + {file = "ruff-0.0.275-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0dbdea02942131dbc15dd45f431d152224f15e1dd1859fcd0c0487b658f60f1a"}, + {file = "ruff-0.0.275-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:22efd9f41af27ef8fb9779462c46c35c89134d33e326c889971e10b2eaf50c63"}, + {file = "ruff-0.0.275-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c09662112cfa22d7467a19252a546291fd0eae4f423e52b75a7a2000a1894db"}, + {file = "ruff-0.0.275-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80043726662144876a381efaab88841c88e8df8baa69559f96b22d4fa216bef1"}, + {file = "ruff-0.0.275-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5859ee543b01b7eb67835dfd505faa8bb7cc1550f0295c92c1401b45b42be399"}, + {file = "ruff-0.0.275-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c8ace4d40a57b5ea3c16555f25a6b16bc5d8b2779ae1912ce2633543d4e9b1da"}, + {file = "ruff-0.0.275-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8347fc16aa185aae275906c4ac5b770e00c896b6a0acd5ba521f158801911998"}, + {file = "ruff-0.0.275-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ec43658c64bfda44fd84bbea9da8c7a3b34f65448192d1c4dd63e9f4e7abfdd4"}, + {file = "ruff-0.0.275-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:508b13f7ca37274cceaba4fb3ea5da6ca192356323d92acf39462337c33ad14e"}, + {file = "ruff-0.0.275-py3-none-win32.whl", hash = "sha256:6afb1c4422f24f361e877937e2a44b3f8176774a476f5e33845ebfe887dd5ec2"}, + {file = "ruff-0.0.275-py3-none-win_amd64.whl", hash = "sha256:d9b264d78621bf7b698b6755d4913ab52c19bd28bee1a16001f954d64c1a1220"}, + {file = "ruff-0.0.275-py3-none-win_arm64.whl", hash = "sha256:a19ce3bea71023eee5f0f089dde4a4272d088d5ac0b675867e074983238ccc65"}, + {file = "ruff-0.0.275.tar.gz", hash = "sha256:a63a0b645da699ae5c758fce19188e901b3033ec54d862d93fcd042addf7f38d"}, +] + +[[package]] +name = "setuptools" +version = "69.2.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -2522,89 +2220,76 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "smmap" -version = "5.0.1" -description = "A pure Python implementation of a sliding window memory map manager" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, - {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, -] - [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "sqlalchemy" -version = "1.4.51" +version = "1.4.52" description = "Database Abstraction Library" category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "SQLAlchemy-1.4.51-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:1a09d5bd1a40d76ad90e5570530e082ddc000e1d92de495746f6257dc08f166b"}, - {file = "SQLAlchemy-1.4.51-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2be4e6294c53f2ec8ea36486b56390e3bcaa052bf3a9a47005687ccf376745d1"}, - {file = "SQLAlchemy-1.4.51-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca484ca11c65e05639ffe80f20d45e6be81fbec7683d6c9a15cd421e6e8b340"}, - {file = "SQLAlchemy-1.4.51-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0535d5b57d014d06ceeaeffd816bb3a6e2dddeb670222570b8c4953e2d2ea678"}, - {file = "SQLAlchemy-1.4.51-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af55cc207865d641a57f7044e98b08b09220da3d1b13a46f26487cc2f898a072"}, - {file = "SQLAlchemy-1.4.51-cp310-cp310-win32.whl", hash = "sha256:7af40425ac535cbda129d9915edcaa002afe35d84609fd3b9d6a8c46732e02ee"}, - {file = "SQLAlchemy-1.4.51-cp310-cp310-win_amd64.whl", hash = "sha256:8d1d7d63e5d2f4e92a39ae1e897a5d551720179bb8d1254883e7113d3826d43c"}, - {file = "SQLAlchemy-1.4.51-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eaeeb2464019765bc4340214fca1143081d49972864773f3f1e95dba5c7edc7d"}, - {file = "SQLAlchemy-1.4.51-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7deeae5071930abb3669b5185abb6c33ddfd2398f87660fafdb9e6a5fb0f3f2f"}, - {file = "SQLAlchemy-1.4.51-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0892e7ac8bc76da499ad3ee8de8da4d7905a3110b952e2a35a940dab1ffa550e"}, - {file = "SQLAlchemy-1.4.51-cp311-cp311-win32.whl", hash = "sha256:50e074aea505f4427151c286955ea025f51752fa42f9939749336672e0674c81"}, - {file = "SQLAlchemy-1.4.51-cp311-cp311-win_amd64.whl", hash = "sha256:3b0cd89a7bd03f57ae58263d0f828a072d1b440c8c2949f38f3b446148321171"}, - {file = "SQLAlchemy-1.4.51-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a33cb3f095e7d776ec76e79d92d83117438b6153510770fcd57b9c96f9ef623d"}, - {file = "SQLAlchemy-1.4.51-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cacc0b2dd7d22a918a9642fc89840a5d3cee18a0e1fe41080b1141b23b10916"}, - {file = "SQLAlchemy-1.4.51-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:245c67c88e63f1523e9216cad6ba3107dea2d3ee19adc359597a628afcabfbcb"}, - {file = "SQLAlchemy-1.4.51-cp312-cp312-win32.whl", hash = "sha256:8e702e7489f39375601c7ea5a0bef207256828a2bc5986c65cb15cd0cf097a87"}, - {file = "SQLAlchemy-1.4.51-cp312-cp312-win_amd64.whl", hash = "sha256:0525c4905b4b52d8ccc3c203c9d7ab2a80329ffa077d4bacf31aefda7604dc65"}, - {file = "SQLAlchemy-1.4.51-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:1980e6eb6c9be49ea8f89889989127daafc43f0b1b6843d71efab1514973cca0"}, - {file = "SQLAlchemy-1.4.51-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec7a0ed9b32afdf337172678a4a0e6419775ba4e649b66f49415615fa47efbd"}, - {file = "SQLAlchemy-1.4.51-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352df882088a55293f621328ec33b6ffca936ad7f23013b22520542e1ab6ad1b"}, - {file = "SQLAlchemy-1.4.51-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:86a22143a4001f53bf58027b044da1fb10d67b62a785fc1390b5c7f089d9838c"}, - {file = "SQLAlchemy-1.4.51-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c37bc677690fd33932182b85d37433845de612962ed080c3e4d92f758d1bd894"}, - {file = "SQLAlchemy-1.4.51-cp36-cp36m-win32.whl", hash = "sha256:d0a83afab5e062abffcdcbcc74f9d3ba37b2385294dd0927ad65fc6ebe04e054"}, - {file = "SQLAlchemy-1.4.51-cp36-cp36m-win_amd64.whl", hash = "sha256:a61184c7289146c8cff06b6b41807c6994c6d437278e72cf00ff7fe1c7a263d1"}, - {file = "SQLAlchemy-1.4.51-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:3f0ef620ecbab46e81035cf3dedfb412a7da35340500ba470f9ce43a1e6c423b"}, - {file = "SQLAlchemy-1.4.51-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c55040d8ea65414de7c47f1a23823cd9f3fad0dc93e6b6b728fee81230f817b"}, - {file = "SQLAlchemy-1.4.51-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ef80328e3fee2be0a1abe3fe9445d3a2e52a1282ba342d0dab6edf1fef4707"}, - {file = "SQLAlchemy-1.4.51-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f8cafa6f885a0ff5e39efa9325195217bb47d5929ab0051636610d24aef45ade"}, - {file = "SQLAlchemy-1.4.51-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8f2df79a46e130235bc5e1bbef4de0583fb19d481eaa0bffa76e8347ea45ec6"}, - {file = "SQLAlchemy-1.4.51-cp37-cp37m-win32.whl", hash = "sha256:f2e5b6f5cf7c18df66d082604a1d9c7a2d18f7d1dbe9514a2afaccbb51cc4fc3"}, - {file = "SQLAlchemy-1.4.51-cp37-cp37m-win_amd64.whl", hash = "sha256:5e180fff133d21a800c4f050733d59340f40d42364fcb9d14f6a67764bdc48d2"}, - {file = "SQLAlchemy-1.4.51-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:7d8139ca0b9f93890ab899da678816518af74312bb8cd71fb721436a93a93298"}, - {file = "SQLAlchemy-1.4.51-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb18549b770351b54e1ab5da37d22bc530b8bfe2ee31e22b9ebe650640d2ef12"}, - {file = "SQLAlchemy-1.4.51-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55e699466106d09f028ab78d3c2e1f621b5ef2c8694598242259e4515715da7c"}, - {file = "SQLAlchemy-1.4.51-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2ad16880ccd971ac8e570550fbdef1385e094b022d6fc85ef3ce7df400dddad3"}, - {file = "SQLAlchemy-1.4.51-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b97fd5bb6b7c1a64b7ac0632f7ce389b8ab362e7bd5f60654c2a418496be5d7f"}, - {file = "SQLAlchemy-1.4.51-cp38-cp38-win32.whl", hash = "sha256:cecb66492440ae8592797dd705a0cbaa6abe0555f4fa6c5f40b078bd2740fc6b"}, - {file = "SQLAlchemy-1.4.51-cp38-cp38-win_amd64.whl", hash = "sha256:39b02b645632c5fe46b8dd30755682f629ffbb62ff317ecc14c998c21b2896ff"}, - {file = "SQLAlchemy-1.4.51-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:b03850c290c765b87102959ea53299dc9addf76ca08a06ea98383348ae205c99"}, - {file = "SQLAlchemy-1.4.51-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e646b19f47d655261b22df9976e572f588185279970efba3d45c377127d35349"}, - {file = "SQLAlchemy-1.4.51-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3cf56cc36d42908495760b223ca9c2c0f9f0002b4eddc994b24db5fcb86a9e4"}, - {file = "SQLAlchemy-1.4.51-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0d661cff58c91726c601cc0ee626bf167b20cc4d7941c93c5f3ac28dc34ddbea"}, - {file = "SQLAlchemy-1.4.51-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3823dda635988e6744d4417e13f2e2b5fe76c4bf29dd67e95f98717e1b094cad"}, - {file = "SQLAlchemy-1.4.51-cp39-cp39-win32.whl", hash = "sha256:b00cf0471888823b7a9f722c6c41eb6985cf34f077edcf62695ac4bed6ec01ee"}, - {file = "SQLAlchemy-1.4.51-cp39-cp39-win_amd64.whl", hash = "sha256:a055ba17f4675aadcda3005df2e28a86feb731fdcc865e1f6b4f209ed1225cba"}, - {file = "SQLAlchemy-1.4.51.tar.gz", hash = "sha256:e7908c2025eb18394e32d65dd02d2e37e17d733cdbe7d78231c2b6d7eb20cdb9"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f68016f9a5713684c1507cc37133c28035f29925c75c0df2f9d0f7571e23720a"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24bb0f81fbbb13d737b7f76d1821ec0b117ce8cbb8ee5e8641ad2de41aa916d3"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e93983cc0d2edae253b3f2141b0a3fb07e41c76cd79c2ad743fc27eb79c3f6db"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:84e10772cfc333eb08d0b7ef808cd76e4a9a30a725fb62a0495877a57ee41d81"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:427988398d2902de042093d17f2b9619a5ebc605bf6372f7d70e29bde6736842"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-win32.whl", hash = "sha256:1296f2cdd6db09b98ceb3c93025f0da4835303b8ac46c15c2136e27ee4d18d94"}, + {file = "SQLAlchemy-1.4.52-cp310-cp310-win_amd64.whl", hash = "sha256:80e7f697bccc56ac6eac9e2df5c98b47de57e7006d2e46e1a3c17c546254f6ef"}, + {file = "SQLAlchemy-1.4.52-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2f251af4c75a675ea42766880ff430ac33291c8d0057acca79710f9e5a77383d"}, + {file = "SQLAlchemy-1.4.52-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb8f9e4c4718f111d7b530c4e6fb4d28f9f110eb82e7961412955b3875b66de0"}, + {file = "SQLAlchemy-1.4.52-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afb1672b57f58c0318ad2cff80b384e816735ffc7e848d8aa51e0b0fc2f4b7bb"}, + {file = "SQLAlchemy-1.4.52-cp311-cp311-win32.whl", hash = "sha256:6e41cb5cda641f3754568d2ed8962f772a7f2b59403b95c60c89f3e0bd25f15e"}, + {file = "SQLAlchemy-1.4.52-cp311-cp311-win_amd64.whl", hash = "sha256:5bed4f8c3b69779de9d99eb03fd9ab67a850d74ab0243d1be9d4080e77b6af12"}, + {file = "SQLAlchemy-1.4.52-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:49e3772eb3380ac88d35495843daf3c03f094b713e66c7d017e322144a5c6b7c"}, + {file = "SQLAlchemy-1.4.52-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:618827c1a1c243d2540314c6e100aee7af09a709bd005bae971686fab6723554"}, + {file = "SQLAlchemy-1.4.52-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de9acf369aaadb71a725b7e83a5ef40ca3de1cf4cdc93fa847df6b12d3cd924b"}, + {file = "SQLAlchemy-1.4.52-cp312-cp312-win32.whl", hash = "sha256:763bd97c4ebc74136ecf3526b34808c58945023a59927b416acebcd68d1fc126"}, + {file = "SQLAlchemy-1.4.52-cp312-cp312-win_amd64.whl", hash = "sha256:f12aaf94f4d9679ca475975578739e12cc5b461172e04d66f7a3c39dd14ffc64"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:853fcfd1f54224ea7aabcf34b227d2b64a08cbac116ecf376907968b29b8e763"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f98dbb8fcc6d1c03ae8ec735d3c62110949a3b8bc6e215053aa27096857afb45"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e135fff2e84103bc15c07edd8569612ce317d64bdb391f49ce57124a73f45c5"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b5de6af8852500d01398f5047d62ca3431d1e29a331d0b56c3e14cb03f8094c"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3491c85df263a5c2157c594f54a1a9c72265b75d3777e61ee13c556d9e43ffc9"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-win32.whl", hash = "sha256:427c282dd0deba1f07bcbf499cbcc9fe9a626743f5d4989bfdfd3ed3513003dd"}, + {file = "SQLAlchemy-1.4.52-cp36-cp36m-win_amd64.whl", hash = "sha256:ca5ce82b11731492204cff8845c5e8ca1a4bd1ade85e3b8fcf86e7601bfc6a39"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:29d4247313abb2015f8979137fe65f4eaceead5247d39603cc4b4a610936cd2b"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a752bff4796bf22803d052d4841ebc3c55c26fb65551f2c96e90ac7c62be763a"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7ea11727feb2861deaa293c7971a4df57ef1c90e42cb53f0da40c3468388000"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d913f8953e098ca931ad7f58797f91deed26b435ec3756478b75c608aa80d139"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a251146b921725547ea1735b060a11e1be705017b568c9f8067ca61e6ef85f20"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-win32.whl", hash = "sha256:1f8e1c6a6b7f8e9407ad9afc0ea41c1f65225ce505b79bc0342159de9c890782"}, + {file = "SQLAlchemy-1.4.52-cp37-cp37m-win_amd64.whl", hash = "sha256:346ed50cb2c30f5d7a03d888e25744154ceac6f0e6e1ab3bc7b5b77138d37710"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:4dae6001457d4497736e3bc422165f107ecdd70b0d651fab7f731276e8b9e12d"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5d2e08d79f5bf250afb4a61426b41026e448da446b55e4770c2afdc1e200fce"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bbce5dd7c7735e01d24f5a60177f3e589078f83c8a29e124a6521b76d825b85"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bdb7b4d889631a3b2a81a3347c4c3f031812eb4adeaa3ee4e6b0d028ad1852b5"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c294ae4e6bbd060dd79e2bd5bba8b6274d08ffd65b58d106394cb6abbf35cf45"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-win32.whl", hash = "sha256:bcdfb4b47fe04967669874fb1ce782a006756fdbebe7263f6a000e1db969120e"}, + {file = "SQLAlchemy-1.4.52-cp38-cp38-win_amd64.whl", hash = "sha256:7d0dbc56cb6af5088f3658982d3d8c1d6a82691f31f7b0da682c7b98fa914e91"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:a551d5f3dc63f096ed41775ceec72fdf91462bb95abdc179010dc95a93957800"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab773f9ad848118df7a9bbabca53e3f1002387cdbb6ee81693db808b82aaab0"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2de46f5d5396d5331127cfa71f837cca945f9a2b04f7cb5a01949cf676db7d1"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7027be7930a90d18a386b25ee8af30514c61f3852c7268899f23fdfbd3107181"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99224d621affbb3c1a4f72b631f8393045f4ce647dd3262f12fe3576918f8bf3"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-win32.whl", hash = "sha256:c124912fd4e1bb9d1e7dc193ed482a9f812769cb1e69363ab68e01801e859821"}, + {file = "SQLAlchemy-1.4.52-cp39-cp39-win_amd64.whl", hash = "sha256:2c286fab42e49db23c46ab02479f328b8bdb837d3e281cae546cc4085c83b680"}, + {file = "SQLAlchemy-1.4.52.tar.gz", hash = "sha256:80e63bbdc5217dad3485059bdf6f65a7d43f33c8bde619df5c220edf03d87296"}, ] [package.dependencies] greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] @@ -2629,14 +2314,14 @@ sqlcipher = ["sqlcipher3_binary"] [[package]] name = "starlette" -version = "0.27.0" +version = "0.36.3" description = "The little ASGI library that shines." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, - {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, + {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, + {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, ] [package.dependencies] @@ -2644,42 +2329,7 @@ anyio = ">=3.4.0,<5" typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] - -[[package]] -name = "stdlib-list" -version = "0.9.0" -description = "A list of Python Standard Libraries (2.7 through 3.9)." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "stdlib_list-0.9.0-py3-none-any.whl", hash = "sha256:f79957d59e41930d44afcd81e465f740b9a7a9828707a40e24ab1092b12bd423"}, - {file = "stdlib_list-0.9.0.tar.gz", hash = "sha256:98eb66135976c96b4ee3f4c0ef0552ebb5a9977ce3028433db79f4738b02af26"}, -] - -[package.extras] -dev = ["build", "stdlib-list[doc,lint,test]"] -doc = ["furo", "sphinx"] -lint = ["black", "mypy", "ruff"] -support = ["sphobjinv"] -test = ["coverage[toml]", "pytest", "pytest-cov"] - -[[package]] -name = "stevedore" -version = "3.5.2" -description = "Manage dynamic plugins for Python applications" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "stevedore-3.5.2-py3-none-any.whl", hash = "sha256:fa2630e3d0ad3e22d4914aff2501445815b9a4467a6edc49387c667a38faf5bf"}, - {file = "stevedore-3.5.2.tar.gz", hash = "sha256:cf99f41fc0d5a4f185ca4d3d42b03be9011b0a1ec1a4ea1a282be1b4b306dcc2"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} -pbr = ">=2.0.0,<2.1.0 || >2.1.0" +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] [[package]] name = "tomli" @@ -2693,67 +2343,16 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -[[package]] -name = "typed-ast" -version = "1.5.5" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, - {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, - {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, - {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, - {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, - {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, - {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, - {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, - {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, - {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, - {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, - {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, - {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, - {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, - {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"}, - {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"}, - {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"}, - {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"}, - {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"}, - {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"}, - {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, - {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, - {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, - {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, - {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, - {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, - {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, - {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, - {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, - {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, - {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, - {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, - {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, - {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, - {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, - {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, - {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, - {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, - {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, - {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, - {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, -] - [[package]] name = "types-aiofiles" -version = "23.2.0.0" +version = "23.2.0.20240311" description = "Typing stubs for aiofiles" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "types-aiofiles-23.2.0.0.tar.gz", hash = "sha256:b6a7127bd232e0802532837b84140b1cd5df19ee60bea3a5699720d2b583361b"}, - {file = "types_aiofiles-23.2.0.0-py3-none-any.whl", hash = "sha256:5d6719e8148cb2a9c4ea46dad86d50d3b675c46a940adca698533a8d2216d53d"}, + {file = "types-aiofiles-23.2.0.20240311.tar.gz", hash = "sha256:208e6b090de732739ef74ab8f133c954479c8e77e614f276f9e475a0cc986430"}, + {file = "types_aiofiles-23.2.0.20240311-py3-none-any.whl", hash = "sha256:ed10a8002d88c94220597b77304cf1a1d8cf489c7143fc3ffa2c96488b20fec7"}, ] [[package]] @@ -2830,14 +2429,14 @@ files = [ [[package]] name = "types-requests" -version = "2.31.0.20231231" +version = "2.31.0.20240311" description = "Typing stubs for requests" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "types-requests-2.31.0.20231231.tar.gz", hash = "sha256:0f8c0c9764773384122813548d9eea92a5c4e1f33ed54556b508968ec5065cee"}, - {file = "types_requests-2.31.0.20231231-py3-none-any.whl", hash = "sha256:2e2230c7bc8dd63fa3153c1c0ae335f8a368447f0582fc332f17d54f88e69027"}, + {file = "types-requests-2.31.0.20240311.tar.gz", hash = "sha256:b1c1b66abfb7fa79aae09097a811c4aa97130eb8831c60e47aee4ca344731ca5"}, + {file = "types_requests-2.31.0.20240311-py3-none-any.whl", hash = "sha256:47872893d65a38e282ee9f277a4ee50d1b28bd592040df7d1fdaffdf3779937d"}, ] [package.dependencies] @@ -2845,14 +2444,14 @@ urllib3 = ">=2" [[package]] name = "types-toml" -version = "0.10.8.7" +version = "0.10.8.20240310" description = "Typing stubs for toml" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "types-toml-0.10.8.7.tar.gz", hash = "sha256:58b0781c681e671ff0b5c0319309910689f4ab40e8a2431e205d70c94bb6efb1"}, - {file = "types_toml-0.10.8.7-py3-none-any.whl", hash = "sha256:61951da6ad410794c97bec035d59376ce1cbf4453dc9b6f90477e81e4442d631"}, + {file = "types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331"}, + {file = "types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d"}, ] [[package]] @@ -2869,31 +2468,31 @@ files = [ [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] name = "urllib3" -version = "2.0.7" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2914,21 +2513,20 @@ test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] [[package]] name = "virtualenv" -version = "20.24.6" +version = "20.25.1" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, - {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, + {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, + {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" -importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} -platformdirs = ">=3.9.1,<4" +platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] @@ -2974,6 +2572,21 @@ files = [ [package.extras] watchmedo = ["PyYAML (>=3.10)"] +[[package]] +name = "wheel" +version = "0.43.0" +description = "A built-package format for Python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "wheel-0.43.0-py3-none-any.whl", hash = "sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81"}, + {file = "wheel-0.43.0.tar.gz", hash = "sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85"}, +] + +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=65)"] + [[package]] name = "yappi" version = "1.6.0" @@ -3039,19 +2652,19 @@ test = ["gevent (>=20.6.2)"] [[package]] name = "zipp" -version = "3.15.0" +version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] aiopg = ["aiopg", "psycopg2-binary"] @@ -3065,5 +2678,5 @@ sqlite = ["aiosqlite"] [metadata] lock-version = "2.0" -python-versions = "^3.7.0" -content-hash = "e61acb4cb597d78bc6d1e0aa2d9fd2d144ade8e027aba7c5a66c0cf9442b1e61" +python-versions = "^3.8.0" +content-hash = "44acc5ac9c3f812f1cd60c3fabc82b723f76d4441455da81b79cd9eee40f321f" diff --git a/pyproject.toml b/pyproject.toml index 64c3a6e..bc5c846 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "ormar" [tool.poetry] name = "ormar" -version = "0.12.2" +version = "0.20.0" description = "An async ORM with fastapi in mind and pydantic validation." authors = ["Radosław Drążkiewicz "] license = "MIT" @@ -32,19 +32,19 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", "Framework :: AsyncIO", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3 :: Only", ] [tool.poetry.dependencies] -python = "^3.7.0" -databases = "^0.8.0" -pydantic = ">=1.6.1,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<1.10.12" -SQLAlchemy = "^1.4.48" +python = "^3.8.0" +databases = "^0.7.0" +pydantic = "v2.5.3" +SQLAlchemy = "^1.4.42" cryptography = { version = "^41.0.3", optional = true } # Async database drivers aiosqlite = { version = "^0.19.0", optional = true } @@ -61,37 +61,42 @@ PyMySQL = { version = "^1.1.0", optional = true } version = ">=3.6.4" optional = true -[tool.poetry.dependencies.typing-extensions] -version = ">=3.7,<=5.0" -python = "<3.8" -[tool.poetry.dependencies.importlib-metadata] -version = ">=3.1" -python = "<3.8" +[tool.poetry.extras] +postgresql = ["asyncpg", "psycopg2-binary"] +postgres = ["asyncpg", "psycopg2-binary"] +aiopg = ["aiopg", "psycopg2-binary"] +mysql = ["aiomysql", "PyMySQL"] +sqlite = ["aiosqlite"] +orjson = ["orjson"] +crypto = ["cryptography"] +all = [ + "aiosqlite", + "asyncpg", + "aiopg", + "psycopg2-binary", + "aiomysql", + "mysqlclient", + "PyMySQL", + "orjson", + "cryptography", +] - -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] # Testing pytest = "^7.4.4" pytest-cov = "^4.0.0" codecov = "^2.1.13" -pytest-asyncio = "^0.21.1" -fastapi = ">=0.70.1,<=0.101.1" -flake8 = "^3.9.2" -flake8-black = "^0.3.6" -flake8-bugbear = "^23.3.12" -flake8-import-order = "^0.18.1" -flake8-bandit = "^3.0.0" -flake8-builtins = "^2.1.0" -flake8-variables-names = "^0.0.6" -flake8-cognitive-complexity = "^0.1.0" -flake8-functions = "^0.0.8" -flake8-expression-complexity = "^0.0.11" +pytest-asyncio = "^0.21.0" +fastapi = "^0.109.1" + +black = "^24.1.0" +ruff = "^0.0.275" # types -mypy = "^0.982" -types-ujson = "^5.9.0" -types-PyMySQL = "^1.1.0" +mypy = "^1.8.0" +types-ujson = "^5.7.0" +types-PyMySQL = "^1.0.19" types-ipaddress = "^1.0.1" types-enum34 = "^1.1.1" types-cryptography = "^3.3.23" @@ -117,30 +122,10 @@ pytest-benchmark = "^4.0.0" nest-asyncio = "^1.6.0" pre-commit = "^2.21.0" - -[tool.poetry.extras] -postgresql = ["asyncpg", "psycopg2-binary"] -postgres = ["asyncpg", "psycopg2-binary"] -aiopg = ["aiopg", "psycopg2-binary"] -mysql = ["aiomysql", "PyMySQL"] -sqlite = ["aiosqlite"] -orjson = ["orjson"] -crypto = ["cryptography"] -all = [ - "aiosqlite", - "asyncpg", - "aiopg", - "psycopg2-binary", - "aiomysql", - "mysqlclient", - "PyMySQL", - "orjson", - "cryptography", -] - -[tool.poetry.group.dev.dependencies] httpx = "^0.24.1" asgi-lifespan = "^2.1.0" +pydantic-extra-types = "^2.5.0" +watchdog = "<4.0.0" pytest-codspeed = "^2.2.0" mike = "^2.0.0" @@ -172,3 +157,9 @@ ignore_missing_imports = true based_on_style = "pep8" disable_ending_comma_heuristic = true split_arguments_when_comma_terminated = true + +[tool.ruff] +select = ["E", "F", "I"] +ignore = ["E402"] +line-length = 88 +src = ["ormar", "tests"] diff --git a/scripts/test_docs.sh b/scripts/test_docs.sh new file mode 100644 index 0000000..a5dacf6 --- /dev/null +++ b/scripts/test_docs.sh @@ -0,0 +1,12 @@ +#!/bin/sh -e + +PACKAGE="docs_src" + +PREFIX="" +if [ -d 'venv' ] ; then + PREFIX="venv/bin/" +fi + +set -x + +PYTHONPATH=. ${PREFIX}pytest --ignore venv docs_src/ "${@}" diff --git a/tests/lifespan.py b/tests/lifespan.py new file mode 100644 index 0000000..afd786f --- /dev/null +++ b/tests/lifespan.py @@ -0,0 +1,33 @@ +from contextlib import asynccontextmanager +from typing import AsyncIterator + +import pytest +import sqlalchemy +from fastapi import FastAPI + + +def lifespan(config): + @asynccontextmanager + async def do_lifespan(_: FastAPI) -> AsyncIterator[None]: + if not config.database.is_connected: + await config.database.connect() + + yield + + if config.database.is_connected: + await config.database.disconnect() + + return do_lifespan + + +def init_tests(config, scope="module"): + @pytest.fixture(autouse=True, scope=scope) + def create_database(): + config.engine = sqlalchemy.create_engine(config.database.url._url) + config.metadata.create_all(config.engine) + + yield + + config.metadata.drop_all(config.engine) + + return create_database diff --git a/tests/settings.py b/tests/settings.py index be1bed2..b2b7c9e 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,9 +1,23 @@ import os import databases +import ormar +import sqlalchemy DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///test.db") database_url = databases.DatabaseURL(DATABASE_URL) if database_url.scheme == "postgresql+aiopg": # pragma no cover DATABASE_URL = str(database_url.replace(driver=None)) print("USED DB:", DATABASE_URL) + + +def create_config(**args): + database_ = databases.Database(DATABASE_URL, **args) + metadata_ = sqlalchemy.MetaData() + engine_ = sqlalchemy.create_engine(DATABASE_URL) + + return ormar.OrmarConfig( + metadata=metadata_, + database=database_, + engine=engine_, + ) diff --git a/tests/test_deferred/test_forward_cross_refs.py b/tests/test_deferred/test_forward_cross_refs.py index 5805759..8a87fc1 100644 --- a/tests/test_deferred/test_forward_cross_refs.py +++ b/tests/test_deferred/test_forward_cross_refs.py @@ -1,31 +1,20 @@ # type: ignore -from typing import List, Optional - -import databases -import pytest -import sqlalchemy as sa -from pydantic.typing import ForwardRef -from sqlalchemy import create_engine +from typing import ForwardRef, List, Optional import ormar -from ormar import ModelMeta -from tests.settings import DATABASE_URL +import pytest + +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() -metadata = sa.MetaData() -db = databases.Database(DATABASE_URL) -engine = create_engine(DATABASE_URL) TeacherRef = ForwardRef("Teacher") -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = db - - class Student(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -35,14 +24,11 @@ class Student(ormar.Model): class StudentTeacher(ormar.Model): - class Meta(BaseMeta): - tablename = "students_x_teachers" + ormar_config = base_ormar_config.copy(tablename="students_x_teachers") class Teacher(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -58,8 +44,7 @@ CountryRef = ForwardRef("Country") class Country(ormar.Model): - class Meta(BaseMeta): - tablename = "countries" + ormar_config = base_ormar_config.copy(tablename="countries") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=128) @@ -70,8 +55,7 @@ class Country(ormar.Model): class City(ormar.Model): - class Meta(BaseMeta): - tablename = "cities" + ormar_config = base_ormar_config.copy(tablename="cities") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=128) @@ -83,17 +67,13 @@ class City(ormar.Model): Country.update_forward_refs() -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_double_relations(): - async with db: - async with db.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): t1 = await Teacher.objects.create(name="Mr. Jones") t2 = await Teacher.objects.create(name="Ms. Smith") t3 = await Teacher.objects.create(name="Mr. Quibble") @@ -150,8 +130,8 @@ async def test_double_relations(): @pytest.mark.asyncio async def test_auto_through_model(): - async with db: - async with db.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): england = await Country(name="England").save() france = await Country(name="France").save() london = await City(name="London", country=england).save() diff --git a/tests/test_deferred/test_forward_refs.py b/tests/test_deferred/test_forward_refs.py index 2753113..6208fa7 100644 --- a/tests/test_deferred/test_forward_refs.py +++ b/tests/test_deferred/test_forward_refs.py @@ -1,29 +1,23 @@ # type: ignore -from typing import List +from typing import ForwardRef, List, Optional -import databases +import ormar import pytest import pytest_asyncio import sqlalchemy as sa -from pydantic.typing import ForwardRef -from sqlalchemy import create_engine - -import ormar -from ormar import ModelMeta from ormar.exceptions import ModelError -from tests.settings import DATABASE_URL -metadata = sa.MetaData() -db = databases.Database(DATABASE_URL) -engine = create_engine(DATABASE_URL) +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() + PersonRef = ForwardRef("Person") class Person(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -38,9 +32,7 @@ ChildFriendRef = ForwardRef("ChildFriend") class Child(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -54,15 +46,11 @@ class Child(ormar.Model): class ChildFriend(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() class Game(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -71,17 +59,13 @@ class Game(ormar.Model): Child.update_forward_refs() -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest_asyncio.fixture(scope="function") async def cleanup(): yield - async with db: + async with base_ormar_config.database: await ChildFriend.objects.delete(each=True) await Child.objects.delete(each=True) await Game.objects.delete(each=True) @@ -93,9 +77,7 @@ async def test_not_updated_model_raises_errors(): Person2Ref = ForwardRef("Person2") class Person2(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -116,14 +98,10 @@ async def test_not_updated_model_m2m_raises_errors(): Person3Ref = ForwardRef("Person3") class PersonFriend(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() class Person3(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -146,17 +124,13 @@ async def test_not_updated_model_m2m_through_raises_errors(): PersonPetRef = ForwardRef("PersonPet") class Pet(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Person4(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -165,9 +139,7 @@ async def test_not_updated_model_m2m_through_raises_errors(): ) class PersonPet(ormar.Model): - class Meta(ModelMeta): - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() with pytest.raises(ModelError): await Person4.objects.create(name="Test") @@ -180,25 +152,25 @@ async def test_not_updated_model_m2m_through_raises_errors(): def test_proper_field_init(): - assert "supervisor" in Person.Meta.model_fields - assert Person.Meta.model_fields["supervisor"].to == Person + assert "supervisor" in Person.ormar_config.model_fields + assert Person.ormar_config.model_fields["supervisor"].to == Person - assert "supervisor" in Person.__fields__ - assert Person.__fields__["supervisor"].type_ == Person + assert "supervisor" in Person.model_fields + assert Person.model_fields["supervisor"].annotation == Optional[Person] - assert "supervisor" in Person.Meta.table.columns + assert "supervisor" in Person.ormar_config.table.columns assert isinstance( - Person.Meta.table.columns["supervisor"].type, sa.sql.sqltypes.Integer + Person.ormar_config.table.columns["supervisor"].type, sa.sql.sqltypes.Integer ) - assert len(Person.Meta.table.columns["supervisor"].foreign_keys) > 0 + assert len(Person.ormar_config.table.columns["supervisor"].foreign_keys) > 0 - assert "person_supervisor" in Person.Meta.alias_manager._aliases_new + assert "person_supervisor" in Person.ormar_config.alias_manager._aliases_new @pytest.mark.asyncio async def test_self_relation(): - async with db: - async with db.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): sam = await Person.objects.create(name="Sam") joe = await Person(name="Joe", supervisor=sam).save() assert joe.supervisor.name == "Sam" @@ -215,8 +187,8 @@ async def test_self_relation(): @pytest.mark.asyncio async def test_other_forwardref_relation(cleanup): - async with db: - async with db.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): checkers = await Game.objects.create(name="checkers") uno = await Game(name="Uno").save() @@ -242,8 +214,8 @@ async def test_other_forwardref_relation(cleanup): @pytest.mark.asyncio async def test_m2m_self_forwardref_relation(cleanup): - async with db: - async with db.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): checkers = await Game.objects.create(name="Checkers") uno = await Game(name="Uno").save() jenga = await Game(name="Jenga").save() diff --git a/tests/test_deferred/test_more_same_table_joins.py b/tests/test_deferred/test_more_same_table_joins.py index c4f906b..fc89f40 100644 --- a/tests/test_deferred/test_more_same_table_joins.py +++ b/tests/test_deferred/test_more_same_table_joins.py @@ -1,42 +1,30 @@ -import asyncio from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config(force_rollback=True) class Department(ormar.Model): - class Meta: - tablename = "departments" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="departments") id: int = ormar.Integer(primary_key=True, autoincrement=False) name: str = ormar.String(max_length=100) class SchoolClass(ormar.Model): - class Meta: - tablename = "schoolclasses" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="schoolclasses") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -44,10 +32,7 @@ class Category(ormar.Model): class Student(ormar.Model): - class Meta: - tablename = "students" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="students") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -56,10 +41,7 @@ class Student(ormar.Model): class Teacher(ormar.Model): - class Meta: - tablename = "teachers" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="teachers") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -67,13 +49,7 @@ class Teacher(ormar.Model): category: Optional[Category] = ormar.ForeignKey(Category, nullable=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) async def create_data(): @@ -91,35 +67,35 @@ async def create_data(): @pytest.mark.asyncio async def test_model_multiple_instances_of_same_table_in_schema(): - async with database: + async with base_ormar_config.database: await create_data() classes = await SchoolClass.objects.select_related( ["teachers__category__department", "students__category__department"] ).all() assert classes[0].name == "Math" assert classes[0].students[0].name == "Jane" - assert len(classes[0].dict().get("students")) == 2 + assert len(classes[0].model_dump().get("students")) == 2 assert classes[0].teachers[0].category.department.name == "Law Department" assert classes[0].students[0].category.department.name == "Math Department" @pytest.mark.asyncio async def test_load_all_multiple_instances_of_same_table_in_schema(): - async with database: + async with base_ormar_config.database: await create_data() math_class = await SchoolClass.objects.get(name="Math") assert math_class.name == "Math" await math_class.load_all(follow=True) assert math_class.students[0].name == "Jane" - assert len(math_class.dict().get("students")) == 2 + assert len(math_class.model_dump().get("students")) == 2 assert math_class.teachers[0].category.department.name == "Law Department" assert math_class.students[0].category.department.name == "Math Department" @pytest.mark.asyncio async def test_filter_groups_with_instances_of_same_table_in_schema(): - async with database: + async with base_ormar_config.database: await create_data() math_class = ( await SchoolClass.objects.select_related( @@ -136,7 +112,7 @@ async def test_filter_groups_with_instances_of_same_table_in_schema(): ) assert math_class.name == "Math" assert math_class.students[0].name == "Jane" - assert len(math_class.dict().get("students")) == 2 + assert len(math_class.model_dump().get("students")) == 2 assert math_class.teachers[0].category.department.name == "Law Department" assert math_class.students[0].category.department.name == "Math Department" diff --git a/tests/test_deferred/test_same_table_joins.py b/tests/test_deferred/test_same_table_joins.py index dd03e8d..2b0c9f8 100644 --- a/tests/test_deferred/test_same_table_joins.py +++ b/tests/test_deferred/test_same_table_joins.py @@ -1,32 +1,23 @@ -import asyncio from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Department(ormar.Model): - class Meta: - tablename = "departments" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="departments") id: int = ormar.Integer(primary_key=True, autoincrement=False) name: str = ormar.String(max_length=100) class SchoolClass(ormar.Model): - class Meta: - tablename = "schoolclasses" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="schoolclasses") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -34,20 +25,14 @@ class SchoolClass(ormar.Model): class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Student(ormar.Model): - class Meta: - tablename = "students" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="students") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -56,10 +41,7 @@ class Student(ormar.Model): class Teacher(ormar.Model): - class Meta: - tablename = "teachers" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="teachers") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -67,13 +49,7 @@ class Teacher(ormar.Model): category: Optional[Category] = ormar.ForeignKey(Category, nullable=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) async def create_data(): @@ -91,8 +67,8 @@ async def create_data(): @pytest.mark.asyncio async def test_model_multiple_instances_of_same_table_in_schema(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await create_data() classes = await SchoolClass.objects.select_related( ["teachers__category", "students__schoolclass"] @@ -100,9 +76,10 @@ async def test_model_multiple_instances_of_same_table_in_schema(): assert classes[0].name == "Math" assert classes[0].students[0].name == "Jane" - assert len(classes[0].dict().get("students")) == 2 + assert len(classes[0].model_dump().get("students")) == 2 - # since it's going from schoolclass => teacher => schoolclass (same class) department is already populated + # since it's going from schoolclass => teacher + # => schoolclass (same class) department is already populated assert classes[0].students[0].schoolclass.name == "Math" assert classes[0].students[0].schoolclass.department.name is None await classes[0].students[0].schoolclass.department.load() @@ -118,8 +95,8 @@ async def test_model_multiple_instances_of_same_table_in_schema(): @pytest.mark.asyncio async def test_right_tables_join(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await create_data() classes = await SchoolClass.objects.select_related( ["teachers__category", "students"] @@ -133,8 +110,8 @@ async def test_right_tables_join(): @pytest.mark.asyncio async def test_multiple_reverse_related_objects(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await create_data() classes = await SchoolClass.objects.select_related( ["teachers__category", "students__category"] diff --git a/tests/test_encryption/test_encrypted_columns.py b/tests/test_encryption/test_encrypted_columns.py index 387d84e..872fd48 100644 --- a/tests/test_encryption/test_encrypted_columns.py +++ b/tests/test_encryption/test_encrypted_columns.py @@ -1,29 +1,20 @@ # type: ignore import base64 +import datetime import decimal import hashlib import uuid -import datetime from typing import Any -import databases -import pytest -import sqlalchemy - import ormar +import pytest from ormar import ModelDefinitionError, NoMatch from ormar.fields.sqlalchemy_encrypted import EncryptedString -from tests.settings import DATABASE_URL - -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +from tests.lifespan import init_tests +from tests.settings import create_config +base_ormar_config = create_config() default_fernet = dict( encrypt_secret="asd123", encrypt_backend=ormar.EncryptBackends.FERNET ) @@ -41,8 +32,7 @@ class DummyBackend(ormar.fields.EncryptBackend): class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, **default_fernet) @@ -70,6 +60,10 @@ class Author(ormar.Model): test_smallint: int = ormar.SmallInteger(default=0, **default_fernet) test_decimal = ormar.Decimal(scale=2, precision=10, **default_fernet) test_decimal2 = ormar.Decimal(max_digits=10, decimal_places=2, **default_fernet) + test_bytes = ormar.LargeBinary(max_length=100, **default_fernet) + test_b64bytes = ormar.LargeBinary( + max_length=100, represent_as_base64_str=True, **default_fernet + ) custom_backend: str = ormar.String( max_length=200, encrypt_secret="asda8", @@ -79,8 +73,7 @@ class Author(ormar.Model): class Hash(ormar.Model): - class Meta(BaseMeta): - tablename = "hashes" + ormar_config = base_ormar_config.copy(tablename="hashes") id: int = ormar.Integer(primary_key=True) name: str = ormar.String( @@ -91,8 +84,7 @@ class Hash(ormar.Model): class Filter(ormar.Model): - class Meta(BaseMeta): - tablename = "filters" + ormar_config = base_ormar_config.copy(tablename="filters") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, **default_fernet) @@ -100,29 +92,21 @@ class Filter(ormar.Model): class Report(ormar.Model): - class Meta(BaseMeta): - tablename = "reports" + ormar_config = base_ormar_config.copy(tablename="reports") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) filters = ormar.ManyToMany(Filter) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_error_on_encrypted_pk(): with pytest.raises(ModelDefinitionError): class Wrong(ormar.Model): - class Meta(BaseMeta): - tablename = "wrongs" + ormar_config = base_ormar_config.copy(tablename="wrongs") id: int = ormar.Integer( primary_key=True, @@ -135,8 +119,7 @@ def test_error_on_encrypted_relation(): with pytest.raises(ModelDefinitionError): class Wrong2(ormar.Model): - class Meta(BaseMeta): - tablename = "wrongs2" + ormar_config = base_ormar_config.copy(tablename="wrongs2") id: int = ormar.Integer(primary_key=True) author = ormar.ForeignKey( @@ -150,8 +133,7 @@ def test_error_on_encrypted_m2m_relation(): with pytest.raises(ModelDefinitionError): class Wrong3(ormar.Model): - class Meta(BaseMeta): - tablename = "wrongs3" + ormar_config = base_ormar_config.copy(tablename="wrongs3") id: int = ormar.Integer(primary_key=True) author = ormar.ManyToMany( @@ -165,8 +147,7 @@ def test_wrong_backend(): with pytest.raises(ModelDefinitionError): class Wrong3(ormar.Model): - class Meta(BaseMeta): - tablename = "wrongs3" + ormar_config = base_ormar_config.copy(tablename="wrongs3") id: int = ormar.Integer(primary_key=True) author = ormar.Integer( @@ -177,12 +158,12 @@ def test_wrong_backend(): def test_db_structure(): - assert Author.Meta.table.c.get("name").type.__class__ == EncryptedString + assert Author.ormar_config.table.c.get("name").type.__class__ == EncryptedString @pytest.mark.asyncio async def test_save_and_retrieve(): - async with database: + async with base_ormar_config.database: test_uuid = uuid.uuid4() await Author( name="Test", @@ -191,10 +172,12 @@ async def test_save_and_retrieve(): uuid_test=test_uuid, test_float=1.2, test_bool=True, - test_decimal=decimal.Decimal(3.5), + test_decimal=3.57, test_decimal2=decimal.Decimal(5.5), test_json=dict(aa=12), custom_backend="test12", + test_bytes=b"test", + test_b64bytes=b"test2", ).save() author = await Author.objects.get() @@ -214,14 +197,17 @@ async def test_save_and_retrieve(): assert author.test_float2 is None assert author.test_bigint == 0 assert author.test_json == {"aa": 12} - assert author.test_decimal == 3.5 + assert float(author.test_decimal) == 3.57 assert author.test_decimal2 == 5.5 assert author.custom_backend == "test12" + assert author.test_bytes == "test".encode("utf-8") + assert author.test_b64bytes == "dGVzdDI=" + assert base64.b64decode(author.test_b64bytes) == b"test2" @pytest.mark.asyncio async def test_fernet_filters_nomatch(): - async with database: + async with base_ormar_config.database: await Filter(name="test1").save() await Filter(name="test1").save() @@ -236,7 +222,7 @@ async def test_fernet_filters_nomatch(): @pytest.mark.asyncio async def test_hash_filters_works(): - async with database: + async with base_ormar_config.database: await Hash(name="test1").save() await Hash(name="test2").save() @@ -253,7 +239,7 @@ async def test_hash_filters_works(): @pytest.mark.asyncio async def test_related_model_fields_properly_decrypted(): - async with database: + async with base_ormar_config.database: hash1 = await Hash(name="test1").save() report = await Report.objects.create(name="Report1") await report.filters.create(name="test1", hash=hash1) diff --git a/tests/test_exclude_include_dict/test_complex_relation_tree_performance.py b/tests/test_exclude_include_dict/test_complex_relation_tree_performance.py index 561893c..322974c 100644 --- a/tests/test_exclude_include_dict/test_complex_relation_tree_performance.py +++ b/tests/test_exclude_include_dict/test_complex_relation_tree_performance.py @@ -1,114 +1,93 @@ from datetime import datetime from typing import List, Optional, Union -import databases +import ormar as orm import pydantic import pytest -import sqlalchemy -import ormar as orm +from tests.lifespan import init_tests +from tests.settings import create_config -from tests.settings import DATABASE_URL - -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() - - -class MainMeta(orm.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() class ChagenlogRelease(orm.Model): id: int = orm.Integer(name="id", primary_key=True) - class Meta(MainMeta): - tablename = "changelog_release" + ormar_config = base_ormar_config.copy(tablename="changelog_release") class CommitIssue(orm.Model): id: int = orm.Integer(name="id", primary_key=True) - class Meta(MainMeta): - tablename = "commit_issues" + ormar_config = base_ormar_config.copy(tablename="commit_issues") class CommitLabel(orm.Model): id: int = orm.Integer(name="id", primary_key=True) - class Meta(MainMeta): - tablename = "commit_label" + ormar_config = base_ormar_config.copy(tablename="commit_label") class MergeRequestCommit(orm.Model): id: int = orm.Integer(name="id", primary_key=True) - class Meta(MainMeta): - tablename = "merge_request_commits" + ormar_config = base_ormar_config.copy(tablename="merge_request_commits") class MergeRequestIssue(orm.Model): id: int = orm.Integer(name="id", primary_key=True) - class Meta(MainMeta): - tablename = "merge_request_issues" + ormar_config = base_ormar_config.copy(tablename="merge_request_issues") class MergeRequestLabel(orm.Model): id: int = orm.Integer(name="id", primary_key=True) - class Meta(MainMeta): - tablename = "merge_request_labels" + ormar_config = base_ormar_config.copy(tablename="merge_request_labels") class ProjectLabel(orm.Model): id: int = orm.Integer(name="id", primary_key=True) - class Meta(MainMeta): - tablename = "project_label" + ormar_config = base_ormar_config.copy(tablename="project_label") class PushCommit(orm.Model): id: int = orm.Integer(name="id", primary_key=True) - class Meta(MainMeta): - tablename = "push_commit" + ormar_config = base_ormar_config.copy(tablename="push_commit") class PushLabel(orm.Model): id: int = orm.Integer(name="id", primary_key=True) - class Meta(MainMeta): - tablename = "push_label" + ormar_config = base_ormar_config.copy(tablename="push_label") class TagCommit(orm.Model): id: int = orm.Integer(name="id", primary_key=True) - class Meta(MainMeta): - tablename = "tag_commits" + ormar_config = base_ormar_config.copy(tablename="tag_commits") class TagIssue(orm.Model): id: int = orm.Integer(name="id", primary_key=True) - class Meta(MainMeta): - tablename = "tag_issue" + ormar_config = base_ormar_config.copy(tablename="tag_issue") class TagLabel(orm.Model): id: int = orm.Integer(name="id", primary_key=True) - class Meta(MainMeta): - tablename = "tag_label" + ormar_config = base_ormar_config.copy(tablename="tag_label") class UserProject(orm.Model): id: int = orm.Integer(name="id", primary_key=True) access_level: int = orm.Integer(default=0) - class Meta(MainMeta): - tablename = "user_project" + ormar_config = base_ormar_config.copy(tablename="user_project") class Label(orm.Model): @@ -117,8 +96,7 @@ class Label(orm.Model): description: str = orm.Text(default="") type: str = orm.String(max_length=100, default="") - class Meta(MainMeta): - tablename = "labels" + ormar_config = base_ormar_config.copy(tablename="labels") class Project(orm.Model): @@ -139,8 +117,7 @@ class Project(orm.Model): changelog_file: str = orm.String(max_length=250, default="") version_file: str = orm.String(max_length=250, default="") - class Meta(MainMeta): - tablename = "projects" + ormar_config = base_ormar_config.copy(tablename="projects") class Issue(orm.Model): @@ -154,8 +131,7 @@ class Issue(orm.Model): change_type: str = orm.String(max_length=100, default="") data: pydantic.Json = orm.JSON(default={}) - class Meta(MainMeta): - tablename = "issues" + ormar_config = base_ormar_config.copy(tablename="issues") class User(orm.Model): @@ -163,8 +139,7 @@ class User(orm.Model): username: str = orm.String(max_length=100, unique=True) name: str = orm.String(max_length=200, default="") - class Meta(MainMeta): - tablename = "users" + ormar_config = base_ormar_config.copy(tablename="users") class Branch(orm.Model): @@ -177,8 +152,7 @@ class Branch(orm.Model): postfix_tag: str = orm.String(max_length=50, default="") project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE") - class Meta(MainMeta): - tablename = "branches" + ormar_config = base_ormar_config.copy(tablename="branches") class Changelog(orm.Model): @@ -192,8 +166,7 @@ class Changelog(orm.Model): project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE") created_date: datetime = orm.DateTime(default=datetime.utcnow()) - class Meta(MainMeta): - tablename = "changelogs" + ormar_config = base_ormar_config.copy(tablename="changelogs") class Commit(orm.Model): @@ -210,8 +183,7 @@ class Commit(orm.Model): Issue, through=CommitIssue, ondelete="CASCADE", onupdate="CASCADE" ) - class Meta(MainMeta): - tablename = "commits" + ormar_config = base_ormar_config.copy(tablename="commits") class MergeRequest(orm.Model): @@ -234,8 +206,7 @@ class MergeRequest(orm.Model): ) project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE") - class Meta(MainMeta): - tablename = "merge_requests" + ormar_config = base_ormar_config.copy(tablename="merge_requests") class Push(orm.Model): @@ -259,8 +230,7 @@ class Push(orm.Model): author: User = orm.ForeignKey(User, ondelete="CASCADE", onupdate="CASCADE") project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE") - class Meta(MainMeta): - tablename = "pushes" + ormar_config = base_ormar_config.copy(tablename="pushes") class Tag(orm.Model): @@ -291,8 +261,7 @@ class Tag(orm.Model): Branch, nullable=True, ondelete="CASCADE", onupdate="CASCADE" ) - class Meta(MainMeta): - tablename = "tags" + ormar_config = base_ormar_config.copy(tablename="tags") class Release(orm.Model): @@ -305,8 +274,7 @@ class Release(orm.Model): ) data: pydantic.Json = orm.JSON(default={}) - class Meta(MainMeta): - tablename = "releases" + ormar_config = base_ormar_config.copy(tablename="releases") class Webhook(orm.Model): @@ -328,18 +296,12 @@ class Webhook(orm.Model): error: str = orm.Text(default="") -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_very_complex_relation_map(): - async with database: + async with base_ormar_config.database: tags = [ {"id": 18, "name": "name-18", "ref": "ref-18"}, {"id": 17, "name": "name-17", "ref": "ref-17"}, @@ -349,19 +311,25 @@ async def test_very_complex_relation_map(): { "id": 9, "title": "prueba-2321", - "description": "\n\n### [v.1.3.0.0] - 2021-08-19\n#### Resolved Issues\n\n#### Task\n\n- Probar flujo de changelog Automatic Jira: [SAN-86](https://htech.atlassian.net/browse/SAN-86)\n\n Description: Se probara el flujo de changelog automatic. \n\n Changelog: Se agrega función para extraer texto del campo changelog del dashboard de Sanval y ponerlo directamente en el changelog.md del repositorio. \n\n\n \n\n", + "description": "\n" + "Description 1" + "\n", "data": {}, }, { "id": 8, "title": "prueba-123-prod", - "description": "\n\n### [v.1.2.0.0] - 2021-08-19\n#### Resolved Issues\n\n#### Task\n\n- Probar flujo de changelog Automatic Jira: [SAN-86](https://htech.atlassian.net/browse/SAN-86)\n\n Description: Se probara el flujo de changelog automatic. \n\n Changelog: Se agrega función para extraer texto del campo changelog del dashboard de Sanval y ponerlo directamente en el changelog.md del repositorio. \n\n\n \n\n", + "description": "\n" + "Description 2" + "\n", "data": {}, }, { "id": 6, "title": "prueba-3-2", - "description": "\n\n### [v.1.1.0.0] - 2021-07-29\n#### Resolved Issues\n\n#### Task\n\n- Probar flujo de changelog Automatic Jira: [SAN-86](https://htech.atlassian.net/browse/SAN-86)\n\n Description: Se probara el flujo de changelog automatic. \n\n Changelog: Se agrega función para extraer texto del campo changelog del dashboard de Sanval y ponerlo directamente en el changelog.md del repositorio. \n\n\n \n\n", + "description": "\n" + "Description 3" + "\n", "data": {}, }, ] @@ -373,45 +341,42 @@ async def test_very_complex_relation_map(): await Release(**pay, tag=saved_tags[ind]).save() releases = await Release.objects.order_by(Release.id.desc()).all() - dicts = [release.dict() for release in releases] + dicts = [release.model_dump() for release in releases] result = [ { "id": 9, "title": "prueba-2321", - "description": "\n\n### [v.1.3.0.0] - 2021-08-19\n#### Resolved Issues\n\n#### Task\n\n- Probar flujo de changelog Automatic Jira: [SAN-86](https://htech.atlassian.net/browse/SAN-86)\n\n Description: Se probara el flujo de changelog automatic. \n\n Changelog: Se agrega función para extraer texto del campo changelog del dashboard de Sanval y ponerlo directamente en el changelog.md del repositorio. \n\n\n \n\n", + "description": "\n" + "Description 1" + "\n", "data": {}, "tag": { "id": 18, - "taglabel": None, - "tagcommit": None, - "tagissue": None, }, "changelogs": [], }, { "id": 8, "title": "prueba-123-prod", - "description": "\n\n### [v.1.2.0.0] - 2021-08-19\n#### Resolved Issues\n\n#### Task\n\n- Probar flujo de changelog Automatic Jira: [SAN-86](https://htech.atlassian.net/browse/SAN-86)\n\n Description: Se probara el flujo de changelog automatic. \n\n Changelog: Se agrega función para extraer texto del campo changelog del dashboard de Sanval y ponerlo directamente en el changelog.md del repositorio. \n\n\n \n\n", + "description": "\n" + "Description 2" + "\n", "data": {}, "tag": { "id": 17, - "taglabel": None, - "tagcommit": None, - "tagissue": None, }, "changelogs": [], }, { "id": 6, "title": "prueba-3-2", - "description": "\n\n### [v.1.1.0.0] - 2021-07-29\n#### Resolved Issues\n\n#### Task\n\n- Probar flujo de changelog Automatic Jira: [SAN-86](https://htech.atlassian.net/browse/SAN-86)\n\n Description: Se probara el flujo de changelog automatic. \n\n Changelog: Se agrega función para extraer texto del campo changelog del dashboard de Sanval y ponerlo directamente en el changelog.md del repositorio. \n\n\n \n\n", + "description": "\n" + "Description 3" + "\n", "data": {}, "tag": { "id": 12, - "taglabel": None, - "tagcommit": None, - "tagissue": None, }, "changelogs": [], }, diff --git a/tests/test_exclude_include_dict/test_dumping_model_to_dict.py b/tests/test_exclude_include_dict/test_dumping_model_to_dict.py index 8b89e21..9c062cf 100644 --- a/tests/test_exclude_include_dict/test_dumping_model_to_dict.py +++ b/tests/test_exclude_include_dict/test_dumping_model_to_dict.py @@ -1,32 +1,23 @@ from typing import List, Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) +from tests.lifespan import init_tests +from tests.settings import create_config - -class MainMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Role(ormar.Model): - class Meta(MainMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=255, nullable=False) class User(ormar.Model): - class Meta(MainMeta): - tablename: str = "users" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) email: str = ormar.String(max_length=255, nullable=False) @@ -36,20 +27,14 @@ class User(ormar.Model): class Tier(ormar.Model): - class Meta: - tablename = "tiers" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -57,10 +42,7 @@ class Category(ormar.Model): class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -68,6 +50,9 @@ class Item(ormar.Model): created_by: Optional[User] = ormar.ForeignKey(User) +create_test_database = init_tests(base_ormar_config) + + @pytest.fixture(autouse=True, scope="module") def sample_data(): role = Role(name="User", id=1) @@ -90,13 +75,13 @@ def sample_data(): def test_dumping_to_dict_no_exclusion(sample_data): item1, item2 = sample_data - dict1 = item1.dict() + dict1 = item1.model_dump() assert dict1["name"] == "Teddy Bear" assert dict1["category"]["name"] == "Toys" assert dict1["category"]["tier"]["name"] == "Tier I" assert dict1["created_by"]["email"] == "test@test.com" - dict2 = item2.dict() + dict2 = item2.model_dump() assert dict2["name"] == "M16" assert dict2["category"]["name"] == "Weapons" assert dict2["created_by"]["email"] == "test@test.com" @@ -104,17 +89,17 @@ def test_dumping_to_dict_no_exclusion(sample_data): def test_dumping_to_dict_exclude_set(sample_data): item1, item2 = sample_data - dict3 = item2.dict(exclude={"name"}) + dict3 = item2.model_dump(exclude={"name"}) assert "name" not in dict3 assert dict3["category"]["name"] == "Weapons" assert dict3["created_by"]["email"] == "test@test.com" - dict4 = item2.dict(exclude={"category"}) + dict4 = item2.model_dump(exclude={"category"}) assert dict4["name"] == "M16" assert "category" not in dict4 assert dict4["created_by"]["email"] == "test@test.com" - dict5 = item2.dict(exclude={"category", "name"}) + dict5 = item2.model_dump(exclude={"category", "name"}) assert "name" not in dict5 assert "category" not in dict5 assert dict5["created_by"]["email"] == "test@test.com" @@ -122,7 +107,7 @@ def test_dumping_to_dict_exclude_set(sample_data): def test_dumping_to_dict_exclude_dict(sample_data): item1, item2 = sample_data - dict6 = item2.dict(exclude={"category": {"name"}, "name": ...}) + dict6 = item2.model_dump(exclude={"category": {"name"}, "name": ...}) assert "name" not in dict6 assert "category" in dict6 assert "name" not in dict6["category"] @@ -131,7 +116,7 @@ def test_dumping_to_dict_exclude_dict(sample_data): def test_dumping_to_dict_exclude_nested_dict(sample_data): item1, item2 = sample_data - dict1 = item2.dict(exclude={"category": {"tier": {"name"}}, "name": ...}) + dict1 = item2.model_dump(exclude={"category": {"tier": {"name"}}, "name": ...}) assert "name" not in dict1 assert "category" in dict1 assert dict1["category"]["name"] == "Weapons" @@ -141,7 +126,7 @@ def test_dumping_to_dict_exclude_nested_dict(sample_data): def test_dumping_to_dict_exclude_and_include_nested_dict(sample_data): item1, item2 = sample_data - dict1 = item2.dict( + dict1 = item2.model_dump( exclude={"category": {"tier": {"name"}}}, include={"name", "category"} ) assert dict1.get("name") == "M16" @@ -150,7 +135,7 @@ def test_dumping_to_dict_exclude_and_include_nested_dict(sample_data): assert "created_by" not in dict1 assert dict1["category"]["tier"].get("name") is None - dict2 = item1.dict( + dict2 = item1.model_dump( exclude={"id": ...}, include={"name": ..., "category": {"name": ..., "tier": {"id"}}}, ) @@ -164,25 +149,31 @@ def test_dumping_to_dict_exclude_and_include_nested_dict(sample_data): def test_dumping_dict_without_primary_keys(sample_data): item1, item2 = sample_data - dict1 = item2.dict(exclude_primary_keys=True) + dict1 = item2.model_dump(exclude_primary_keys=True) assert dict1 == { "category": {"name": "Weapons", "tier": {"name": "Tier I"}}, "created_by": { "email": "test@test.com", "first_name": "Anna", "password": "ijacids7^*&", - "roles": [{"name": "User"}, {"name": "Admin"}], + "roles": [ + {"name": "User"}, + {"name": "Admin"}, + ], }, "name": "M16", } - dict2 = item1.dict(exclude_primary_keys=True) + dict2 = item1.model_dump(exclude_primary_keys=True) assert dict2 == { "category": {"name": "Toys", "tier": {"name": "Tier I"}}, "created_by": { "email": "test@test.com", "first_name": "Anna", "password": "ijacids7^*&", - "roles": [{"name": "User"}, {"name": "Admin"}], + "roles": [ + {"name": "User"}, + {"name": "Admin"}, + ], }, "name": "Teddy Bear", } diff --git a/tests/test_exclude_include_dict/test_excludable_items.py b/tests/test_exclude_include_dict/test_excludable_items.py index 66d3b87..a4338f5 100644 --- a/tests/test_exclude_include_dict/test_excludable_items.py +++ b/tests/test_exclude_include_dict/test_excludable_items.py @@ -1,24 +1,16 @@ from typing import List, Optional -import databases -import sqlalchemy - import ormar from ormar.models.excludable import ExcludableItems -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() class NickNames(ormar.Model): - class Meta(BaseMeta): - tablename = "nicks" + ormar_config = base_ormar_config.copy(tablename="nicks") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -26,13 +18,11 @@ class NickNames(ormar.Model): class NicksHq(ormar.Model): - class Meta(BaseMeta): - tablename = "nicks_x_hq" + ormar_config = base_ormar_config.copy(tablename="nicks_x_hq") class HQ(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -40,8 +30,7 @@ class HQ(ormar.Model): class Company(ormar.Model): - class Meta(BaseMeta): - tablename = "companies" + ormar_config = base_ormar_config.copy(tablename="companies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="company_name") @@ -50,8 +39,7 @@ class Company(ormar.Model): class Car(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) manufacturer: Optional[Company] = ormar.ForeignKey(Company) @@ -62,6 +50,9 @@ class Car(ormar.Model): aircon_type: str = ormar.String(max_length=20, nullable=True) +create_test_database = init_tests(base_ormar_config) + + def compare_results(excludable): car_excludable = excludable.get(Car) assert car_excludable.exclude == {"year", "gearbox_type", "gears", "aircon_type"} @@ -69,7 +60,9 @@ def compare_results(excludable): assert car_excludable.is_excluded("year") - alias = Company.Meta.alias_manager.resolve_relation_alias(Car, "manufacturer") + alias = Company.ormar_config.alias_manager.resolve_relation_alias( + Car, "manufacturer" + ) manu_excludable = excludable.get(Company, alias=alias) assert manu_excludable.exclude == {"founded"} assert manu_excludable.include == set() @@ -78,7 +71,7 @@ def compare_results(excludable): def compare_results_include(excludable): - manager = Company.Meta.alias_manager + manager = Company.ormar_config.alias_manager car_excludable = excludable.get(Car) assert car_excludable.include == {"id", "name"} assert car_excludable.exclude == set() @@ -204,7 +197,9 @@ def test_includes_and_excludes_combo(): assert car_excludable.is_excluded("aircon_type") assert car_excludable.is_included("name") - alias = Company.Meta.alias_manager.resolve_relation_alias(Car, "manufacturer") + alias = Company.ormar_config.alias_manager.resolve_relation_alias( + Car, "manufacturer" + ) manu_excludable = excludable.get(Company, alias=alias) assert manu_excludable.include == {"name"} assert manu_excludable.exclude == {"founded"} diff --git a/tests/test_exclude_include_dict/test_excluding_fields_in_fastapi.py b/tests/test_exclude_include_dict/test_excluding_fields_in_fastapi.py index 1250c7c..cb2ce94 100644 --- a/tests/test_exclude_include_dict/test_excluding_fields_in_fastapi.py +++ b/tests/test_exclude_include_dict/test_excluding_fields_in_fastapi.py @@ -1,44 +1,28 @@ import datetime -import string import random +import string -import databases +import ormar import pydantic import pytest import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient +from ormar import post_save +from pydantic import ConfigDict, computed_field -import ormar -from ormar import post_save, property_field -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -app = FastAPI() -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) -app.state.database = database - - -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) # note that you can set orm_mode here # and in this case UserSchema become unnecessary class UserBase(pydantic.BaseModel): - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) email: str first_name: str @@ -51,8 +35,7 @@ class UserCreateSchema(UserBase): class UserSchema(UserBase): - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) def gen_pass(): @@ -61,10 +44,7 @@ def gen_pass(): class RandomModel(ormar.Model): - class Meta: - tablename: str = "random_users" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="random_users") id: int = ormar.Integer(primary_key=True) password: str = ormar.String(max_length=255, default=gen_pass) @@ -74,16 +54,13 @@ class RandomModel(ormar.Model): server_default=sqlalchemy.func.now() ) - @property_field + @computed_field def full_name(self) -> str: return " ".join([self.first_name, self.last_name]) class User(ormar.Model): - class Meta: - tablename: str = "users" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="users") id: int = ormar.Integer(primary_key=True) email: str = ormar.String(max_length=255) @@ -94,10 +71,7 @@ class User(ormar.Model): class User2(ormar.Model): - class Meta: - tablename: str = "users2" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="users2") id: int = ormar.Integer(primary_key=True) email: str = ormar.String(max_length=255, nullable=False) @@ -105,17 +79,10 @@ class User2(ormar.Model): first_name: str = ormar.String(max_length=255) last_name: str = ormar.String(max_length=255) category: str = ormar.String(max_length=255, nullable=True) - timestamp: datetime.datetime = ormar.DateTime( - pydantic_only=True, default=datetime.datetime.now - ) + timestamp: datetime.datetime = pydantic.Field(default=datetime.datetime.now) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @app.post("/users/", response_model=User, response_model_exclude={"password"}) @@ -126,7 +93,7 @@ async def create_user(user: User): @app.post("/users2/", response_model=User) async def create_user2(user: User): user = await user.save() - return user.dict(exclude={"password"}) + return user.model_dump(exclude={"password"}) @app.post("/users3/", response_model=UserBase) @@ -136,7 +103,7 @@ async def create_user3(user: User2): @app.post("/users4/") async def create_user4(user: User2): - return (await user.save()).dict(exclude={"password"}) + return (await user.save()).model_dump(exclude={"password"}) @app.post("/random/", response_model=RandomModel) @@ -276,12 +243,11 @@ async def test_adding_fields_in_endpoints2(): @pytest.mark.asyncio async def test_excluding_property_field_in_endpoints2(): - dummy_registry = {} @post_save(RandomModel) async def after_save(sender, instance, **kwargs): - dummy_registry[instance.pk] = instance.dict() + dummy_registry[instance.pk] = instance.model_dump() client = AsyncClient(app=app, base_url="http://testserver") async with client as client, LifespanManager(app): diff --git a/tests/test_exclude_include_dict/test_excluding_fields_with_default.py b/tests/test_exclude_include_dict/test_excluding_fields_with_default.py index f27274b..92dff49 100644 --- a/tests/test_exclude_include_dict/test_excluding_fields_with_default.py +++ b/tests/test_exclude_include_dict/test_excluding_fields_with_default.py @@ -1,15 +1,13 @@ import random from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() def get_position() -> int: @@ -17,10 +15,7 @@ def get_position() -> int: class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="albums") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -28,10 +23,7 @@ class Album(ormar.Model): class Track(ormar.Model): - class Meta: - tablename = "tracks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="tracks") id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -40,19 +32,13 @@ class Track(ormar.Model): play_count: int = ormar.Integer(nullable=True, default=0) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_excluding_field_with_default(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): album = await Album.objects.create(name="Miami") await Track.objects.create(title="Vice City", album=album, play_count=10) await Track.objects.create(title="Beach Sand", album=album, play_count=20) diff --git a/tests/test_exclude_include_dict/test_excluding_nested_models_lists.py b/tests/test_exclude_include_dict/test_excluding_nested_models_lists.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_exclude_include_dict/test_excluding_subset_of_columns.py b/tests/test_exclude_include_dict/test_excluding_subset_of_columns.py index dd9ac57..71fe6a9 100644 --- a/tests/test_exclude_include_dict/test_excluding_subset_of_columns.py +++ b/tests/test_exclude_include_dict/test_excluding_subset_of_columns.py @@ -1,22 +1,17 @@ from typing import Optional -import databases +import ormar import pydantic import pytest -import sqlalchemy -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class Company(ormar.Model): - class Meta: - tablename = "companies" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="companies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False) @@ -24,10 +19,7 @@ class Company(ormar.Model): class Car(ormar.Model): - class Meta: - tablename = "cars" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="cars") id: int = ormar.Integer(primary_key=True) manufacturer: Optional[Company] = ormar.ForeignKey(Company) @@ -38,19 +30,13 @@ class Car(ormar.Model): aircon_type: str = ormar.String(max_length=20, nullable=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_selecting_subset(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): toyota = await Company.objects.create(name="Toyota", founded=1937) await Car.objects.create( manufacturer=toyota, @@ -173,8 +159,62 @@ async def test_selecting_subset(): assert car.manufacturer.name == "Toyota" assert car.manufacturer.founded is None - with pytest.raises(pydantic.error_wrappers.ValidationError): + with pytest.raises(pydantic.ValidationError): # cannot exclude mandatory model columns - company__name in this example await Car.objects.select_related("manufacturer").exclude_fields( ["manufacturer__name"] ).all() + + +@pytest.mark.asyncio +async def test_excluding_nested_lists_in_dump(): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): + toyota = await Company.objects.create(name="Toyota", founded=1937) + car1 = await Car.objects.create( + manufacturer=toyota, + name="Corolla", + year=2020, + gearbox_type="Manual", + gears=5, + aircon_type="Manual", + ) + car2 = await Car.objects.create( + manufacturer=toyota, + name="Yaris", + year=2019, + gearbox_type="Manual", + gears=5, + aircon_type="Manual", + ) + manufacturer = await Company.objects.select_related("cars").get( + name="Toyota" + ) + assert manufacturer.model_dump() == { + "cars": [ + { + "aircon_type": "Manual", + "gearbox_type": "Manual", + "gears": 5, + "id": car1.id, + "name": "Corolla", + "year": 2020, + }, + { + "aircon_type": "Manual", + "gearbox_type": "Manual", + "gears": 5, + "id": car2.id, + "name": "Yaris", + "year": 2019, + }, + ], + "founded": 1937, + "id": toyota.id, + "name": "Toyota", + } + assert manufacturer.model_dump(exclude_list=True) == { + "founded": 1937, + "id": toyota.id, + "name": "Toyota", + } diff --git a/tests/test_exclude_include_dict/test_pydantic_dict_params.py b/tests/test_exclude_include_dict/test_pydantic_dict_params.py index 474580e..ca23f28 100644 --- a/tests/test_exclude_include_dict/test_pydantic_dict_params.py +++ b/tests/test_exclude_include_dict/test_pydantic_dict_params.py @@ -1,21 +1,16 @@ from typing import List -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, default="Test", nullable=True) @@ -23,10 +18,7 @@ class Category(ormar.Model): class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="items") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -34,49 +26,44 @@ class Item(ormar.Model): categories: List[Category] = ormar.ManyToMany(Category) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_exclude_default(): - async with database: + async with base_ormar_config.database: category = Category() - assert category.dict() == { + assert category.model_dump() == { "id": None, "items": [], "name": "Test", "visibility": True, } - assert category.dict(exclude_defaults=True) == {"items": []} + assert category.model_dump(exclude_defaults=True) == {"items": []} await category.save() category2 = await Category.objects.get() - assert category2.dict() == { + assert category2.model_dump() == { "id": 1, "items": [], "name": "Test", "visibility": True, } - assert category2.dict(exclude_defaults=True) == {"id": 1, "items": []} - assert category2.json(exclude_defaults=True) == '{"id": 1, "items": []}' + assert category2.model_dump(exclude_defaults=True) == {"id": 1, "items": []} + assert category2.model_dump_json(exclude_defaults=True) == '{"id":1,"items":[]}' @pytest.mark.asyncio async def test_exclude_none(): - async with database: + async with base_ormar_config.database: category = Category(id=2, name=None) - assert category.dict() == { + assert category.model_dump() == { "id": 2, "items": [], "name": None, "visibility": True, } - assert category.dict(exclude_none=True) == { + assert category.model_dump(exclude_none=True) == { "id": 2, "items": [], "visibility": True, @@ -84,34 +71,34 @@ async def test_exclude_none(): await category.save() category2 = await Category.objects.get() - assert category2.dict() == { + assert category2.model_dump() == { "id": 2, "items": [], "name": None, "visibility": True, } - assert category2.dict(exclude_none=True) == { + assert category2.model_dump(exclude_none=True) == { "id": 2, "items": [], "visibility": True, } assert ( - category2.json(exclude_none=True) - == '{"id": 2, "visibility": true, "items": []}' + category2.model_dump_json(exclude_none=True) + == '{"id":2,"visibility":true,"items":[]}' ) @pytest.mark.asyncio async def test_exclude_unset(): - async with database: + async with base_ormar_config.database: category = Category(id=3, name="Test 2") - assert category.dict() == { + assert category.model_dump() == { "id": 3, "items": [], "name": "Test 2", "visibility": True, } - assert category.dict(exclude_unset=True) == { + assert category.model_dump(exclude_unset=True) == { "id": 3, "items": [], "name": "Test 2", @@ -119,7 +106,7 @@ async def test_exclude_unset(): await category.save() category2 = await Category.objects.get() - assert category2.dict() == { + assert category2.model_dump() == { "id": 3, "items": [], "name": "Test 2", @@ -127,7 +114,7 @@ async def test_exclude_unset(): } # NOTE how after loading from db all fields are set explicitly # as this is what happens when you populate a model from db - assert category2.dict(exclude_unset=True) == { + assert category2.model_dump(exclude_unset=True) == { "id": 3, "items": [], "name": "Test 2", diff --git a/tests/test_fastapi/test_binary_fields.py b/tests/test_fastapi/test_binary_fields.py index 39e82a3..41343c6 100644 --- a/tests/test_fastapi/test_binary_fields.py +++ b/tests/test_fastapi/test_binary_fields.py @@ -1,63 +1,44 @@ import base64 -import json import uuid +from enum import Enum from typing import List -import databases +import ormar import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient -import ormar -from tests.settings import DATABASE_URL - -app = FastAPI() - -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() -app.state.database = database +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config headers = {"content-type": "application/json"} +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() - - -blob3 = b"\xc3\x28" +blob3 = b"\xc3\x83\x28" blob4 = b"\xf0\x28\x8c\x28" blob5 = b"\xee" blob6 = b"\xff" -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +class BinaryEnum(Enum): + blob3 = blob3 + blob4 = blob4 + blob5 = blob5 + blob6 = blob6 class BinaryThing(ormar.Model): - class Meta(BaseMeta): - tablename = "things" + ormar_config = base_ormar_config.copy(tablename="things") id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4) name: str = ormar.Text(default="") - bt: str = ormar.LargeBinary( - max_length=1000, - choices=[blob3, blob4, blob5, blob6], - represent_as_base64_str=True, - ) + bt: str = ormar.LargeBinary(represent_as_base64_str=True, max_length=100) + + +create_test_database = init_tests(base_ormar_config) @app.get("/things", response_model=List[BinaryThing]) @@ -71,14 +52,6 @@ async def create_things(thing: BinaryThing): return thing -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) - - @pytest.mark.asyncio async def test_read_main(): client = AsyncClient(app=app, base_url="http://testserver") @@ -90,17 +63,15 @@ async def test_read_main(): ) assert response.status_code == 200 response = await client.get("/things") - assert response.json()[0]["bt"] == base64.b64encode(blob3).decode() - thing = BinaryThing(**response.json()[0]) + assert response.json()[0]["bt"] == blob3.decode() + resp_json = response.json() + resp_json[0]["bt"] = resp_json[0]["bt"].encode() + thing = BinaryThing(**resp_json[0]) assert thing.__dict__["bt"] == blob3 + assert thing.bt == base64.b64encode(blob3).decode() def test_schema(): - schema = BinaryThing.schema() + schema = BinaryThing.model_json_schema() assert schema["properties"]["bt"]["format"] == "base64" - converted_choices = ["7g==", "/w==", "8CiMKA==", "wyg="] - assert len(schema["properties"]["bt"]["enum"]) == 4 - assert all( - choice in schema["properties"]["bt"]["enum"] for choice in converted_choices - ) assert schema["example"]["bt"] == "string" diff --git a/tests/test_fastapi/test_choices_schema.py b/tests/test_fastapi/test_choices_schema.py deleted file mode 100644 index dda3306..0000000 --- a/tests/test_fastapi/test_choices_schema.py +++ /dev/null @@ -1,151 +0,0 @@ -import datetime -import decimal -import uuid -from enum import Enum - -import databases -import pydantic -import pytest -import sqlalchemy -from asgi_lifespan import LifespanManager -from fastapi import FastAPI -from httpx import AsyncClient - -import ormar -from tests.settings import DATABASE_URL - -app = FastAPI() -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() -app.state.database = database - -uuid1 = uuid.uuid4() -uuid2 = uuid.uuid4() - - -blob = b"test" -blob2 = b"test2icac89uc98" - - -class EnumTest(Enum): - val1 = "Val1" - val2 = "Val2" - - -class Organisation(ormar.Model): - class Meta: - tablename = "org" - metadata = metadata - database = database - - id: int = ormar.Integer(primary_key=True) - ident: str = ormar.String(max_length=100, choices=["ACME Ltd", "Other ltd"]) - priority: int = ormar.Integer(choices=[1, 2, 3, 4, 5]) - priority2: int = ormar.BigInteger(choices=[1, 2, 3, 4, 5]) - priority3: int = ormar.SmallInteger(choices=[1, 2, 3, 4, 5]) - expire_date: datetime.date = ormar.Date( - choices=[datetime.date(2021, 1, 1), datetime.date(2022, 5, 1)] - ) - expire_time: datetime.time = ormar.Time( - choices=[datetime.time(10, 0, 0), datetime.time(12, 30)] - ) - - expire_datetime: datetime.datetime = ormar.DateTime( - choices=[ - datetime.datetime(2021, 1, 1, 10, 0, 0), - datetime.datetime(2022, 5, 1, 12, 30), - ] - ) - random_val: float = ormar.Float(choices=[2.0, 3.5]) - random_decimal: decimal.Decimal = ormar.Decimal( - scale=2, precision=4, choices=[decimal.Decimal(12.4), decimal.Decimal(58.2)] - ) - random_json: pydantic.Json = ormar.JSON(choices=["aa", '{"aa": "bb"}']) - random_uuid: uuid.UUID = ormar.UUID(choices=[uuid1, uuid2]) - enum_string: str = ormar.String(max_length=100, choices=list(EnumTest)) - blob_col: bytes = ormar.LargeBinary(max_length=100000, choices=[blob, blob2]) - - -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() - - -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) - - -@app.post("/items/", response_model=Organisation) -async def create_item(item: Organisation): - await item.save() - return item - - -@pytest.mark.asyncio -async def test_all_endpoints(): - client = AsyncClient(app=app, base_url="http://testserver") - async with client as client, LifespanManager(app): - response = await client.post( - "/items/", - json={"id": 1, "ident": "", "priority": 4, "expire_date": "2022-05-01"}, - ) - - assert response.status_code == 422 - response = await client.post( - "/items/", - json={ - "id": 1, - "ident": "ACME Ltd", - "priority": 4, - "priority2": 2, - "priority3": 1, - "expire_date": "2022-05-01", - "expire_time": "10:00:00", - "expire_datetime": "2022-05-01T12:30:00", - "random_val": 3.5, - "random_decimal": 12.4, - "random_json": '{"aa": "bb"}', - "random_uuid": str(uuid1), - "enum_string": EnumTest.val1.value, - "blob_col": blob.decode("utf-8"), - }, - ) - assert response.status_code == 200 - item = Organisation(**response.json()) - assert item.pk is not None - response = await client.get("/docs") - assert response.status_code == 200 - assert b"FastAPI - Swagger UI" in response.content - - -def test_schema_modification(): - schema = Organisation.schema() - for field in ["ident", "priority", "expire_date"]: - assert field in schema["properties"] - assert schema["properties"].get(field).get("enum") == list( - Organisation.Meta.model_fields.get(field).choices - ) - assert "An enumeration." in schema["properties"].get(field).get("description") - - -def test_schema_gen(): - schema = app.openapi() - assert "Organisation" in schema["components"]["schemas"] - props = schema["components"]["schemas"]["Organisation"]["properties"] - for field in [k for k in Organisation.Meta.model_fields.keys() if k != "id"]: - assert "enum" in props.get(field) - assert "description" in props.get(field) - assert "An enumeration." in props.get(field).get("description") diff --git a/tests/test_fastapi/test_docs_with_multiple_relations_to_one.py b/tests/test_fastapi/test_docs_with_multiple_relations_to_one.py index 7aae2fe..43df072 100644 --- a/tests/test_fastapi/test_docs_with_multiple_relations_to_one.py +++ b/tests/test_fastapi/test_docs_with_multiple_relations_to_one.py @@ -1,37 +1,28 @@ from typing import Optional from uuid import UUID, uuid4 -import databases +import ormar import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient -import ormar +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -app = FastAPI() -DATABASE_URL = "sqlite:///db.sqlite" -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) class CA(ormar.Model): - class Meta(BaseMeta): - tablename = "cas" + ormar_config = base_ormar_config.copy(tablename="cas") id: UUID = ormar.UUID(primary_key=True, default=uuid4) ca_name: str = ormar.Text(default="") class CB1(ormar.Model): - class Meta(BaseMeta): - tablename = "cb1s" + ormar_config = base_ormar_config.copy(tablename="cb1s") id: UUID = ormar.UUID(primary_key=True, default=uuid4) cb1_name: str = ormar.Text(default="") @@ -39,14 +30,16 @@ class CB1(ormar.Model): class CB2(ormar.Model): - class Meta(BaseMeta): - tablename = "cb2s" + ormar_config = base_ormar_config.copy(tablename="cb2s") id: UUID = ormar.UUID(primary_key=True, default=uuid4) cb2_name: str = ormar.Text(default="") ca2: Optional[CA] = ormar.ForeignKey(CA, nullable=True) +create_test_database = init_tests(base_ormar_config) + + @app.get("/ca", response_model=CA) async def get_ca(): # pragma: no cover return None @@ -70,6 +63,7 @@ async def test_all_endpoints(): assert response.status_code == 200, response.text schema = response.json() components = schema["components"]["schemas"] - assert all(x in components for x in ["CA", "CB1", "CB2"]) - pk_onlys = [x for x in list(components.keys()) if x.startswith("PkOnly")] - assert len(pk_onlys) == 2 + raw_names_w_o_modules = [x.split("__")[-1] for x in components.keys()] + assert all(x in raw_names_w_o_modules for x in ["CA", "CB1", "CB2"]) + pk_onlys = [x for x in list(raw_names_w_o_modules) if x.startswith("PkOnly")] + assert len(pk_onlys) == 4 diff --git a/tests/test_fastapi/test_enum_schema.py b/tests/test_fastapi/test_enum_schema.py index 6fa3ce1..da1f7c0 100644 --- a/tests/test_fastapi/test_enum_schema.py +++ b/tests/test_fastapi/test_enum_schema.py @@ -1,13 +1,11 @@ from enum import Enum -import databases -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class MyEnum(Enum): @@ -16,18 +14,17 @@ class MyEnum(Enum): class EnumExample(ormar.Model): - class Meta: - tablename = "enum_example" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="enum_example") id: int = ormar.Integer(primary_key=True) size: MyEnum = ormar.Enum(enum_class=MyEnum, default=MyEnum.SMALL) +create_test_database = init_tests(base_ormar_config) + + def test_proper_schema(): - schema = EnumExample.schema_json() - assert ( - '{"MyEnum": {"title": "MyEnum", "description": "An enumeration.", ' - '"enum": [1, 2]}}' in schema - ) + schema = EnumExample.model_json_schema() + assert {"MyEnum": {"title": "MyEnum", "enum": [1, 2], "type": "integer"}} == schema[ + "$defs" + ] diff --git a/tests/test_fastapi/test_excludes_with_get_pydantic.py b/tests/test_fastapi/test_excludes_with_get_pydantic.py index 26cd971..73727ef 100644 --- a/tests/test_fastapi/test_excludes_with_get_pydantic.py +++ b/tests/test_fastapi/test_excludes_with_get_pydantic.py @@ -1,45 +1,49 @@ +from typing import ForwardRef, Optional + +import ormar import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient -from tests.settings import DATABASE_URL -from tests.test_inheritance_and_pydantic_generation.test_geting_pydantic_models import ( - Category, - SelfRef, - database, - metadata, -) # type: ignore +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -app = FastAPI() -app.state.database = database +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() +class SelfRef(ormar.Model): + ormar_config = base_ormar_config.copy(tablename="self_refs") + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100, default="selfref") + parent = ormar.ForeignKey(ForwardRef("SelfRef"), related_name="children") -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() +SelfRef.update_forward_refs() -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +class Category(ormar.Model): + ormar_config = base_ormar_config.copy(tablename="categories") + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + + +class Item(ormar.Model): + ormar_config = base_ormar_config.copy() + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100, default="test") + category: Optional[Category] = ormar.ForeignKey(Category, nullable=True) + + +create_test_database = init_tests(base_ormar_config) async def create_category(category: Category): - return await Category(**category.dict()).save() + return await Category(**category.model_dump()).save() create_category.__annotations__["category"] = Category.get_pydantic(exclude={"id"}) @@ -55,7 +59,7 @@ async def create_selfref( exclude={"children__name"} # noqa: F821 ), ): - selfr = SelfRef(**selfref.dict()) + selfr = SelfRef(**selfref.model_dump()) await selfr.save() if selfr.children: for child in selfr.children: @@ -107,12 +111,12 @@ async def test_read_main(): assert self_ref.id == 3 assert self_ref.name == "test3" assert self_ref.parent is None - assert self_ref.children[0].dict() == {"id": 4} + assert self_ref.children[0].model_dump() == {"id": 4} response = await client.get("/selfrefs/3/") assert response.status_code == 200 check_children = SelfRef(**response.json()) - assert check_children.children[0].dict() == { + assert check_children.children[0].model_dump() == { "children": [], "id": 4, "name": "selfref", @@ -122,9 +126,19 @@ async def test_read_main(): response = await client.get("/selfrefs/2/") assert response.status_code == 200 check_children = SelfRef(**response.json()) - assert check_children.dict() == { + assert check_children.model_dump() == { "children": [], "id": 2, "name": "test2", "parent": {"id": 1}, } + + response = await client.get("/selfrefs/1/") + assert response.status_code == 200 + check_children = SelfRef(**response.json()) + assert check_children.model_dump() == { + "children": [{"id": 2, "name": "test2"}], + "id": 1, + "name": "test", + "parent": None, + } diff --git a/tests/test_fastapi/test_excluding_fields.py b/tests/test_fastapi/test_excluding_fields.py index 7acf896..c4f9511 100644 --- a/tests/test_fastapi/test_excluding_fields.py +++ b/tests/test_fastapi/test_excluding_fields.py @@ -1,62 +1,34 @@ from typing import List -import databases +import ormar import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -app = FastAPI() -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) -app.state.database = database - - -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="items") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) categories: List[Category] = ormar.ManyToMany(Category) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @app.post("/items/", response_model=Item) @@ -68,40 +40,26 @@ async def create_item(item: Item): @app.get("/items/{item_id}") async def get_item(item_id: int): item = await Item.objects.select_related("categories").get(pk=item_id) - return item.dict(exclude_primary_keys=True, exclude_through_models=True) + return item.model_dump(exclude_primary_keys=True, exclude_through_models=True) @app.get("/categories/{category_id}") async def get_category(category_id: int): category = await Category.objects.select_related("items").get(pk=category_id) - return category.dict(exclude_primary_keys=True) + return category.model_dump(exclude_primary_keys=True) @app.get("/categories/nt/{category_id}") async def get_category_no_through(category_id: int): category = await Category.objects.select_related("items").get(pk=category_id) - return category.dict(exclude_through_models=True) + result = category.model_dump(exclude_through_models=True) + return result @app.get("/categories/ntp/{category_id}") async def get_category_no_pk_through(category_id: int): category = await Category.objects.select_related("items").get(pk=category_id) - return category.dict(exclude_through_models=True, exclude_primary_keys=True) - - -@app.get( - "/items/fex/{item_id}", - response_model=Item, - response_model_exclude={ - "id", - "categories__id", - "categories__itemcategory", - "categories__items", - }, -) -async def get_item_excl(item_id: int): - item = await Item.objects.select_all().get(pk=item_id) - return item + return category.model_dump(exclude_through_models=True, exclude_primary_keys=True) @pytest.mark.asyncio @@ -120,9 +78,6 @@ async def test_all_endpoints(): no_pk_item = (await client.get(f"/items/{item_check.id}")).json() assert no_pk_item == item - no_pk_item2 = (await client.get(f"/items/fex/{item_check.id}")).json() - assert no_pk_item2 == item - no_pk_category = ( await client.get(f"/categories/{item_check.categories[0].id}") ).json() diff --git a/tests/test_fastapi/test_extra_ignore_parameter.py b/tests/test_fastapi/test_extra_ignore_parameter.py index 967f7b8..183ee27 100644 --- a/tests/test_fastapi/test_extra_ignore_parameter.py +++ b/tests/test_fastapi/test_extra_ignore_parameter.py @@ -1,52 +1,25 @@ -import json - -import databases +import ormar import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient - -import ormar from ormar import Extra -from tests.settings import DATABASE_URL -app = FastAPI() -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) -app.state.database = database +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config - -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) class Item(ormar.Model): - class Meta: - database = database - metadata = metadata - extra = Extra.ignore + ormar_config = base_ormar_config.copy(extra=Extra.ignore) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @app.post("/item/", response_model=Item) diff --git a/tests/test_fastapi/test_fastapi_docs.py b/tests/test_fastapi/test_fastapi_docs.py index c586216..ca1d892 100644 --- a/tests/test_fastapi/test_fastapi_docs.py +++ b/tests/test_fastapi/test_fastapi_docs.py @@ -1,40 +1,19 @@ import datetime -from typing import List, Optional +from typing import List, Optional, Union -import databases +import ormar import pydantic import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient +from pydantic import Field -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -app = FastAPI() -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) -app.state.database = database - - -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() - - -class LocalMeta: - metadata = metadata - database = database +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) class PTestA(pydantic.BaseModel): @@ -49,35 +28,31 @@ class PTestP(pydantic.BaseModel): class Category(ormar.Model): - class Meta(LocalMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Item(ormar.Model): - class Meta(LocalMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) - pydantic_int: Optional[int] - test_P: Optional[List[PTestP]] + pydantic_int: Optional[int] = None + test_P: List[PTestP] = Field(default_factory=list) + test_P_or_A: Union[int, str, None] = None categories = ormar.ManyToMany(Category) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @app.get("/items/", response_model=List[Item]) async def get_items(): items = await Item.objects.select_related("categories").all() + for item in items: + item.test_P_or_A = 2 return items @@ -104,26 +79,35 @@ async def test_all_endpoints(): client = AsyncClient(app=app, base_url="http://testserver") async with client as client, LifespanManager(app): response = await client.post("/categories/", json={"name": "test cat"}) + assert response.status_code == 200 category = response.json() response = await client.post("/categories/", json={"name": "test cat2"}) + assert response.status_code == 200 category2 = response.json() - response = await client.post("/items/", json={"name": "test", "id": 1}) + response = await client.post( + "/items/", json={"name": "test", "id": 1, "test_P_or_A": 0} + ) + assert response.status_code == 200 item = Item(**response.json()) assert item.pk is not None response = await client.post( - "/items/add_category/", json={"item": item.dict(), "category": category} + "/items/add_category/", + json={"item": item.model_dump(), "category": category}, ) + assert response.status_code == 200 item = Item(**response.json()) assert len(item.categories) == 1 assert item.categories[0].name == "test cat" await client.post( - "/items/add_category/", json={"item": item.dict(), "category": category2} + "/items/add_category/", + json={"item": item.model_dump(), "category": category2}, ) response = await client.get("/items/") + assert response.status_code == 200 items = [Item(**item) for item in response.json()] assert items[0] == item assert len(items[0].categories) == 2 @@ -136,7 +120,7 @@ async def test_all_endpoints(): def test_schema_modification(): - schema = Item.schema() + schema = Item.model_json_schema() assert any( x.get("type") == "array" for x in schema["properties"]["categories"]["anyOf"] ) @@ -147,10 +131,11 @@ def test_schema_modification(): "name": "string", "pydantic_int": 0, "test_P": [{"a": 0, "b": {"c": "string", "d": "string", "e": "string"}}], + "test_P_or_A": (0, "string"), } - schema = Category.schema() - assert schema["example"] == { + schema = Category.model_json_schema() + assert schema["$defs"]["Category"]["example"] == { "id": 0, "name": "string", "items": [ @@ -161,6 +146,7 @@ def test_schema_modification(): "test_P": [ {"a": 0, "b": {"c": "string", "d": "string", "e": "string"}} ], + "test_P_or_A": (0, "string"), } ], } @@ -169,4 +155,6 @@ def test_schema_modification(): def test_schema_gen(): schema = app.openapi() assert "Category" in schema["components"]["schemas"] - assert "Item" in schema["components"]["schemas"] + subschemas = [x.split("__")[-1] for x in schema["components"]["schemas"]] + assert "Item-Input" in subschemas + assert "Item-Output" in subschemas diff --git a/tests/test_fastapi/test_fastapi_usage.py b/tests/test_fastapi/test_fastapi_usage.py index 8a83105..ec32de1 100644 --- a/tests/test_fastapi/test_fastapi_usage.py +++ b/tests/test_fastapi/test_fastapi_usage.py @@ -1,42 +1,36 @@ from typing import Optional -import databases +import ormar import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -app = FastAPI() - -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="items") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) category: Optional[Category] = ormar.ForeignKey(Category, nullable=True) +create_test_database = init_tests(base_ormar_config) + + @app.post("/items/", response_model=Item) async def create_item(item: Item): return item @@ -53,7 +47,6 @@ async def test_read_main(): assert response.json() == { "category": { "id": None, - "items": [{"id": 1, "name": "test"}], "name": "test cat", }, "id": 1, @@ -61,3 +54,4 @@ async def test_read_main(): } item = Item(**response.json()) assert item.id == 1 + assert item.category.items[0].id == 1 diff --git a/tests/test_fastapi/test_inheritance_concrete_fastapi.py b/tests/test_fastapi/test_inheritance_concrete_fastapi.py index 817e306..8a582fb 100644 --- a/tests/test_fastapi/test_inheritance_concrete_fastapi.py +++ b/tests/test_fastapi/test_inheritance_concrete_fastapi.py @@ -1,41 +1,139 @@ import datetime -from typing import List +from typing import List, Optional +import ormar import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient +from ormar.relations.relation_proxy import RelationProxy +from pydantic import computed_field -from tests.settings import DATABASE_URL -from tests.test_inheritance_and_pydantic_generation.test_inheritance_concrete import ( # type: ignore - Category, - Subject, - Person, - Bus, - Truck, - Bus2, - Truck2, - db as database, - metadata, -) +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -app = FastAPI() -app.state.database = database +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() +class AuditModel(ormar.Model): + ormar_config = base_ormar_config.copy(abstract=True) + + created_by: str = ormar.String(max_length=100) + updated_by: str = ormar.String(max_length=100, default="Sam") + + @computed_field + def audit(self) -> str: # pragma: no cover + return f"{self.created_by} {self.updated_by}" -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() +class DateFieldsModelNoSubclass(ormar.Model): + ormar_config = base_ormar_config.copy(tablename="test_date_models") + + date_id: int = ormar.Integer(primary_key=True) + created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now) + updated_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now) + + +class DateFieldsModel(ormar.Model): + ormar_config = base_ormar_config.copy( + abstract=True, + constraints=[ + ormar.fields.constraints.UniqueColumns( + "creation_date", + "modification_date", + ), + ormar.fields.constraints.CheckColumns( + "creation_date <= modification_date", + ), + ], + ) + + created_date: datetime.datetime = ormar.DateTime( + default=datetime.datetime.now, name="creation_date" + ) + updated_date: datetime.datetime = ormar.DateTime( + default=datetime.datetime.now, name="modification_date" + ) + + +class Person(ormar.Model): + ormar_config = base_ormar_config.copy() + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + + +class Car(ormar.Model): + ormar_config = base_ormar_config.copy(abstract=True) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=50) + owner: Person = ormar.ForeignKey(Person) + co_owner: Person = ormar.ForeignKey(Person, related_name="coowned") + created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now) + + +class Car2(ormar.Model): + ormar_config = base_ormar_config.copy(abstract=True) + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=50) + owner: Person = ormar.ForeignKey(Person, related_name="owned") + co_owners: RelationProxy[Person] = ormar.ManyToMany(Person, related_name="coowned") + created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now) + + +class Bus(Car): + ormar_config = base_ormar_config.copy(tablename="buses") + + owner: Person = ormar.ForeignKey(Person, related_name="buses") + max_persons: int = ormar.Integer() + + +class Bus2(Car2): + ormar_config = base_ormar_config.copy(tablename="buses2") + + max_persons: int = ormar.Integer() + + +class Category(DateFieldsModel, AuditModel): + ormar_config = base_ormar_config.copy(tablename="categories") + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=50, unique=True, index=True) + code: int = ormar.Integer() + + @computed_field + def code_name(self) -> str: + return f"{self.code}:{self.name}" + + @computed_field + def audit(self) -> str: + return f"{self.created_by} {self.updated_by}" + + +class Subject(DateFieldsModel): + ormar_config = base_ormar_config.copy() + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=50, unique=True, index=True) + category: Optional[Category] = ormar.ForeignKey(Category) + + +class Truck(Car): + ormar_config = base_ormar_config.copy() + + max_capacity: int = ormar.Integer() + + +class Truck2(Car2): + ormar_config = base_ormar_config.copy(tablename="trucks2") + + max_capacity: int = ormar.Integer() + + +create_test_database = init_tests(base_ormar_config) @app.post("/subjects/", response_model=Subject) @@ -111,14 +209,6 @@ async def add_truck_coowner(item_id: int, person: Person): return truck -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) - - @pytest.mark.asyncio async def test_read_main(): client = AsyncClient(app=app, base_url="http://testserver") @@ -134,7 +224,7 @@ async def test_read_main(): assert cat.created_date is not None assert cat.id == 1 - cat_dict = cat.dict() + cat_dict = cat.model_dump() cat_dict["updated_date"] = cat_dict["updated_date"].strftime( "%Y-%m-%d %H:%M:%S.%f" ) @@ -160,11 +250,14 @@ async def test_inheritance_with_relation(): truck_dict = dict( name="Shelby wanna be", max_capacity=1400, - owner=sam.dict(), - co_owner=joe.dict(), + owner=sam.model_dump(), + co_owner=joe.model_dump(), ) bus_dict = dict( - name="Unicorn", max_persons=50, owner=sam.dict(), co_owner=joe.dict() + name="Unicorn", + max_persons=50, + owner=sam.model_dump(), + co_owner=joe.model_dump(), ) unicorn = Bus(**(await client.post("/buses/", json=bus_dict)).json()) shelby = Truck(**(await client.post("/trucks/", json=truck_dict)).json()) @@ -201,21 +294,25 @@ async def test_inheritance_with_m2m_relation(): joe = Person(**(await client.post("/persons/", json={"name": "Joe"})).json()) alex = Person(**(await client.post("/persons/", json={"name": "Alex"})).json()) - truck_dict = dict(name="Shelby wanna be", max_capacity=2000, owner=sam.dict()) - bus_dict = dict(name="Unicorn", max_persons=80, owner=sam.dict()) + truck_dict = dict( + name="Shelby wanna be", max_capacity=2000, owner=sam.model_dump() + ) + bus_dict = dict(name="Unicorn", max_persons=80, owner=sam.model_dump()) unicorn = Bus2(**(await client.post("/buses2/", json=bus_dict)).json()) shelby = Truck2(**(await client.post("/trucks2/", json=truck_dict)).json()) unicorn = Bus2( **( - await client.post(f"/buses2/{unicorn.pk}/add_coowner/", json=joe.dict()) + await client.post( + f"/buses2/{unicorn.pk}/add_coowner/", json=joe.model_dump() + ) ).json() ) unicorn = Bus2( **( await client.post( - f"/buses2/{unicorn.pk}/add_coowner/", json=alex.dict() + f"/buses2/{unicorn.pk}/add_coowner/", json=alex.model_dump() ) ).json() ) @@ -232,11 +329,13 @@ async def test_inheritance_with_m2m_relation(): assert unicorn.co_owners[1] == alex assert unicorn.max_persons == 80 - await client.post(f"/trucks2/{shelby.pk}/add_coowner/", json=alex.dict()) + await client.post(f"/trucks2/{shelby.pk}/add_coowner/", json=alex.model_dump()) shelby = Truck2( **( - await client.post(f"/trucks2/{shelby.pk}/add_coowner/", json=joe.dict()) + await client.post( + f"/trucks2/{shelby.pk}/add_coowner/", json=joe.model_dump() + ) ).json() ) diff --git a/tests/test_fastapi/test_inheritance_mixins_fastapi.py b/tests/test_fastapi/test_inheritance_mixins_fastapi.py index c1404d6..2c8c183 100644 --- a/tests/test_fastapi/test_inheritance_mixins_fastapi.py +++ b/tests/test_fastapi/test_inheritance_mixins_fastapi.py @@ -1,30 +1,46 @@ import datetime +from typing import Optional +import ormar import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient -from tests.settings import DATABASE_URL -from tests.test_inheritance_and_pydantic_generation.test_inheritance_mixins import Category, Subject, metadata, db as database # type: ignore +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -app = FastAPI() -app.state.database = database +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() +class AuditMixin: + created_by: str = ormar.String(max_length=100) + updated_by: str = ormar.String(max_length=100, default="Sam") -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() +class DateFieldsMixins: + created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now) + updated_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now) + + +class Category(ormar.Model, DateFieldsMixins, AuditMixin): + ormar_config = base_ormar_config.copy(tablename="categories") + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=50, unique=True, index=True) + code: int = ormar.Integer() + + +class Subject(ormar.Model, DateFieldsMixins): + ormar_config = base_ormar_config.copy(tablename="subjects") + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=50, unique=True, index=True) + category: Optional[Category] = ormar.ForeignKey(Category) + + +create_test_database = init_tests(base_ormar_config) @app.post("/subjects/", response_model=Subject) @@ -38,14 +54,6 @@ async def create_category(category: Category): return category -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) - - @pytest.mark.asyncio async def test_read_main(): client = AsyncClient(app=app, base_url="http://testserver") @@ -61,7 +69,7 @@ async def test_read_main(): assert cat.created_date is not None assert cat.id == 1 - cat_dict = cat.dict() + cat_dict = cat.model_dump() cat_dict["updated_date"] = cat_dict["updated_date"].strftime( "%Y-%m-%d %H:%M:%S.%f" ) diff --git a/tests/test_fastapi/test_json_field_fastapi.py b/tests/test_fastapi/test_json_field_fastapi.py index cd883ea..282c353 100644 --- a/tests/test_fastapi/test_json_field_fastapi.py +++ b/tests/test_fastapi/test_json_field_fastapi.py @@ -2,52 +2,31 @@ import uuid from typing import List -import databases +import ormar import pydantic import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -app = FastAPI() - -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() -app.state.database = database - - -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() - - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) class Thing(ormar.Model): - class Meta(BaseMeta): - tablename = "things" + ormar_config = base_ormar_config.copy(tablename="things") id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4) name: str = ormar.Text(default="") js: pydantic.Json = ormar.JSON() +create_test_database = init_tests(base_ormar_config) + + @app.get("/things", response_model=List[Thing]) async def read_things(): return await Thing.objects.order_by("name").all() @@ -87,14 +66,6 @@ async def read_things_untyped(): return await Thing.objects.order_by("name").all() -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) - - @pytest.mark.asyncio async def test_json_is_required_if_not_nullable(): with pytest.raises(pydantic.ValidationError): @@ -104,8 +75,7 @@ async def test_json_is_required_if_not_nullable(): @pytest.mark.asyncio async def test_json_is_not_required_if_nullable(): class Thing2(ormar.Model): - class Meta(BaseMeta): - tablename = "things2" + ormar_config = base_ormar_config.copy(tablename="things2") id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4) name: str = ormar.Text(default="") @@ -116,16 +86,16 @@ async def test_json_is_not_required_if_nullable(): @pytest.mark.asyncio async def test_setting_values_after_init(): - async with database: + async with base_ormar_config.database: t1 = Thing(id="67a82813-d90c-45ff-b546-b4e38d7030d7", name="t1", js=["thing1"]) - assert '["thing1"]' in t1.json() + assert '["thing1"]' in t1.model_dump_json() await t1.save() - t1.json() - assert '["thing1"]' in t1.json() + t1.model_dump_json() + assert '["thing1"]' in t1.model_dump_json() - assert '["thing1"]' in (await Thing.objects.get(id=t1.id)).json() + assert '["thing1"]' in (await Thing.objects.get(id=t1.id)).model_dump_json() await t1.update() - assert '["thing1"]' in (await Thing.objects.get(id=t1.id)).json() + assert '["thing1"]' in (await Thing.objects.get(id=t1.id)).model_dump_json() @pytest.mark.asyncio diff --git a/tests/test_fastapi/test_m2m_forwardref.py b/tests/test_fastapi/test_m2m_forwardref.py index e50c835..20c10bf 100644 --- a/tests/test_fastapi/test_m2m_forwardref.py +++ b/tests/test_fastapi/test_m2m_forwardref.py @@ -1,42 +1,17 @@ -from typing import List, Optional - -import databases -import pytest -import sqlalchemy -from asgi_lifespan import LifespanManager -from fastapi import FastAPI -from pydantic.schema import ForwardRef -from starlette import status -from httpx import AsyncClient +from typing import ForwardRef, List, Optional import ormar +import pytest +from asgi_lifespan import LifespanManager +from fastapi import FastAPI +from httpx import AsyncClient +from starlette import status -app = FastAPI() -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() - -app.state.database = database - - -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() - - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) CityRef = ForwardRef("City") @@ -45,8 +20,7 @@ CountryRef = ForwardRef("Country") # models.py class Country(ormar.Model): - class Meta(BaseMeta): - tablename = "countries" + ormar_config = base_ormar_config.copy(tablename="countries") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=128, unique=True) @@ -64,8 +38,7 @@ class Country(ormar.Model): class City(ormar.Model): - class Meta(BaseMeta): - tablename = "cities" + ormar_config = base_ormar_config.copy(tablename="cities") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=128) @@ -77,12 +50,7 @@ class City(ormar.Model): Country.update_forward_refs() -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @app.post("/", response_model=Country, status_code=status.HTTP_201_CREATED) diff --git a/tests/test_fastapi/test_more_reallife_fastapi.py b/tests/test_fastapi/test_more_reallife_fastapi.py index 421afa8..fc9c08c 100644 --- a/tests/test_fastapi/test_more_reallife_fastapi.py +++ b/tests/test_fastapi/test_more_reallife_fastapi.py @@ -1,62 +1,34 @@ from typing import List, Optional -import databases +import ormar import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -app = FastAPI() -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) -app.state.database = database - - -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Item(ormar.Model): - class Meta: - tablename = "items" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="items") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) category: Optional[Category] = ormar.ForeignKey(Category, nullable=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @app.get("/items", response_model=List[Item]) @@ -92,7 +64,7 @@ async def get_item(item_id: int): @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item): item_db = await Item.objects.get(pk=item_id) - return await item_db.update(**item.dict()) + return await item_db.update(**item.model_dump()) @app.delete("/items/{item_id}") @@ -118,8 +90,8 @@ async def test_all_endpoints(): assert items[0] == item item.name = "New name" - response = await client.put(f"/items/{item.pk}", json=item.dict()) - assert response.json() == item.dict() + response = await client.put(f"/items/{item.pk}", json=item.model_dump()) + assert response.json() == item.model_dump() response = await client.get("/items") items = [Item(**item) for item in response.json()] diff --git a/tests/test_fastapi/test_nested_saving.py b/tests/test_fastapi/test_nested_saving.py index 44f1064..8b69927 100644 --- a/tests/test_fastapi/test_nested_saving.py +++ b/tests/test_fastapi/test_nested_saving.py @@ -1,52 +1,29 @@ -import json from typing import Any, Dict, Optional, Set, Type, Union, cast -import databases +import ormar import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient - -import ormar from ormar.queryset.utils import translate_list_to_dict -from tests.settings import DATABASE_URL -app = FastAPI() -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) -app.state.database = database +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) headers = {"content-type": "application/json"} -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() - - class Department(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) department_name: str = ormar.String(max_length=100) class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) course_name: str = ormar.String(max_length=100) @@ -55,24 +32,13 @@ class Course(ormar.Model): class Student(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) courses = ormar.ManyToMany(Course) -# create db and tables -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) - - to_exclude = { "id": ..., "courses": { @@ -88,6 +54,9 @@ to_exclude_ormar = { } +create_test_database = init_tests(base_ormar_config) + + def auto_exclude_id_field(to_exclude: Any) -> Union[Dict, Set]: if isinstance(to_exclude, dict): for key in to_exclude.keys(): @@ -117,7 +86,7 @@ async def get_department(department_name: str): department = await Department.objects.select_all(follow=True).get( department_name=department_name ) - return department.dict(exclude=to_exclude) + return department.model_dump(exclude=to_exclude) @app.get("/departments/{department_name}/second") @@ -125,7 +94,7 @@ async def get_department_exclude(department_name: str): department = await Department.objects.select_all(follow=True).get( department_name=department_name ) - return department.dict(exclude=to_exclude_ormar) + return department.model_dump(exclude=to_exclude_ormar) @app.get("/departments/{department_name}/exclude") @@ -133,7 +102,7 @@ async def get_department_exclude_all(department_name: str): department = await Department.objects.select_all(follow=True).get( department_name=department_name ) - return department.dict(exclude=exclude_all) + return department.model_dump(exclude=exclude_all) @pytest.mark.asyncio diff --git a/tests/test_fastapi/test_recursion_error.py b/tests/test_fastapi/test_recursion_error.py index 1e1e254..ef6b91e 100644 --- a/tests/test_fastapi/test_recursion_error.py +++ b/tests/test_fastapi/test_recursion_error.py @@ -1,41 +1,22 @@ -import json import uuid from datetime import datetime from typing import List -import databases +import ormar import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import Depends, FastAPI from httpx import AsyncClient from pydantic import BaseModel, Json -import ormar -from tests.settings import DATABASE_URL - -router = FastAPI() -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) -router.state.database = database +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config +base_ormar_config = create_config() +router = FastAPI(lifespan=lifespan(base_ormar_config)) headers = {"content-type": "application/json"} -@router.on_event("startup") -async def startup() -> None: - database_ = router.state.database - if not database_.is_connected: - await database_.connect() - - -@router.on_event("shutdown") -async def shutdown() -> None: - database_ = router.state.database - if database_.is_connected: - await database_.disconnect() - - class User(ormar.Model): """ The user model @@ -49,10 +30,7 @@ class User(ormar.Model): verify_key: str = ormar.String(unique=True, max_length=100, nullable=True) created_at: datetime = ormar.DateTime(default=datetime.now()) - class Meta: - tablename = "users" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="users") class UserSession(ormar.Model): @@ -65,10 +43,7 @@ class UserSession(ormar.Model): session_key: str = ormar.String(unique=True, max_length=64) created_at: datetime = ormar.DateTime(default=datetime.now()) - class Meta: - tablename = "user_sessions" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="user_sessions") class QuizAnswer(BaseModel): @@ -96,18 +71,10 @@ class Quiz(ormar.Model): user_id: uuid.UUID = ormar.UUID(foreign_key=User.id) questions: Json = ormar.JSON(nullable=False) - class Meta: - tablename = "quiz" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="quiz") -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) async def get_current_user(): @@ -118,7 +85,7 @@ async def get_current_user(): async def create_quiz_lol( quiz_input: QuizInput, user: User = Depends(get_current_user) ): - quiz = Quiz(**quiz_input.dict(), user_id=user.id) + quiz = Quiz(**quiz_input.model_dump(), user_id=user.id) return await quiz.save() diff --git a/tests/test_fastapi/test_relations_with_nested_defaults.py b/tests/test_fastapi/test_relations_with_nested_defaults.py index 1b8cfbb..ebbb6c7 100644 --- a/tests/test_fastapi/test_relations_with_nested_defaults.py +++ b/tests/test_fastapi/test_relations_with_nested_defaults.py @@ -1,53 +1,28 @@ from typing import Optional -import databases +import ormar import pytest import pytest_asyncio -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() - -app = FastAPI() -app.state.database = database - - -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() - - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) class Country(ormar.Model): - class Meta(BaseMeta): - tablename = "countries" + ormar_config = base_ormar_config.copy(tablename="countries") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, default="Poland") class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -56,8 +31,7 @@ class Author(ormar.Model): class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" + ormar_config = base_ormar_config.copy(tablename="books") id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey(Author) @@ -65,17 +39,12 @@ class Book(ormar.Model): year: int = ormar.Integer(nullable=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest_asyncio.fixture async def sample_data(): - async with database: + async with base_ormar_config.database: country = await Country(id=1, name="USA").save() author = await Author(id=1, name="bug", rating=5, country=country).save() await Book( @@ -101,7 +70,17 @@ async def test_related_with_defaults(sample_data): async with client as client, LifespanManager(app): response = await client.get("/books/1") assert response.json() == { - "author": {"id": 1}, + "author": { + "books": [ + { + "author": {"id": 1}, + "id": 1, + "title": "Bug caused by default value", + "year": 2021, + } + ], + "id": 1, + }, "id": 1, "title": "Bug caused by default value", "year": 2021, @@ -111,9 +90,14 @@ async def test_related_with_defaults(sample_data): assert response.json() == { "author": { "books": [ - {"id": 1, "title": "Bug caused by default value", "year": 2021} + { + "author": {"id": 1}, + "id": 1, + "title": "Bug caused by default value", + "year": 2021, + } ], - "country": {"id": 1}, + "country": {"authors": [{"id": 1}], "id": 1}, "id": 1, "name": "bug", "rating": 5, diff --git a/tests/test_fastapi/test_schema_not_allowed_params.py b/tests/test_fastapi/test_schema_not_allowed_params.py index bc24a08..5147f91 100644 --- a/tests/test_fastapi/test_schema_not_allowed_params.py +++ b/tests/test_fastapi/test_schema_not_allowed_params.py @@ -1,29 +1,24 @@ -import databases -import sqlalchemy - import ormar -DATABASE_URL = "sqlite:///db.sqlite" -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) contents: str = ormar.Text() +create_test_database = init_tests(base_ormar_config) + + def test_schema_not_allowed(): - schema = Author.schema() + schema = Author.model_json_schema() for field_schema in schema.get("properties").values(): for key in field_schema.keys(): assert "_" not in key, f"Found illegal field in openapi schema: {key}" diff --git a/tests/test_fastapi/test_skip_reverse_models.py b/tests/test_fastapi/test_skip_reverse_models.py index 3b28e05..fd06ae0 100644 --- a/tests/test_fastapi/test_skip_reverse_models.py +++ b/tests/test_fastapi/test_skip_reverse_models.py @@ -1,45 +1,21 @@ from typing import List, Optional -import databases +import ormar import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient -import ormar -from tests.settings import DATABASE_URL - -app = FastAPI() -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) -app.state.database = database +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) headers = {"content-type": "application/json"} -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() - - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata - - class Author(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) first_name: str = ormar.String(max_length=80) @@ -47,16 +23,18 @@ class Author(ormar.Model): class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) +class Category2(Category): + model_config = dict(extra="forbid") + + class Post(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -64,12 +42,12 @@ class Post(ormar.Model): author: Optional[Author] = ormar.ForeignKey(Author, skip_reverse=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) + + +@app.post("/categories/forbid/", response_model=Category2) +async def create_category_forbid(category: Category2): # pragma: no cover + pass @app.post("/categories/", response_model=Category) @@ -115,7 +93,7 @@ async def test_queries(): assert response.status_code == 200 response = await client.get("/categories/") assert response.status_code == 200 - assert not "posts" in response.json() + assert "posts" not in response.json() categories = [Category(**x) for x in response.json()] assert categories[0] is not None assert categories[0].name == "Test category2" @@ -139,7 +117,7 @@ async def test_queries(): response = await client.post("/posts/", json=right_post, headers=headers) assert response.status_code == 200 - Category.__config__.extra = "allow" + Category.model_config["extra"] = "allow" response = await client.get("/posts/") assert response.status_code == 200 posts = [Post(**x) for x in response.json()] @@ -150,6 +128,6 @@ async def test_queries(): wrong_category = {"name": "Test category3", "posts": [{"title": "Test Post"}]} # cannot add posts if skipped, will be error with extra forbid - Category.__config__.extra = "forbid" - response = await client.post("/categories/", json=wrong_category) + assert Category2.model_config["extra"] == "forbid" + response = await client.post("/categories/forbid/", json=wrong_category) assert response.status_code == 422 diff --git a/tests/test_fastapi/test_wekref_exclusion.py b/tests/test_fastapi/test_wekref_exclusion.py index 989ba2b..bcaf522 100644 --- a/tests/test_fastapi/test_wekref_exclusion.py +++ b/tests/test_fastapi/test_wekref_exclusion.py @@ -1,55 +1,22 @@ from typing import List, Optional from uuid import UUID, uuid4 -import databases +import ormar import pydantic import pytest -import sqlalchemy from asgi_lifespan import LifespanManager from fastapi import FastAPI from httpx import AsyncClient -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests, lifespan +from tests.settings import create_config -app = FastAPI() - -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() - -app.state.database = database - - -@app.on_event("startup") -async def startup() -> None: - database_ = app.state.database - if not database_.is_connected: - await database_.connect() - - -@app.on_event("shutdown") -async def shutdown() -> None: - database_ = app.state.database - if database_.is_connected: - await database_.disconnect() - - -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) - - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() +app = FastAPI(lifespan=lifespan(base_ormar_config)) class OtherThing(ormar.Model): - class Meta(BaseMeta): - tablename = "other_things" + ormar_config = base_ormar_config.copy(tablename="other_things") id: UUID = ormar.UUID(primary_key=True, default=uuid4) name: str = ormar.Text(default="") @@ -57,8 +24,7 @@ class OtherThing(ormar.Model): class Thing(ormar.Model): - class Meta(BaseMeta): - tablename = "things" + ormar_config = base_ormar_config.copy(tablename="things") id: UUID = ormar.UUID(primary_key=True, default=uuid4) name: str = ormar.Text(default="") @@ -66,6 +32,9 @@ class Thing(ormar.Model): other_thing: Optional[OtherThing] = ormar.ForeignKey(OtherThing, nullable=True) +create_test_database = init_tests(base_ormar_config) + + @app.post("/test/1") async def post_test_1(): # don't split initialization and attribute assignment @@ -98,7 +67,7 @@ async def get_test_3(): ot = await OtherThing.objects.select_related("things").get() # exclude unwanted field while ot is still in scope # in order not to pass it to fastapi - return [t.dict(exclude={"other_thing"}) for t in ot.things] + return [t.model_dump(exclude={"other_thing"}) for t in ot.things] @app.get("/test/4", response_model=List[Thing], response_model_exclude={"other_thing"}) diff --git a/tests/test_inheritance_and_pydantic_generation/test_excluding_parent_fields_inheritance.py b/tests/test_inheritance_and_pydantic_generation/test_excluding_parent_fields_inheritance.py index ade7085..a083a29 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_excluding_parent_fields_inheritance.py +++ b/tests/test_inheritance_and_pydantic_generation/test_excluding_parent_fields_inheritance.py @@ -1,49 +1,37 @@ import datetime -import databases -import pytest -import sqlalchemy as sa -from sqlalchemy import create_engine - import ormar -from tests.settings import DATABASE_URL +import pytest -metadata = sa.MetaData() -db = databases.Database(DATABASE_URL) -engine = create_engine(DATABASE_URL) +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class User(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "users" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="users") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50, unique=True, index=True) class RelationalAuditModel(ormar.Model): - class Meta: - abstract = True + ormar_config = base_ormar_config.copy(abstract=True) created_by: User = ormar.ForeignKey(User, nullable=False) updated_by: User = ormar.ForeignKey(User, nullable=False) class AuditModel(ormar.Model): - class Meta: - abstract = True + ormar_config = base_ormar_config.copy(abstract=True) created_by: str = ormar.String(max_length=100) updated_by: str = ormar.String(max_length=100, default="Sam") class DateFieldsModel(ormar.Model): - class Meta(ormar.ModelMeta): - abstract = True - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(abstract=True) created_date: datetime.datetime = ormar.DateTime( default=datetime.datetime.now, name="creation_date" @@ -54,9 +42,10 @@ class DateFieldsModel(ormar.Model): class Category(DateFieldsModel, AuditModel): - class Meta(ormar.ModelMeta): - tablename = "categories" - exclude_parent_fields = ["updated_by", "updated_date"] + ormar_config = base_ormar_config.copy( + tablename="categories", + exclude_parent_fields=["updated_by", "updated_date"], + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50, unique=True, index=True) @@ -64,9 +53,10 @@ class Category(DateFieldsModel, AuditModel): class Item(DateFieldsModel, AuditModel): - class Meta(ormar.ModelMeta): - tablename = "items" - exclude_parent_fields = ["updated_by", "updated_date"] + ormar_config = base_ormar_config.copy( + tablename="items", + exclude_parent_fields=["updated_by", "updated_date"], + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50, unique=True, index=True) @@ -75,25 +65,22 @@ class Item(DateFieldsModel, AuditModel): class Gun(RelationalAuditModel, DateFieldsModel): - class Meta(ormar.ModelMeta): - tablename = "guns" - exclude_parent_fields = ["updated_by"] + ormar_config = base_ormar_config.copy( + tablename="guns", + exclude_parent_fields=["updated_by"], + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_model_definition(): - model_fields = Category.Meta.model_fields - sqlalchemy_columns = Category.Meta.table.c - pydantic_columns = Category.__fields__ + model_fields = Category.ormar_config.model_fields + sqlalchemy_columns = Category.ormar_config.table.c + pydantic_columns = Category.model_fields assert "updated_by" not in model_fields assert "updated_by" not in sqlalchemy_columns assert "updated_by" not in pydantic_columns @@ -101,15 +88,15 @@ def test_model_definition(): assert "updated_date" not in sqlalchemy_columns assert "updated_date" not in pydantic_columns - assert "updated_by" not in Gun.Meta.model_fields - assert "updated_by" not in Gun.Meta.table.c - assert "updated_by" not in Gun.__fields__ + assert "updated_by" not in Gun.ormar_config.model_fields + assert "updated_by" not in Gun.ormar_config.table.c + assert "updated_by" not in Gun.model_fields @pytest.mark.asyncio async def test_model_works_as_expected(): - async with db: - async with db.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): test = await Category(name="Cat", code=2, created_by="Joe").save() assert test.created_date is not None @@ -120,8 +107,8 @@ async def test_model_works_as_expected(): @pytest.mark.asyncio async def test_exclude_with_redefinition(): - async with db: - async with db.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): test = await Item(name="Item", code=3, created_by="Anna").save() assert test.created_date is not None assert test.updated_by == "Bob" @@ -133,8 +120,8 @@ async def test_exclude_with_redefinition(): @pytest.mark.asyncio async def test_exclude_with_relation(): - async with db: - async with db.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): user = await User(name="Michail Kalasznikow").save() test = await Gun(name="AK47", created_by=user).save() assert test.created_date is not None diff --git a/tests/test_inheritance_and_pydantic_generation/test_geting_pydantic_models.py b/tests/test_inheritance_and_pydantic_generation/test_geting_pydantic_models.py index 146c87f..5ca9a3e 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_geting_pydantic_models.py +++ b/tests/test_inheritance_and_pydantic_generation/test_geting_pydantic_models.py @@ -1,26 +1,17 @@ -from typing import List, Optional - -import databases -import pydantic -import sqlalchemy -from pydantic import ConstrainedStr -from pydantic.typing import ForwardRef +from typing import ForwardRef, List, Optional import ormar -from tests.settings import DATABASE_URL +import pydantic +from pydantic_core import PydanticUndefined -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class SelfRef(ormar.Model): - class Meta(BaseMeta): - tablename = "self_refs" + ormar_config = base_ormar_config.copy(tablename="self_refs") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, default="selfref") @@ -31,16 +22,14 @@ SelfRef.update_forward_refs() class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Item(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, default="test") @@ -48,16 +37,14 @@ class Item(ormar.Model): class MutualA(ormar.Model): - class Meta(BaseMeta): - tablename = "mutual_a" + ormar_config = base_ormar_config.copy(tablename="mutual_a") id: int = ormar.Integer(primary_key=True) mutual_b = ormar.ForeignKey(ForwardRef("MutualB"), related_name="mutuals_a") class MutualB(ormar.Model): - class Meta(BaseMeta): - tablename = "mutual_b" + ormar_config = base_ormar_config.copy(tablename="mutual_b") id: int = ormar.Integer(primary_key=True) name = ormar.String(max_length=100, default="test") @@ -67,26 +54,50 @@ class MutualB(ormar.Model): MutualA.update_forward_refs() +create_test_database = init_tests(base_ormar_config) + + def test_getting_pydantic_model(): PydanticCategory = Category.get_pydantic() assert issubclass(PydanticCategory, pydantic.BaseModel) - assert {*PydanticCategory.__fields__.keys()} == {"items", "id", "name"} + assert {*PydanticCategory.model_fields.keys()} == {"items", "id", "name"} - assert not PydanticCategory.__fields__["id"].required - assert PydanticCategory.__fields__["id"].outer_type_ == int - assert PydanticCategory.__fields__["id"].default is None + assert not PydanticCategory.model_fields["id"].is_required() + assert ( + PydanticCategory.__pydantic_core_schema__["schema"]["fields"]["id"]["schema"][ + "schema" + ]["schema"]["type"] + == "int" + ) + assert PydanticCategory.model_fields["id"].default is None - assert PydanticCategory.__fields__["name"].required - assert issubclass(PydanticCategory.__fields__["name"].outer_type_, ConstrainedStr) - assert PydanticCategory.__fields__["name"].default in [None, Ellipsis] + assert PydanticCategory.model_fields["name"].is_required() + assert ( + PydanticCategory.__pydantic_core_schema__["schema"]["fields"]["name"]["schema"][ + "type" + ] + == "str" + ) + assert PydanticCategory.model_fields["name"].default == PydanticUndefined - PydanticItem = PydanticCategory.__fields__["items"].type_ - assert PydanticCategory.__fields__["items"].outer_type_ == List[PydanticItem] + PydanticItem = PydanticCategory.__pydantic_core_schema__["schema"]["fields"][ + "items" + ]["schema"]["schema"]["schema"]["items_schema"]["cls"] + assert ( + PydanticCategory.__pydantic_core_schema__["schema"]["fields"]["items"][ + "schema" + ]["schema"]["schema"]["type"] + == "list" + ) + assert ( + PydanticCategory.model_fields["items"].annotation + == Optional[List[PydanticItem]] + ) assert issubclass(PydanticItem, pydantic.BaseModel) - assert not PydanticItem.__fields__["name"].required - assert PydanticItem.__fields__["name"].default == "test" - assert issubclass(PydanticItem.__fields__["name"].outer_type_, ConstrainedStr) - assert "category" not in PydanticItem.__fields__ + assert not PydanticItem.model_fields["name"].is_required() + assert PydanticItem.model_fields["name"].default == "test" + assert PydanticItem.model_fields["name"].annotation == Optional[str] + assert "category" not in PydanticItem.model_fields def test_initializing_pydantic_model(): @@ -96,140 +107,199 @@ def test_initializing_pydantic_model(): "items": [{"id": 1, "name": "test_i1"}, {"id": 2, "name": "test_i2"}], } PydanticCategory = Category.get_pydantic() + ormar_cat = Category(**data) + assert ormar_cat.model_dump() == data cat = PydanticCategory(**data) - assert cat.dict() == data + assert cat.model_dump() == data data = {"id": 1, "name": "test"} cat = PydanticCategory(**data) - assert cat.dict() == {**data, "items": None} + assert cat.model_dump() == {**data, "items": None} def test_getting_pydantic_model_include(): PydanticCategory = Category.get_pydantic(include={"id", "name"}) - assert len(PydanticCategory.__fields__) == 2 - assert "items" not in PydanticCategory.__fields__ + assert len(PydanticCategory.model_fields) == 2 + assert "items" not in PydanticCategory.model_fields def test_getting_pydantic_model_nested_include_set(): PydanticCategory = Category.get_pydantic(include={"id", "items__id"}) - assert len(PydanticCategory.__fields__) == 2 - assert "name" not in PydanticCategory.__fields__ - PydanticItem = PydanticCategory.__fields__["items"].type_ - assert len(PydanticItem.__fields__) == 1 - assert "id" in PydanticItem.__fields__ + assert len(PydanticCategory.model_fields) == 2 + assert "name" not in PydanticCategory.model_fields + PydanticItem = PydanticCategory.__pydantic_core_schema__["schema"]["fields"][ + "items" + ]["schema"]["schema"]["schema"]["items_schema"]["cls"] + assert len(PydanticItem.model_fields) == 1 + assert "id" in PydanticItem.model_fields def test_getting_pydantic_model_nested_include_dict(): PydanticCategory = Category.get_pydantic(include={"id": ..., "items": {"id"}}) - assert len(PydanticCategory.__fields__) == 2 - assert "name" not in PydanticCategory.__fields__ - PydanticItem = PydanticCategory.__fields__["items"].type_ - assert len(PydanticItem.__fields__) == 1 - assert "id" in PydanticItem.__fields__ + assert len(PydanticCategory.model_fields) == 2 + assert "name" not in PydanticCategory.model_fields + PydanticItem = PydanticCategory.__pydantic_core_schema__["schema"]["fields"][ + "items" + ]["schema"]["schema"]["schema"]["items_schema"]["cls"] + assert len(PydanticItem.model_fields) == 1 + assert "id" in PydanticItem.model_fields def test_getting_pydantic_model_nested_include_nested_dict(): PydanticCategory = Category.get_pydantic(include={"id": ..., "items": {"id": ...}}) - assert len(PydanticCategory.__fields__) == 2 - assert "name" not in PydanticCategory.__fields__ - PydanticItem = PydanticCategory.__fields__["items"].type_ - assert len(PydanticItem.__fields__) == 1 - assert "id" in PydanticItem.__fields__ + assert len(PydanticCategory.model_fields) == 2 + assert "name" not in PydanticCategory.model_fields + PydanticItem = PydanticCategory.__pydantic_core_schema__["schema"]["fields"][ + "items" + ]["schema"]["schema"]["schema"]["items_schema"]["cls"] + assert len(PydanticItem.model_fields) == 1 + assert "id" in PydanticItem.model_fields def test_getting_pydantic_model_include_exclude(): PydanticCategory = Category.get_pydantic( include={"id": ..., "items": {"id", "name"}}, exclude={"items__name"} ) - assert len(PydanticCategory.__fields__) == 2 - assert "name" not in PydanticCategory.__fields__ - PydanticItem = PydanticCategory.__fields__["items"].type_ - assert len(PydanticItem.__fields__) == 1 - assert "id" in PydanticItem.__fields__ + assert len(PydanticCategory.model_fields) == 2 + assert "name" not in PydanticCategory.model_fields + PydanticItem = PydanticCategory.__pydantic_core_schema__["schema"]["fields"][ + "items" + ]["schema"]["schema"]["schema"]["items_schema"]["cls"] + assert len(PydanticItem.model_fields) == 1 + assert "id" in PydanticItem.model_fields def test_getting_pydantic_model_exclude(): PydanticItem = Item.get_pydantic(exclude={"category__name"}) - assert len(PydanticItem.__fields__) == 3 - assert "category" in PydanticItem.__fields__ - PydanticCategory = PydanticItem.__fields__["category"].type_ - assert len(PydanticCategory.__fields__) == 1 - assert "name" not in PydanticCategory.__fields__ + assert len(PydanticItem.model_fields) == 3 + assert "category" in PydanticItem.model_fields + PydanticCategory = PydanticItem.__pydantic_core_schema__["schema"]["fields"][ + "category" + ]["schema"]["schema"]["schema"]["cls"] + assert len(PydanticCategory.model_fields) == 1 + assert "name" not in PydanticCategory.model_fields def test_getting_pydantic_model_exclude_dict(): PydanticItem = Item.get_pydantic(exclude={"id": ..., "category": {"name"}}) - assert len(PydanticItem.__fields__) == 2 - assert "category" in PydanticItem.__fields__ - assert "id" not in PydanticItem.__fields__ - PydanticCategory = PydanticItem.__fields__["category"].type_ - assert len(PydanticCategory.__fields__) == 1 - assert "name" not in PydanticCategory.__fields__ + assert len(PydanticItem.model_fields) == 2 + assert "category" in PydanticItem.model_fields + assert "id" not in PydanticItem.model_fields + PydanticCategory = PydanticItem.__pydantic_core_schema__["schema"]["fields"][ + "category" + ]["schema"]["schema"]["schema"]["cls"] + assert len(PydanticCategory.model_fields) == 1 + assert "name" not in PydanticCategory.model_fields def test_getting_pydantic_model_self_ref(): PydanticSelfRef = SelfRef.get_pydantic() - assert len(PydanticSelfRef.__fields__) == 4 - assert set(PydanticSelfRef.__fields__.keys()) == { + assert len(PydanticSelfRef.model_fields) == 4 + assert set(PydanticSelfRef.model_fields.keys()) == { "id", "name", "parent", "children", } - InnerSelf = PydanticSelfRef.__fields__["parent"].type_ - assert len(InnerSelf.__fields__) == 2 - assert set(InnerSelf.__fields__.keys()) == {"id", "name"} + inner_self_ref_id = PydanticSelfRef.__pydantic_core_schema__["schema"]["schema"][ + "fields" + ]["parent"]["schema"]["schema"]["schema"]["schema_ref"] + InnerSelf = next( + ( + x + for x in PydanticSelfRef.__pydantic_core_schema__["definitions"] + if x["ref"] == inner_self_ref_id + ) + )["cls"] + assert len(InnerSelf.model_fields) == 2 + assert set(InnerSelf.model_fields.keys()) == {"id", "name"} - InnerSelf2 = PydanticSelfRef.__fields__["children"].type_ - assert len(InnerSelf2.__fields__) == 2 - assert set(InnerSelf2.__fields__.keys()) == {"id", "name"} + inner_self_ref_id2 = PydanticSelfRef.__pydantic_core_schema__["schema"]["schema"][ + "fields" + ]["children"]["schema"]["schema"]["schema"]["items_schema"]["schema_ref"] + InnerSelf2 = next( + ( + x + for x in PydanticSelfRef.__pydantic_core_schema__["definitions"] + if x["ref"] == inner_self_ref_id2 + ) + )["cls"] + assert len(InnerSelf2.model_fields) == 2 + assert set(InnerSelf2.model_fields.keys()) == {"id", "name"} def test_getting_pydantic_model_self_ref_exclude(): PydanticSelfRef = SelfRef.get_pydantic(exclude={"children": {"name"}}) - assert len(PydanticSelfRef.__fields__) == 4 - assert set(PydanticSelfRef.__fields__.keys()) == { + assert len(PydanticSelfRef.model_fields) == 4 + assert set(PydanticSelfRef.model_fields.keys()) == { "id", "name", "parent", "children", } - InnerSelf = PydanticSelfRef.__fields__["parent"].type_ - assert len(InnerSelf.__fields__) == 2 - assert set(InnerSelf.__fields__.keys()) == {"id", "name"} + InnerSelf = PydanticSelfRef.__pydantic_core_schema__["schema"]["fields"]["parent"][ + "schema" + ]["schema"]["schema"]["cls"] + assert len(InnerSelf.model_fields) == 2 + assert set(InnerSelf.model_fields.keys()) == {"id", "name"} - PydanticSelfRefChildren = PydanticSelfRef.__fields__["children"].type_ - assert len(PydanticSelfRefChildren.__fields__) == 1 - assert set(PydanticSelfRefChildren.__fields__.keys()) == {"id"} + # PydanticSelfRefChildren = PydanticSelfRef.model_fields["children"].type_ + PydanticSelfRefChildren = PydanticSelfRef.__pydantic_core_schema__["schema"][ + "fields" + ]["children"]["schema"]["schema"]["schema"]["items_schema"]["cls"] + assert len(PydanticSelfRefChildren.model_fields) == 1 + assert set(PydanticSelfRefChildren.model_fields.keys()) == {"id"} assert PydanticSelfRef != PydanticSelfRefChildren assert InnerSelf != PydanticSelfRefChildren def test_getting_pydantic_model_mutual_rels(): MutualAPydantic = MutualA.get_pydantic() - assert len(MutualAPydantic.__fields__) == 3 - assert set(MutualAPydantic.__fields__.keys()) == {"id", "mutual_b", "mutuals_b"} + assert len(MutualAPydantic.model_fields) == 3 + assert set(MutualAPydantic.model_fields.keys()) == {"id", "mutual_b", "mutuals_b"} - MutualB1 = MutualAPydantic.__fields__["mutual_b"].type_ - MutualB2 = MutualAPydantic.__fields__["mutuals_b"].type_ - assert len(MutualB1.__fields__) == 2 - assert set(MutualB1.__fields__.keys()) == {"id", "name"} - assert len(MutualB2.__fields__) == 2 - assert set(MutualB2.__fields__.keys()) == {"id", "name"} + mutual_ref_1 = MutualAPydantic.__pydantic_core_schema__["schema"]["schema"][ + "fields" + ]["mutual_b"]["schema"]["schema"]["schema"]["schema_ref"] + MutualB1 = next( + ( + x + for x in MutualAPydantic.__pydantic_core_schema__["definitions"] + if x["ref"] == mutual_ref_1 + ) + )["cls"] + mutual_ref_2 = MutualAPydantic.__pydantic_core_schema__["schema"]["schema"][ + "fields" + ]["mutuals_b"]["schema"]["schema"]["schema"]["items_schema"]["schema_ref"] + MutualB2 = next( + ( + x + for x in MutualAPydantic.__pydantic_core_schema__["definitions"] + if x["ref"] == mutual_ref_2 + ) + )["cls"] + assert len(MutualB1.model_fields) == 2 + assert set(MutualB1.model_fields.keys()) == {"id", "name"} + assert len(MutualB2.model_fields) == 2 + assert set(MutualB2.model_fields.keys()) == {"id", "name"} assert MutualB1 == MutualB2 def test_getting_pydantic_model_mutual_rels_exclude(): MutualAPydantic = MutualA.get_pydantic(exclude={"mutual_b": {"name"}}) - assert len(MutualAPydantic.__fields__) == 3 - assert set(MutualAPydantic.__fields__.keys()) == {"id", "mutual_b", "mutuals_b"} + assert len(MutualAPydantic.model_fields) == 3 + assert set(MutualAPydantic.model_fields.keys()) == {"id", "mutual_b", "mutuals_b"} - MutualB1 = MutualAPydantic.__fields__["mutual_b"].type_ - MutualB2 = MutualAPydantic.__fields__["mutuals_b"].type_ + MutualB1 = MutualAPydantic.__pydantic_core_schema__["schema"]["fields"]["mutual_b"][ + "schema" + ]["schema"]["schema"]["cls"] + MutualB2 = MutualAPydantic.__pydantic_core_schema__["schema"]["fields"][ + "mutuals_b" + ]["schema"]["schema"]["schema"]["items_schema"]["cls"] - assert len(MutualB1.__fields__) == 1 - assert set(MutualB1.__fields__.keys()) == {"id"} - assert len(MutualB2.__fields__) == 2 - assert set(MutualB2.__fields__.keys()) == {"id", "name"} + assert len(MutualB1.model_fields) == 1 + assert set(MutualB1.model_fields.keys()) == {"id"} + assert len(MutualB2.model_fields) == 2 + assert set(MutualB2.model_fields.keys()) == {"id", "name"} assert MutualB1 != MutualB2 diff --git a/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py b/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py index ccaf27f..aa4d775 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py +++ b/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py @@ -1,42 +1,37 @@ -# type: ignore import datetime -from typing import List, Optional from collections import Counter - -import databases -import pytest -import sqlalchemy as sa -from sqlalchemy import create_engine +from typing import Optional import ormar import ormar.fields.constraints -from ormar import ModelDefinitionError, property_field +import pydantic +import pytest +import sqlalchemy as sa +from ormar import ModelDefinitionError from ormar.exceptions import ModelError from ormar.models.metaclass import get_constraint_copy -from tests.settings import DATABASE_URL +from ormar.relations.relation_proxy import RelationProxy +from pydantic import computed_field -metadata = sa.MetaData() -db = databases.Database(DATABASE_URL) -engine = create_engine(DATABASE_URL) +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class AuditModel(ormar.Model): - class Meta: - abstract = True + ormar_config = base_ormar_config.copy(abstract=True) created_by: str = ormar.String(max_length=100) updated_by: str = ormar.String(max_length=100, default="Sam") - @property_field - def audit(self): # pragma: no cover + @computed_field + def audit(self) -> str: # pragma: no cover return f"{self.created_by} {self.updated_by}" class DateFieldsModelNoSubclass(ormar.Model): - class Meta: - tablename = "test_date_models" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="test_date_models") date_id: int = ormar.Integer(primary_key=True) created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now) @@ -44,11 +39,9 @@ class DateFieldsModelNoSubclass(ormar.Model): class DateFieldsModel(ormar.Model): - class Meta: - abstract = True - metadata = metadata - database = db - constraints = [ + ormar_config = base_ormar_config.copy( + abstract=True, + constraints=[ ormar.fields.constraints.UniqueColumns( "creation_date", "modification_date", @@ -56,7 +49,8 @@ class DateFieldsModel(ormar.Model): ormar.fields.constraints.CheckColumns( "creation_date <= modification_date", ), - ] + ], + ) created_date: datetime.datetime = ormar.DateTime( default=datetime.datetime.now, name="creation_date" @@ -67,26 +61,26 @@ class DateFieldsModel(ormar.Model): class Category(DateFieldsModel, AuditModel): - class Meta(ormar.ModelMeta): - tablename = "categories" - constraints = [ormar.fields.constraints.UniqueColumns("name", "code")] + ormar_config = base_ormar_config.copy( + tablename="categories", + constraints=[ormar.fields.constraints.UniqueColumns("name", "code")], + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50, unique=True, index=True) code: int = ormar.Integer() - @property_field - def code_name(self): + @computed_field + def code_name(self) -> str: return f"{self.code}:{self.name}" - @property_field - def audit(self): + @computed_field + def audit(self) -> str: return f"{self.created_by} {self.updated_by}" class Subject(DateFieldsModel): - class Meta(ormar.ModelMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50, unique=True, index=True) @@ -94,19 +88,14 @@ class Subject(DateFieldsModel): class Person(ormar.Model): - class Meta: - metadata = metadata - database = db + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Car(ormar.Model): - class Meta: - abstract = True - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(abstract=True) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50) @@ -116,135 +105,109 @@ class Car(ormar.Model): class Truck(Car): - class Meta: - pass + ormar_config = ormar.OrmarConfig() max_capacity: int = ormar.Integer() class Bus(Car): - class Meta: - tablename = "buses" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="buses") owner: Person = ormar.ForeignKey(Person, related_name="buses") max_persons: int = ormar.Integer() class Car2(ormar.Model): - class Meta: - abstract = True - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(abstract=True) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50) owner: Person = ormar.ForeignKey(Person, related_name="owned") - co_owners: List[Person] = ormar.ManyToMany(Person, related_name="coowned") + co_owners: RelationProxy[Person] = ormar.ManyToMany(Person, related_name="coowned") created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now) class Truck2(Car2): - class Meta: - tablename = "trucks2" + ormar_config = base_ormar_config.copy(tablename="trucks2") max_capacity: int = ormar.Integer() class Bus2(Car2): - class Meta: - tablename = "buses2" + ormar_config = base_ormar_config.copy(tablename="buses2") max_persons: int = ormar.Integer() class ImmutablePerson(Person): - class Config: - allow_mutation = False - validate_assignment = False + model_config = dict(frozen=True, validate_assignment=False) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) -def test_init_of_abstract_model(): +def test_init_of_abstract_model() -> None: with pytest.raises(ModelError): DateFieldsModel() -def test_duplicated_related_name_on_different_model(): +def test_duplicated_related_name_on_different_model() -> None: with pytest.raises(ModelDefinitionError): class Bus3(Car2): # pragma: no cover - class Meta: - tablename = "buses3" + ormar_config = ormar.OrmarConfig(tablename="buses3") owner: Person = ormar.ForeignKey(Person, related_name="buses") max_persons: int = ormar.Integer() -def test_config_is_not_a_class_raises_error(): - with pytest.raises(ModelDefinitionError): - - class ImmutablePerson2(Person): - Config = dict(allow_mutation=False, validate_assignment=False) - - -def test_field_redefining_in_concrete_models(): +def test_field_redefining_in_concrete_models() -> None: class RedefinedField(DateFieldsModel): - class Meta(ormar.ModelMeta): - tablename = "redefines" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="redefines") id: int = ormar.Integer(primary_key=True) - created_date: str = ormar.String(max_length=200, name="creation_date") + created_date: str = ormar.String( + max_length=200, + name="creation_date", + ) # type: ignore - changed_field = RedefinedField.Meta.model_fields["created_date"] + changed_field = RedefinedField.ormar_config.model_fields["created_date"] assert changed_field.ormar_default is None assert changed_field.get_alias() == "creation_date" - assert any(x.name == "creation_date" for x in RedefinedField.Meta.table.columns) + assert any( + x.name == "creation_date" for x in RedefinedField.ormar_config.table.columns + ) assert isinstance( - RedefinedField.Meta.table.columns["creation_date"].type, sa.sql.sqltypes.String + RedefinedField.ormar_config.table.columns["creation_date"].type, + sa.sql.sqltypes.String, ) -def test_model_subclassing_that_redefines_constraints_column_names(): +def test_model_subclassing_that_redefines_constraints_column_names() -> None: with pytest.raises(ModelDefinitionError): class WrongField2(DateFieldsModel): # pragma: no cover - class Meta(ormar.ModelMeta): - tablename = "wrongs" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="wrongs") id: int = ormar.Integer(primary_key=True) - created_date: str = ormar.String(max_length=200) + created_date: str = ormar.String(max_length=200) # type: ignore -def test_model_subclassing_non_abstract_raises_error(): +def test_model_subclassing_non_abstract_raises_error() -> None: with pytest.raises(ModelDefinitionError): class WrongField2(DateFieldsModelNoSubclass): # pragma: no cover - class Meta(ormar.ModelMeta): - tablename = "wrongs" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="wrongs") id: int = ormar.Integer(primary_key=True) -def test_params_are_inherited(): - assert Category.Meta.metadata == metadata - assert Category.Meta.database == db - assert len(Category.Meta.property_fields) == 2 +def test_params_are_inherited() -> None: + assert Category.ormar_config.metadata == base_ormar_config.metadata + assert Category.ormar_config.database == base_ormar_config.database + assert len(Category.ormar_config.property_fields) == 2 - constraints = Counter(map(lambda c: type(c), Category.Meta.constraints)) + constraints = Counter(map(lambda c: type(c), Category.ormar_config.constraints)) assert constraints[ormar.fields.constraints.UniqueColumns] == 2 assert constraints[ormar.fields.constraints.IndexColumns] == 0 assert constraints[ormar.fields.constraints.CheckColumns] == 1 @@ -259,9 +222,9 @@ def round_date_to_seconds( @pytest.mark.asyncio -async def test_fields_inherited_from_mixin(): - async with db: - async with db.transaction(force_rollback=True): +async def test_fields_inherited_from_mixin() -> None: + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): cat = await Category( name="Foo", code=123, created_by="Sam", updated_by="Max" ).save() @@ -269,19 +232,28 @@ async def test_fields_inherited_from_mixin(): mixin_columns = ["created_date", "updated_date"] mixin_db_columns = ["creation_date", "modification_date"] mixin2_columns = ["created_by", "updated_by"] - assert all(field in Category.Meta.model_fields for field in mixin_columns) + assert all( + field in Category.ormar_config.model_fields for field in mixin_columns + ) assert cat.created_date is not None assert cat.updated_date is not None - assert all(field in Subject.Meta.model_fields for field in mixin_columns) + assert all( + field in Subject.ormar_config.model_fields for field in mixin_columns + ) + assert cat.code_name == "123:Foo" + assert cat.audit == "Sam Max" assert sub.created_date is not None assert sub.updated_date is not None - assert all(field in Category.Meta.model_fields for field in mixin2_columns) assert all( - field not in Subject.Meta.model_fields for field in mixin2_columns + field in Category.ormar_config.model_fields for field in mixin2_columns + ) + assert all( + field not in Subject.ormar_config.model_fields + for field in mixin2_columns ) - inspector = sa.inspect(engine) + inspector = sa.inspect(base_ormar_config.engine) assert "categories" in inspector.get_table_names() table_columns = [x.get("name") for x in inspector.get_columns("categories")] assert all( @@ -301,6 +273,7 @@ async def test_fields_inherited_from_mixin(): assert round_date_to_seconds(sub2.created_date) == round_date_to_seconds( sub.created_date ) + assert sub2.category is not None assert sub2.category.updated_date is not None assert round_date_to_seconds( sub2.category.created_date @@ -328,9 +301,9 @@ async def test_fields_inherited_from_mixin(): @pytest.mark.asyncio -async def test_inheritance_with_relation(): - async with db: - async with db.transaction(force_rollback=True): +async def test_inheritance_with_relation() -> None: + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): sam = await Person(name="Sam").save() joe = await Person(name="Joe").save() await Truck( @@ -377,9 +350,9 @@ async def test_inheritance_with_relation(): @pytest.mark.asyncio -async def test_inheritance_with_multi_relation(): - async with db: - async with db.transaction(force_rollback=True): +async def test_inheritance_with_multi_relation() -> None: + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): sam = await Person(name="Sam").save() joe = await Person(name="Joe").save() alex = await Person(name="Alex").save() @@ -523,16 +496,16 @@ async def test_inheritance_with_multi_relation(): assert len(unicorns[1].co_owners) == 1 -def test_custom_config(): +def test_custom_config() -> None: # Custom config inherits defaults - assert getattr(ImmutablePerson.__config__, "orm_mode") is True + assert ImmutablePerson.model_config["from_attributes"] is True # Custom config can override defaults - assert getattr(ImmutablePerson.__config__, "validate_assignment") is False + assert ImmutablePerson.model_config["validate_assignment"] is False sam = ImmutablePerson(name="Sam") - with pytest.raises(TypeError): + with pytest.raises(pydantic.ValidationError): sam.name = "Not Sam" -def test_get_constraint_copy(): +def test_get_constraint_copy() -> None: with pytest.raises(ValueError): get_constraint_copy("INVALID CONSTRAINT") diff --git a/tests/test_inheritance_and_pydantic_generation/test_inheritance_mixins.py b/tests/test_inheritance_and_pydantic_generation/test_inheritance_mixins.py index b84fec3..68a6b91 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_inheritance_mixins.py +++ b/tests/test_inheritance_and_pydantic_generation/test_inheritance_mixins.py @@ -1,18 +1,14 @@ -# type: ignore import datetime from typing import Optional -import databases +import ormar import pytest import sqlalchemy as sa -from sqlalchemy import create_engine -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -metadata = sa.MetaData() -db = databases.Database(DATABASE_URL) -engine = create_engine(DATABASE_URL) +base_ormar_config = create_config() class AuditMixin: @@ -26,10 +22,7 @@ class DateFieldsMixins: class Category(ormar.Model, DateFieldsMixins, AuditMixin): - class Meta(ormar.ModelMeta): - tablename = "categories" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50, unique=True, index=True) @@ -37,65 +30,59 @@ class Category(ormar.Model, DateFieldsMixins, AuditMixin): class Subject(ormar.Model, DateFieldsMixins): - class Meta(ormar.ModelMeta): - tablename = "subjects" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="subjects") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50, unique=True, index=True) category: Optional[Category] = ormar.ForeignKey(Category) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) -def test_field_redefining(): +def test_field_redefining() -> None: class RedefinedField(ormar.Model, DateFieldsMixins): - class Meta(ormar.ModelMeta): - tablename = "redefined" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="redefined") id: int = ormar.Integer(primary_key=True) created_date: datetime.datetime = ormar.DateTime(name="creation_date") - assert RedefinedField.Meta.model_fields["created_date"].ormar_default is None assert ( - RedefinedField.Meta.model_fields["created_date"].get_alias() == "creation_date" + RedefinedField.ormar_config.model_fields["created_date"].ormar_default is None + ) + assert ( + RedefinedField.ormar_config.model_fields["created_date"].get_alias() + == "creation_date" + ) + assert any( + x.name == "creation_date" for x in RedefinedField.ormar_config.table.columns ) - assert any(x.name == "creation_date" for x in RedefinedField.Meta.table.columns) -def test_field_redefining_in_second_raises_error(): - class OkField(ormar.Model, DateFieldsMixins): # pragma: no cover - class Meta(ormar.ModelMeta): - tablename = "oks" - metadata = metadata - database = db - - id: int = ormar.Integer(primary_key=True) +def test_field_redefining_in_second() -> None: class RedefinedField2(ormar.Model, DateFieldsMixins): - class Meta(ormar.ModelMeta): - tablename = "redefines2" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="redefines2") id: int = ormar.Integer(primary_key=True) - created_date: str = ormar.String(max_length=200, name="creation_date") + created_date: str = ormar.String( + max_length=200, + name="creation_date", + ) # type: ignore - assert RedefinedField2.Meta.model_fields["created_date"].ormar_default is None assert ( - RedefinedField2.Meta.model_fields["created_date"].get_alias() == "creation_date" + RedefinedField2.ormar_config.model_fields["created_date"].ormar_default is None + ) + assert ( + RedefinedField2.ormar_config.model_fields["created_date"].get_alias() + == "creation_date" + ) + assert any( + x.name == "creation_date" for x in RedefinedField2.ormar_config.table.columns ) - assert any(x.name == "creation_date" for x in RedefinedField2.Meta.table.columns) assert isinstance( - RedefinedField2.Meta.table.columns["creation_date"].type, sa.sql.sqltypes.String + RedefinedField2.ormar_config.table.columns["creation_date"].type, + sa.sql.sqltypes.String, ) @@ -108,28 +95,35 @@ def round_date_to_seconds( @pytest.mark.asyncio -async def test_fields_inherited_from_mixin(): - async with db: - async with db.transaction(force_rollback=True): +async def test_fields_inherited_from_mixin() -> None: + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): cat = await Category( name="Foo", code=123, created_by="Sam", updated_by="Max" ).save() sub = await Subject(name="Bar", category=cat).save() mixin_columns = ["created_date", "updated_date"] mixin2_columns = ["created_by", "updated_by"] - assert all(field in Category.Meta.model_fields for field in mixin_columns) + assert all( + field in Category.ormar_config.model_fields for field in mixin_columns + ) assert cat.created_date is not None assert cat.updated_date is not None - assert all(field in Subject.Meta.model_fields for field in mixin_columns) + assert all( + field in Subject.ormar_config.model_fields for field in mixin_columns + ) assert sub.created_date is not None assert sub.updated_date is not None - assert all(field in Category.Meta.model_fields for field in mixin2_columns) assert all( - field not in Subject.Meta.model_fields for field in mixin2_columns + field in Category.ormar_config.model_fields for field in mixin2_columns + ) + assert all( + field not in Subject.ormar_config.model_fields + for field in mixin2_columns ) - inspector = sa.inspect(engine) + inspector = sa.inspect(base_ormar_config.engine) assert "categories" in inspector.get_table_names() table_columns = [x.get("name") for x in inspector.get_columns("categories")] assert all(col in table_columns for col in mixin_columns + mixin2_columns) @@ -147,6 +141,7 @@ async def test_fields_inherited_from_mixin(): assert round_date_to_seconds(sub2.created_date) == round_date_to_seconds( sub.created_date ) + assert sub2.category is not None assert sub2.category.updated_date is not None assert round_date_to_seconds( sub2.category.created_date diff --git a/tests/test_inheritance_and_pydantic_generation/test_inheritance_of_property_fields.py b/tests/test_inheritance_and_pydantic_generation/test_inheritance_of_property_fields.py index 9e9169f..6b87cef 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_inheritance_of_property_fields.py +++ b/tests/test_inheritance_and_pydantic_generation/test_inheritance_of_property_fields.py @@ -1,32 +1,26 @@ -import databases -import pytest -import sqlalchemy -import sqlalchemy as sa - import ormar -from tests.settings import DATABASE_URL +from pydantic import computed_field -metadata = sa.MetaData() -database = databases.Database(DATABASE_URL) +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class BaseFoo(ormar.Model): - class Meta: - abstract = True + ormar_config = base_ormar_config.copy(abstract=True) name: str = ormar.String(max_length=100) - @ormar.property_field + @computed_field def prefixed_name(self) -> str: return "prefix_" + self.name class Foo(BaseFoo): - class Meta: - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() - @ormar.property_field + @computed_field def double_prefixed_name(self) -> str: return "prefix2_" + self.name @@ -34,30 +28,22 @@ class Foo(BaseFoo): class Bar(BaseFoo): - class Meta: - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() - @ormar.property_field + @computed_field def prefixed_name(self) -> str: return "baz_" + self.name id: int = ormar.Integer(primary_key=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_property_fields_are_inherited(): foo = Foo(name="foo") assert foo.prefixed_name == "prefix_foo" - assert foo.dict() == { + assert foo.model_dump() == { "name": "foo", "id": None, "double_prefixed_name": "prefix2_foo", @@ -66,4 +52,4 @@ def test_property_fields_are_inherited(): bar = Bar(name="bar") assert bar.prefixed_name == "baz_bar" - assert bar.dict() == {"name": "bar", "id": None, "prefixed_name": "baz_bar"} + assert bar.model_dump() == {"name": "bar", "id": None, "prefixed_name": "baz_bar"} diff --git a/tests/test_inheritance_and_pydantic_generation/test_inheritance_with_default.py b/tests/test_inheritance_and_pydantic_generation/test_inheritance_with_default.py index 1c61e41..bd93e79 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_inheritance_with_default.py +++ b/tests/test_inheritance_and_pydantic_generation/test_inheritance_with_default.py @@ -1,25 +1,17 @@ import datetime import uuid -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL) +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() class BaseModel(ormar.Model): - class Meta(ormar.ModelMeta): - abstract = True + ormar_config = base_ormar_config.copy(abstract=True) id: uuid.UUID = ormar.UUID( primary_key=True, default=uuid.uuid4, uuid_format="string" @@ -29,36 +21,29 @@ class BaseModel(ormar.Model): class Member(BaseModel): - class Meta(BaseMeta): - tablename = "members" + ormar_config = base_ormar_config.copy(tablename="members") first_name: str = ormar.String(max_length=50) last_name: str = ormar.String(max_length=50) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_model_structure(): - assert "id" in BaseModel.__fields__ - assert "id" in BaseModel.Meta.model_fields - assert BaseModel.Meta.model_fields["id"].has_default() - assert BaseModel.__fields__["id"].default_factory is not None + assert "id" in BaseModel.model_fields + assert "id" in BaseModel.ormar_config.model_fields + assert BaseModel.ormar_config.model_fields["id"].has_default() + assert BaseModel.model_fields["id"].default_factory is not None - assert "id" in Member.__fields__ - assert "id" in Member.Meta.model_fields - assert Member.Meta.model_fields["id"].has_default() - assert Member.__fields__["id"].default_factory is not None + assert "id" in Member.model_fields + assert "id" in Member.ormar_config.model_fields + assert Member.ormar_config.model_fields["id"].has_default() + assert Member.model_fields["id"].default_factory is not None @pytest.mark.asyncio async def test_fields_inherited_with_default(): - async with database: + async with base_ormar_config.database: await Member(first_name="foo", last_name="bar").save() await Member.objects.create(first_name="foo", last_name="bar") diff --git a/tests/test_inheritance_and_pydantic_generation/test_inherited_class_is_not_abstract_by_default.py b/tests/test_inheritance_and_pydantic_generation/test_inherited_class_is_not_abstract_by_default.py index fc08515..0f43525 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_inherited_class_is_not_abstract_by_default.py +++ b/tests/test_inheritance_and_pydantic_generation/test_inherited_class_is_not_abstract_by_default.py @@ -1,21 +1,16 @@ import datetime -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL) +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class TableBase(ormar.Model): - class Meta(ormar.ModelMeta): - abstract = True - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(abstract=True) id: int = ormar.Integer(primary_key=True) created_by: str = ormar.String(max_length=20, default="test") @@ -27,8 +22,7 @@ class TableBase(ormar.Model): class NationBase(ormar.Model): - class Meta(ormar.ModelMeta): - abstract = True + ormar_config = base_ormar_config.copy(abstract=True) name: str = ormar.String(max_length=50) alpha2_code: str = ormar.String(max_length=2) @@ -37,22 +31,15 @@ class NationBase(ormar.Model): class Nation(NationBase, TableBase): - class Meta(ormar.ModelMeta): - pass + ormar_config = base_ormar_config.copy() -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_model_is_not_abstract_by_default(): - async with database: + async with base_ormar_config.database: sweden = await Nation( name="Sweden", alpha2_code="SE", region="Europe", subregion="Scandinavia" ).save() diff --git a/tests/test_inheritance_and_pydantic_generation/test_nested_models_pydantic.py b/tests/test_inheritance_and_pydantic_generation/test_nested_models_pydantic.py index a96191c..b73698f 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_nested_models_pydantic.py +++ b/tests/test_inheritance_and_pydantic_generation/test_nested_models_pydantic.py @@ -1,29 +1,20 @@ -import databases -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Library(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Package(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) library: Library = ormar.ForeignKey(Library, related_name="packages") @@ -31,8 +22,7 @@ class Package(ormar.Model): class Ticket(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) number: int = ormar.Integer() @@ -40,8 +30,7 @@ class Ticket(ormar.Model): class TicketPackage(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) status: str = ormar.String(max_length=100) @@ -49,11 +38,16 @@ class TicketPackage(ormar.Model): package: Package = ormar.ForeignKey(Package, related_name="tickets") +create_test_database = init_tests(base_ormar_config) + + def test_have_proper_children(): TicketPackageOut = TicketPackage.get_pydantic(exclude={"ticket"}) - assert "package" in TicketPackageOut.__fields__ - PydanticPackage = TicketPackageOut.__fields__["package"].type_ - assert "library" in PydanticPackage.__fields__ + assert "package" in TicketPackageOut.model_fields + PydanticPackage = TicketPackageOut.__pydantic_core_schema__["schema"]["fields"][ + "package" + ]["schema"]["schema"]["schema"]["cls"] + assert "library" in PydanticPackage.model_fields def test_casts_properly(): @@ -69,7 +63,7 @@ def test_casts_properly(): } test_package = TicketPackage(**payload) TicketPackageOut = TicketPackage.get_pydantic(exclude={"ticket"}) - parsed = TicketPackageOut(**test_package.dict()).dict() + parsed = TicketPackageOut(**test_package.model_dump()).model_dump() assert "ticket" not in parsed assert "package" in parsed assert "library" in parsed.get("package") diff --git a/tests/test_inheritance_and_pydantic_generation/test_pydantic_fields_order.py b/tests/test_inheritance_and_pydantic_generation/test_pydantic_fields_order.py index ffc515e..58bf96c 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_pydantic_fields_order.py +++ b/tests/test_inheritance_and_pydantic_generation/test_pydantic_fields_order.py @@ -1,23 +1,13 @@ -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL) +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() class NewTestModel(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() a: int = ormar.Integer(primary_key=True) b: str = ormar.String(max_length=1) @@ -27,15 +17,9 @@ class NewTestModel(ormar.Model): f: str = ormar.String(max_length=1) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_model_field_order(): TestCreate = NewTestModel.get_pydantic(exclude={"a"}) - assert list(TestCreate.__fields__.keys()) == ["b", "c", "d", "e", "f"] + assert list(TestCreate.model_fields.keys()) == ["b", "c", "d", "e", "f"] diff --git a/tests/test_inheritance_and_pydantic_generation/test_validators_are_inherited.py b/tests/test_inheritance_and_pydantic_generation/test_validators_are_inherited.py index eafce40..e6f1b5e 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_validators_are_inherited.py +++ b/tests/test_inheritance_and_pydantic_generation/test_validators_are_inherited.py @@ -1,28 +1,26 @@ import enum -import databases -import pydantic -import pytest -import sqlalchemy -from pydantic import ValidationError - import ormar -from tests.settings import DATABASE_URL +import pytest +from pydantic import ValidationError, field_validator -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL) +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() class BaseModel(ormar.Model): - class Meta: - abstract = True + ormar_config = base_ormar_config.copy(abstract=True) id: int = ormar.Integer(primary_key=True) + str_field: str = ormar.String(min_length=5, max_length=10, nullable=False) + + @field_validator("str_field") + def validate_str_field(cls, v): + if " " not in v: + raise ValueError("must contain a space") + return v class EnumExample(str, enum.Enum): @@ -32,24 +30,17 @@ class EnumExample(str, enum.Enum): class ModelExample(BaseModel): - class Meta(BaseMeta): - tablename = "examples" + ormar_config = base_ormar_config.copy(tablename="examples") - str_field: str = ormar.String(min_length=5, max_length=10, nullable=False) - enum_field: str = ormar.String( - max_length=1, nullable=False, choices=list(EnumExample) - ) - - @pydantic.validator("str_field") - def validate_str_field(cls, v): - if " " not in v: - raise ValueError("must contain a space") - return v + enum_field: str = ormar.Enum(enum_class=EnumExample, nullable=False) ModelExampleCreate = ModelExample.get_pydantic(exclude={"id"}) +create_test_database = init_tests(base_ormar_config) + + def test_ormar_validator(): ModelExample(str_field="a aaaaaa", enum_field="A") with pytest.raises(ValidationError) as e: @@ -57,7 +48,7 @@ def test_ormar_validator(): assert "must contain a space" in str(e) with pytest.raises(ValidationError) as e: ModelExample(str_field="a aaaaaaa", enum_field="Z") - assert "not in allowed choices" in str(e) + assert "Input should be 'A', 'B' or 'C'" in str(e) def test_pydantic_validator(): @@ -67,4 +58,4 @@ def test_pydantic_validator(): assert "must contain a space" in str(e) with pytest.raises(ValidationError) as e: ModelExampleCreate(str_field="a aaaaaaa", enum_field="Z") - assert "not in allowed choices" in str(e) + assert "Input should be 'A', 'B' or 'C'" in str(e) diff --git a/tests/test_inheritance_and_pydantic_generation/test_validators_in_generated_pydantic.py b/tests/test_inheritance_and_pydantic_generation/test_validators_in_generated_pydantic.py index d105481..b8fce0d 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_validators_in_generated_pydantic.py +++ b/tests/test_inheritance_and_pydantic_generation/test_validators_in_generated_pydantic.py @@ -1,22 +1,13 @@ import enum -import databases -import pydantic -import pytest -import sqlalchemy -from pydantic import ValidationError - - import ormar -from tests.settings import DATABASE_URL +import pytest +from pydantic import ValidationError, field_validator -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL) +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() class EnumExample(str, enum.Enum): @@ -26,18 +17,13 @@ class EnumExample(str, enum.Enum): class ModelExample(ormar.Model): - class Meta(ormar.ModelMeta): - database = database - metadata = metadata - tablename = "examples" + ormar_config = base_ormar_config.copy(tablename="examples") id: int = ormar.Integer(primary_key=True) str_field: str = ormar.String(min_length=5, max_length=10, nullable=False) - enum_field: str = ormar.String( - max_length=1, nullable=False, choices=list(EnumExample) - ) + enum_field: str = ormar.Enum(nullable=False, enum_class=EnumExample) - @pydantic.validator("str_field") + @field_validator("str_field") def validate_str_field(cls, v): if " " not in v: raise ValueError("must contain a space") @@ -47,6 +33,9 @@ class ModelExample(ormar.Model): ModelExampleCreate = ModelExample.get_pydantic(exclude={"id"}) +create_test_database = init_tests(base_ormar_config) + + def test_ormar_validator(): ModelExample(str_field="a aaaaaa", enum_field="A") with pytest.raises(ValidationError) as e: @@ -54,7 +43,7 @@ def test_ormar_validator(): assert "must contain a space" in str(e) with pytest.raises(ValidationError) as e: ModelExample(str_field="a aaaaaaa", enum_field="Z") - assert "not in allowed choices" in str(e) + assert "Input should be 'A', 'B' or 'C'" in str(e) def test_pydantic_validator(): @@ -64,4 +53,4 @@ def test_pydantic_validator(): assert "must contain a space" in str(e) with pytest.raises(ValidationError) as e: ModelExampleCreate(str_field="a aaaaaaa", enum_field="Z") - assert "not in allowed choices" in str(e) + assert "Input should be 'A', 'B' or 'C'" in str(e) diff --git a/tests/test_meta_constraints/test_check_constraints.py b/tests/test_meta_constraints/test_check_constraints.py index 2fc69d4..88084c3 100644 --- a/tests/test_meta_constraints/test_check_constraints.py +++ b/tests/test_meta_constraints/test_check_constraints.py @@ -1,25 +1,22 @@ import sqlite3 import asyncpg # type: ignore -import databases -import pytest -import sqlalchemy - import ormar.fields.constraints -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Product(ormar.Model): - class Meta: - tablename = "products" - metadata = metadata - database = database - constraints = [ + ormar_config = base_ormar_config.copy( + tablename="products", + constraints=[ ormar.fields.constraints.CheckColumns("inventory > buffer"), - ] + ], + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -28,20 +25,14 @@ class Product(ormar.Model): buffer: int = ormar.Integer() -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_check_columns_exclude_mysql(): - if Product.Meta.database._backend._dialect.name != "mysql": - async with database: # pragma: no cover - async with database.transaction(force_rollback=True): + if Product.ormar_config.database._backend._dialect.name != "mysql": + async with base_ormar_config.database: # pragma: no cover + async with base_ormar_config.database.transaction(force_rollback=True): await Product.objects.create( name="Mars", company="Nestle", inventory=100, buffer=10 ) diff --git a/tests/test_meta_constraints/test_index_constraints.py b/tests/test_meta_constraints/test_index_constraints.py index 21bd1b6..e3a2190 100644 --- a/tests/test_meta_constraints/test_index_constraints.py +++ b/tests/test_meta_constraints/test_index_constraints.py @@ -1,24 +1,20 @@ -import asyncpg # type: ignore -import databases -import pytest -import sqlalchemy - import ormar.fields.constraints -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Product(ormar.Model): - class Meta: - tablename = "products" - metadata = metadata - database = database - constraints = [ + ormar_config = base_ormar_config.copy( + tablename="products", + constraints=[ ormar.fields.constraints.IndexColumns("company", "name", name="my_index"), ormar.fields.constraints.IndexColumns("location", "company_type"), - ] + ], + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -27,19 +23,13 @@ class Product(ormar.Model): company_type: str = ormar.String(max_length=200) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_table_structure(): - assert len(Product.Meta.table.indexes) > 0 + assert len(Product.ormar_config.table.indexes) > 0 indexes = sorted( - list(Product.Meta.table.indexes), key=lambda x: x.name, reverse=True + list(Product.ormar_config.table.indexes), key=lambda x: x.name, reverse=True ) test_index = indexes[0] assert test_index.name == "my_index" @@ -52,8 +42,8 @@ def test_table_structure(): @pytest.mark.asyncio async def test_index_is_not_unique(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await Product.objects.create( name="Cookies", company="Nestle", location="A", company_type="B" ) diff --git a/tests/test_meta_constraints/test_unique_constraints.py b/tests/test_meta_constraints/test_unique_constraints.py index 6a1bb58..d1abd91 100644 --- a/tests/test_meta_constraints/test_unique_constraints.py +++ b/tests/test_meta_constraints/test_unique_constraints.py @@ -1,43 +1,34 @@ import sqlite3 import asyncpg # type: ignore -import databases +import ormar.fields.constraints import pymysql import pytest -import sqlalchemy -import ormar.fields.constraints -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class Product(ormar.Model): - class Meta: - tablename = "products" - metadata = metadata - database = database - constraints = [ormar.fields.constraints.UniqueColumns("name", "company")] + ormar_config = base_ormar_config.copy( + tablename="products", + constraints=[ormar.fields.constraints.UniqueColumns("name", "company")], + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) company: str = ormar.String(max_length=200) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_unique_columns(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await Product.objects.create(name="Cookies", company="Nestle") await Product.objects.create(name="Mars", company="Mars") await Product.objects.create(name="Mars", company="Nestle") diff --git a/tests/test_model_definition/pks_and_fks/test_non_integer_pkey.py b/tests/test_model_definition/pks_and_fks/test_non_integer_pkey.py index 78fb179..4ae7855 100644 --- a/tests/test_model_definition/pks_and_fks/test_non_integer_pkey.py +++ b/tests/test_model_definition/pks_and_fks/test_non_integer_pkey.py @@ -1,10 +1,10 @@ import random import databases +import ormar import pytest import sqlalchemy -import ormar from tests.settings import DATABASE_URL database = databases.Database(DATABASE_URL, force_rollback=True) @@ -16,10 +16,11 @@ def key(): class Model(ormar.Model): - class Meta: - tablename = "models" - metadata = metadata - database = database + ormar_config = ormar.OrmarConfig( + tablename="models", + metadata=metadata, + database=database, + ) id: str = ormar.String(primary_key=True, default=key, max_length=8) name: str = ormar.String(max_length=32) diff --git a/tests/test_model_definition/pks_and_fks/test_saving_string_pks.py b/tests/test_model_definition/pks_and_fks/test_saving_string_pks.py index 01085c0..8c6fedd 100644 --- a/tests/test_model_definition/pks_and_fks/test_saving_string_pks.py +++ b/tests/test_model_definition/pks_and_fks/test_saving_string_pks.py @@ -2,13 +2,13 @@ from random import choice from string import ascii_uppercase import databases +import ormar import pytest import pytest_asyncio import sqlalchemy +from ormar import Float, String from sqlalchemy import create_engine -import ormar -from ormar import Float, String from tests.settings import DATABASE_URL database = databases.Database(DATABASE_URL, force_rollback=True) @@ -19,14 +19,14 @@ def get_id() -> str: return "".join(choice(ascii_uppercase) for _ in range(12)) -class MainMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = ormar.OrmarConfig( + metadata=metadata, + database=database, +) class PositionOrm(ormar.Model): - class Meta(MainMeta): - pass + ormar_config = base_ormar_config.copy() name: str = String(primary_key=True, max_length=50) x: float = Float() @@ -35,8 +35,7 @@ class PositionOrm(ormar.Model): class PositionOrmDef(ormar.Model): - class Meta(MainMeta): - pass + ormar_config = base_ormar_config.copy() name: str = String(primary_key=True, max_length=50, default=get_id) x: float = Float() diff --git a/tests/test_model_definition/pks_and_fks/test_uuid_fks.py b/tests/test_model_definition/pks_and_fks/test_uuid_fks.py index 97f728a..5444c7e 100644 --- a/tests/test_model_definition/pks_and_fks/test_uuid_fks.py +++ b/tests/test_model_definition/pks_and_fks/test_uuid_fks.py @@ -1,11 +1,11 @@ import uuid import databases +import ormar import pytest import sqlalchemy from sqlalchemy import create_engine -import ormar from tests.settings import DATABASE_URL metadata = sqlalchemy.MetaData() @@ -13,10 +13,11 @@ db = databases.Database(DATABASE_URL) class User(ormar.Model): - class Meta: - tablename = "user" - metadata = metadata - database = db + ormar_config = ormar.OrmarConfig( + tablename="user", + metadata=metadata, + database=db, + ) id: uuid.UUID = ormar.UUID( primary_key=True, default=uuid.uuid4, uuid_format="string" @@ -29,10 +30,11 @@ class User(ormar.Model): class Token(ormar.Model): - class Meta: - tablename = "token" - metadata = metadata - database = db + ormar_config = ormar.OrmarConfig( + tablename="token", + metadata=metadata, + database=db, + ) id = ormar.Integer(primary_key=True) text = ormar.String(max_length=4, unique=True) diff --git a/tests/test_model_definition/test_aliases.py b/tests/test_model_definition/test_aliases.py index d403304..0bd3d1a 100644 --- a/tests/test_model_definition/test_aliases.py +++ b/tests/test_model_definition/test_aliases.py @@ -1,21 +1,16 @@ from typing import List, Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Child(ormar.Model): - class Meta: - tablename = "children" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="children") id: int = ormar.Integer(name="child_id", primary_key=True) first_name: str = ormar.String(name="fname", max_length=100) @@ -24,10 +19,7 @@ class Child(ormar.Model): class Artist(ormar.Model): - class Meta: - tablename = "artists" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="artists") id: int = ormar.Integer(name="artist_id", primary_key=True) first_name: str = ormar.String(name="fname", max_length=100) @@ -37,37 +29,28 @@ class Artist(ormar.Model): class Album(ormar.Model): - class Meta: - tablename = "music_albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="music_albums") id: int = ormar.Integer(name="album_id", primary_key=True) name: str = ormar.String(name="album_name", max_length=100) artist: Optional[Artist] = ormar.ForeignKey(Artist, name="artist_id") -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_table_structure(): - assert "album_id" in [x.name for x in Album.Meta.table.columns] - assert "album_name" in [x.name for x in Album.Meta.table.columns] - assert "fname" in [x.name for x in Artist.Meta.table.columns] - assert "lname" in [x.name for x in Artist.Meta.table.columns] - assert "year" in [x.name for x in Artist.Meta.table.columns] + assert "album_id" in [x.name for x in Album.ormar_config.table.columns] + assert "album_name" in [x.name for x in Album.ormar_config.table.columns] + assert "fname" in [x.name for x in Artist.ormar_config.table.columns] + assert "lname" in [x.name for x in Artist.ormar_config.table.columns] + assert "year" in [x.name for x in Artist.ormar_config.table.columns] @pytest.mark.asyncio async def test_working_with_aliases(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): artist = await Artist.objects.create( first_name="Ted", last_name="Mosbey", born_year=1975 ) @@ -124,7 +107,7 @@ async def test_working_with_aliases(): @pytest.mark.asyncio async def test_bulk_operations_and_fields(): - async with database: + async with base_ormar_config.database: d1 = Child(first_name="Daughter", last_name="1", born_year=1990) d2 = Child(first_name="Daughter", last_name="2", born_year=1991) await Child.objects.bulk_create([d1, d2]) @@ -155,8 +138,8 @@ async def test_bulk_operations_and_fields(): @pytest.mark.asyncio async def test_working_with_aliases_get_or_create(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): artist, created = await Artist.objects.get_or_create( first_name="Teddy", last_name="Bear", born_year=2020 ) @@ -169,7 +152,7 @@ async def test_working_with_aliases_get_or_create(): assert artist == artist2 assert created is False - art3 = artist2.dict() + art3 = artist2.model_dump() art3["born_year"] = 2019 await Artist.objects.update_or_create(**art3) diff --git a/tests/test_model_definition/test_columns.py b/tests/test_model_definition/test_columns.py index 56c9c74..3fbf8f8 100644 --- a/tests/test_model_definition/test_columns.py +++ b/tests/test_model_definition/test_columns.py @@ -1,17 +1,15 @@ import datetime from enum import Enum -import databases +import ormar import pydantic import pytest -import sqlalchemy - -import ormar from ormar import ModelDefinitionError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config(force_rollback=True) def time(): @@ -24,10 +22,7 @@ class MyEnum(Enum): class Example(ormar.Model): - class Meta: - tablename = "example" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="example") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200, default="aaa") @@ -41,25 +36,17 @@ class Example(ormar.Model): class EnumExample(ormar.Model): - class Meta: - tablename = "enum_example" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="enum_example") id: int = ormar.Integer(primary_key=True) size: MyEnum = ormar.Enum(enum_class=MyEnum, default=MyEnum.SMALL) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_proper_enum_column_type(): - assert Example.__fields__["size"].type_ == MyEnum + assert Example.model_fields["size"].__type__ == MyEnum def test_accepts_only_proper_enums(): @@ -73,7 +60,7 @@ def test_accepts_only_proper_enums(): @pytest.mark.asyncio async def test_enum_bulk_operations(): - async with database: + async with base_ormar_config.database: examples = [EnumExample(), EnumExample()] await EnumExample.objects.bulk_create(examples) @@ -90,7 +77,7 @@ async def test_enum_bulk_operations(): @pytest.mark.asyncio async def test_enum_filter(): - async with database: + async with base_ormar_config.database: examples = [EnumExample(), EnumExample(size=MyEnum.BIG)] await EnumExample.objects.bulk_create(examples) @@ -103,7 +90,7 @@ async def test_enum_filter(): @pytest.mark.asyncio async def test_model_crud(): - async with database: + async with base_ormar_config.database: example = Example() await example.save() @@ -130,15 +117,12 @@ async def test_model_crud(): @pytest.mark.asyncio -async def test_invalid_enum_field(): - async with database: +async def test_invalid_enum_field() -> None: + async with base_ormar_config.database: with pytest.raises(ModelDefinitionError): class Example2(ormar.Model): - class Meta: - tablename = "example" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="example2") id: int = ormar.Integer(primary_key=True) - size: MyEnum = ormar.Enum(enum_class=[]) + size: MyEnum = ormar.Enum(enum_class=[]) # type: ignore diff --git a/tests/test_model_definition/test_create_uses_init_for_consistency.py b/tests/test_model_definition/test_create_uses_init_for_consistency.py index 5440527..80cd772 100644 --- a/tests/test_model_definition/test_create_uses_init_for_consistency.py +++ b/tests/test_model_definition/test_create_uses_init_for_consistency.py @@ -1,21 +1,14 @@ import uuid from typing import ClassVar -import databases -import pytest -import sqlalchemy -from pydantic import root_validator - import ormar -from tests.settings import DATABASE_URL +import pytest +from pydantic import model_validator -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() class Mol(ormar.Model): @@ -24,8 +17,7 @@ class Mol(ormar.Model): "12345678-abcd-1234-abcd-123456789abc" ) - class Meta(BaseMeta): - tablename = "mols" + ormar_config = base_ormar_config.copy(tablename="mols") id: uuid.UUID = ormar.UUID(primary_key=True, index=True, uuid_format="hex") smiles: str = ormar.String(nullable=False, unique=True, max_length=256) @@ -36,7 +28,7 @@ class Mol(ormar.Model): kwargs["id"] = self._UUID_NAMESPACE super().__init__(**kwargs) - @root_validator() + @model_validator(mode="before") def make_canonical_smiles_and_uuid(cls, values): values["id"], values["smiles"] = cls.uuid(values["smiles"]) return values @@ -47,17 +39,12 @@ class Mol(ormar.Model): return id_, smiles -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_json_column(): - async with database: + async with base_ormar_config.database: await Mol.objects.create(smiles="Cc1ccccc1") count = await Mol.objects.count() assert count == 1 diff --git a/tests/test_model_definition/test_dates_with_timezone.py b/tests/test_model_definition/test_dates_with_timezone.py index afb6fbd..42917ec 100644 --- a/tests/test_model_definition/test_dates_with_timezone.py +++ b/tests/test_model_definition/test_dates_with_timezone.py @@ -1,21 +1,16 @@ -from datetime import timezone, timedelta, datetime, date, time - -import databases -import pytest -import sqlalchemy +from datetime import date, datetime, time, timedelta, timezone import ormar +import pytest -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class DateFieldsModel(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) created_date: datetime = ormar.DateTime( @@ -29,27 +24,21 @@ class DateFieldsModel(ormar.Model): class SampleModel(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) updated_at: datetime = ormar.DateTime() class TimeModel(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) elapsed: time = ormar.Time() class DateModel(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) creation_date: date = ormar.Date() @@ -59,24 +48,15 @@ class MyModel(ormar.Model): id: int = ormar.Integer(primary_key=True) created_at: datetime = ormar.DateTime(timezone=True, nullable=False) - class Meta: - tablename = "mymodels" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="mymodels") -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_model_crud_with_timezone(): - async with database: + async with base_ormar_config.database: datemodel = await DateFieldsModel().save() assert datemodel.created_date is not None assert datemodel.updated_date is not None @@ -84,7 +64,7 @@ async def test_model_crud_with_timezone(): @pytest.mark.asyncio async def test_query_with_datetime_in_filter(): - async with database: + async with base_ormar_config.database: creation_dt = datetime(2021, 5, 18, 0, 0, 0, 0) sample = await SampleModel.objects.create(updated_at=creation_dt) @@ -98,7 +78,7 @@ async def test_query_with_datetime_in_filter(): @pytest.mark.asyncio async def test_query_with_date_in_filter(): - async with database: + async with base_ormar_config.database: sample = await TimeModel.objects.create(elapsed=time(0, 20, 20)) await TimeModel.objects.create(elapsed=time(0, 12, 0)) await TimeModel.objects.create(elapsed=time(0, 19, 55)) @@ -114,7 +94,7 @@ async def test_query_with_date_in_filter(): @pytest.mark.asyncio async def test_query_with_time_in_filter(): - async with database: + async with base_ormar_config.database: await DateModel.objects.create(creation_date=date(2021, 5, 18)) sample2 = await DateModel.objects.create(creation_date=date(2021, 5, 19)) sample3 = await DateModel.objects.create(creation_date=date(2021, 5, 20)) @@ -130,7 +110,7 @@ async def test_query_with_time_in_filter(): @pytest.mark.asyncio async def test_filtering_by_timezone_with_timedelta(): - async with database: + async with base_ormar_config.database: now_utc = datetime.now(timezone.utc) object = MyModel(created_at=now_utc) await object.save() diff --git a/tests/test_model_definition/test_equality_and_hash.py b/tests/test_model_definition/test_equality_and_hash.py index b012166..f5b3cd3 100644 --- a/tests/test_model_definition/test_equality_and_hash.py +++ b/tests/test_model_definition/test_equality_and_hash.py @@ -1,38 +1,26 @@ # type: ignore -import databases -import pytest -import sqlalchemy - import ormar -from ormar import ModelDefinitionError, property_field -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Song(ormar.Model): - class Meta: - tablename = "songs" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="songs") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_equality(): - async with database: + async with base_ormar_config.database: song1 = await Song.objects.create(name="Song") song2 = await Song.objects.create(name="Song") song3 = Song(name="Song") @@ -49,7 +37,7 @@ async def test_equality(): @pytest.mark.asyncio async def test_hash_doesnt_change_with_fields_if_pk(): - async with database: + async with base_ormar_config.database: song1 = await Song.objects.create(name="Song") prev_hash = hash(song1) @@ -59,7 +47,7 @@ async def test_hash_doesnt_change_with_fields_if_pk(): @pytest.mark.asyncio async def test_hash_changes_with_fields_if_no_pk(): - async with database: + async with base_ormar_config.database: song1 = Song(name="Song") prev_hash = hash(song1) diff --git a/tests/test_model_definition/test_extra_ignore_parameter.py b/tests/test_model_definition/test_extra_ignore_parameter.py index cd3960c..733faf2 100644 --- a/tests/test_model_definition/test_extra_ignore_parameter.py +++ b/tests/test_model_definition/test_extra_ignore_parameter.py @@ -1,26 +1,26 @@ -import databases -import sqlalchemy - import ormar from ormar import Extra -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config(force_rollback=True) class Child(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "children" - metadata = metadata - database = database - extra = Extra.ignore + ormar_config = base_ormar_config.copy( + tablename="children", + extra=Extra.ignore, + ) id: int = ormar.Integer(name="child_id", primary_key=True) first_name: str = ormar.String(name="fname", max_length=100) last_name: str = ormar.String(name="lname", max_length=100) +create_test_database = init_tests(base_ormar_config) + + def test_allow_extra_parameter(): child = Child(first_name="Test", last_name="Name", extra_param="Unexpected") assert child.first_name == "Test" diff --git a/tests/test_model_definition/test_field_quoting.py b/tests/test_model_definition/test_field_quoting.py index 4cf6568..c903334 100644 --- a/tests/test_model_definition/test_field_quoting.py +++ b/tests/test_model_definition/test_field_quoting.py @@ -1,57 +1,43 @@ -import asyncio from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config(force_rollback=True) class SchoolClass(ormar.Model): - class Meta: - tablename = "app.schoolclasses" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="app.schoolclasses") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Category(ormar.Model): - class Meta: - tablename = "app.categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="app.categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Student(ormar.Model): - class Meta: - tablename = "app.students" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="app.students") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) gpa: float = ormar.Float() - schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass, related_name="students") - category: Optional[Category] = ormar.ForeignKey(Category, nullable=True, related_name="students") + schoolclass: Optional[SchoolClass] = ormar.ForeignKey( + SchoolClass, related_name="students" + ) + category: Optional[Category] = ormar.ForeignKey( + Category, nullable=True, related_name="students" + ) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) async def create_data(): @@ -59,27 +45,37 @@ async def create_data(): class2 = await SchoolClass.objects.create(name="Logic") category = await Category.objects.create(name="Foreign") category2 = await Category.objects.create(name="Domestic") - await Student.objects.create(name="Jane", category=category, schoolclass=class1, gpa=3.2) - await Student.objects.create(name="Judy", category=category2, schoolclass=class1, gpa=2.6) - await Student.objects.create(name="Jack", category=category2, schoolclass=class2, gpa=3.8) + await Student.objects.create( + name="Jane", category=category, schoolclass=class1, gpa=3.2 + ) + await Student.objects.create( + name="Judy", category=category2, schoolclass=class1, gpa=2.6 + ) + await Student.objects.create( + name="Jack", category=category2, schoolclass=class2, gpa=3.8 + ) @pytest.mark.asyncio async def test_quotes_left_join(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await create_data() students = await Student.objects.filter( - (Student.schoolclass.name == "Math") | (Student.category.name == "Foreign") + (Student.schoolclass.name == "Math") + | (Student.category.name == "Foreign") ).all() for student in students: - assert student.schoolclass.name == "Math" or student.category.name == "Foreign" + assert ( + student.schoolclass.name == "Math" + or student.category.name == "Foreign" + ) @pytest.mark.asyncio async def test_quotes_reverse_join(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await create_data() schoolclasses = await SchoolClass.objects.filter(students__gpa__gt=3).all() for schoolclass in schoolclasses: @@ -89,11 +85,12 @@ async def test_quotes_reverse_join(): @pytest.mark.asyncio async def test_quotes_deep_join(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await create_data() - schoolclasses = await SchoolClass.objects.filter(students__category__name="Domestic").all() + schoolclasses = await SchoolClass.objects.filter( + students__category__name="Domestic" + ).all() for schoolclass in schoolclasses: for student in schoolclass.students: assert student.category.name == "Domestic" - diff --git a/tests/test_model_definition/test_fields_access.py b/tests/test_model_definition/test_fields_access.py index 5140b3e..538f825 100644 --- a/tests/test_model_definition/test_fields_access.py +++ b/tests/test_model_definition/test_fields_access.py @@ -1,31 +1,22 @@ -import databases -import pytest -import sqlalchemy - import ormar +import pytest from ormar import BaseField -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class PriceList(ormar.Model): - class Meta(BaseMeta): - tablename = "price_lists" + ormar_config = base_ormar_config.copy(tablename="price_lists") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -33,8 +24,7 @@ class Category(ormar.Model): class Product(ormar.Model): - class Meta(BaseMeta): - tablename = "product" + ormar_config = base_ormar_config.copy(tablename="product") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -42,19 +32,13 @@ class Product(ormar.Model): category = ormar.ForeignKey(Category) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_fields_access(): # basic access - assert Product.id._field == Product.Meta.model_fields["id"] - assert Product.id.id == Product.Meta.model_fields["id"] + assert Product.id._field == Product.ormar_config.model_fields["id"] + assert Product.id.id == Product.ormar_config.model_fields["id"] assert Product.pk.id == Product.id.id assert isinstance(Product.id._field, BaseField) assert Product.id._access_chain == "id" @@ -62,19 +46,19 @@ def test_fields_access(): # nested models curr_field = Product.category.name - assert curr_field._field == Category.Meta.model_fields["name"] + assert curr_field._field == Category.ormar_config.model_fields["name"] assert curr_field._access_chain == "category__name" assert curr_field._source_model == Product # deeper nesting curr_field = Product.category.price_lists.name - assert curr_field._field == PriceList.Meta.model_fields["name"] + assert curr_field._field == PriceList.ormar_config.model_fields["name"] assert curr_field._access_chain == "category__price_lists__name" assert curr_field._source_model == Product # reverse nesting curr_field = PriceList.categories.products.rating - assert curr_field._field == Product.Meta.model_fields["rating"] + assert curr_field._field == Product.ormar_config.model_fields["rating"] assert curr_field._access_chain == "categories__products__rating" assert curr_field._source_model == PriceList @@ -191,8 +175,8 @@ def test_combining_groups_together(): @pytest.mark.asyncio async def test_filtering_by_field_access(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): category = await Category(name="Toys").save() product2 = await Product( name="My Little Pony", rating=3.8, category=category diff --git a/tests/test_model_definition/test_foreign_key_value_used_for_related_model.py b/tests/test_model_definition/test_foreign_key_value_used_for_related_model.py index cc24cb4..6f0e9b7 100644 --- a/tests/test_model_definition/test_foreign_key_value_used_for_related_model.py +++ b/tests/test_model_definition/test_foreign_key_value_used_for_related_model.py @@ -1,25 +1,17 @@ import uuid -from typing import List, Optional - -import databases -import pytest -import sqlalchemy +from typing import Optional import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class PageLink(ormar.Model): - class Meta(BaseMeta): - tablename = "pagelinks" + ormar_config = base_ormar_config.copy(tablename="pagelinks") id: int = ormar.Integer(primary_key=True) value: str = ormar.String(max_length=2048) @@ -27,8 +19,7 @@ class PageLink(ormar.Model): class Post(ormar.Model): - class Meta(BaseMeta): - tablename = "posts" + ormar_config = base_ormar_config.copy(tablename="posts") id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=500) @@ -38,16 +29,14 @@ class Post(ormar.Model): class Department(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4()) name: str = ormar.String(max_length=100) class Course(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -55,18 +44,13 @@ class Course(ormar.Model): department: Optional[Department] = ormar.ForeignKey(Department) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_pass_int_values_as_fk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): link = await PageLink(id=1, value="test", country="USA").save() await Post.objects.create(title="My post", link=link.id) post_check = await Post.objects.select_related("link").get() @@ -75,7 +59,7 @@ async def test_pass_int_values_as_fk(): @pytest.mark.asyncio async def test_pass_uuid_value_as_fk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): dept = await Department(name="Department test").save() await Course(name="Test course", department=dept.id).save() diff --git a/tests/test_model_definition/test_iterate.py b/tests/test_model_definition/test_iterate.py index 54fd324..31e64ff 100644 --- a/tests/test_model_definition/test_iterate.py +++ b/tests/test_model_definition/test_iterate.py @@ -1,31 +1,24 @@ import uuid -import databases -import pytest -import sqlalchemy import ormar +import pytest from ormar.exceptions import QueryDefinitionError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class User(ormar.Model): - class Meta: - tablename = "users3" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="users3") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, default="") class User2(ormar.Model): - class Meta: - tablename = "users4" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="users4") id: uuid.UUID = ormar.UUID( uuid_format="string", primary_key=True, default=uuid.uuid4 @@ -34,10 +27,7 @@ class User2(ormar.Model): class Task(ormar.Model): - class Meta: - tablename = "tasks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="tasks") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, default="") @@ -45,10 +35,7 @@ class Task(ormar.Model): class Task2(ormar.Model): - class Meta: - tablename = "tasks2" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="tasks2") id: uuid.UUID = ormar.UUID( uuid_format="string", primary_key=True, default=uuid.uuid4 @@ -57,27 +44,21 @@ class Task2(ormar.Model): user: User2 = ormar.ForeignKey(to=User2) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_empty_result(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): async for user in User.objects.iterate(): pass # pragma: no cover @pytest.mark.asyncio async def test_model_iterator(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): tom = await User.objects.create(name="Tom") jane = await User.objects.create(name="Jane") lucy = await User.objects.create(name="Lucy") @@ -88,8 +69,8 @@ async def test_model_iterator(): @pytest.mark.asyncio async def test_model_iterator_filter(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): tom = await User.objects.create(name="Tom") await User.objects.create(name="Jane") await User.objects.create(name="Lucy") @@ -100,8 +81,8 @@ async def test_model_iterator_filter(): @pytest.mark.asyncio async def test_model_iterator_relations(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): tom = await User.objects.create(name="Tom") jane = await User.objects.create(name="Jane") lucy = await User.objects.create(name="Lucy") @@ -120,8 +101,8 @@ async def test_model_iterator_relations(): @pytest.mark.asyncio async def test_model_iterator_relations_queryset_proxy(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): tom = await User.objects.create(name="Tom") jane = await User.objects.create(name="Jane") @@ -146,8 +127,8 @@ async def test_model_iterator_relations_queryset_proxy(): @pytest.mark.asyncio async def test_model_iterator_uneven_number_of_relations(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): tom = await User.objects.create(name="Tom") jane = await User.objects.create(name="Jane") lucy = await User.objects.create(name="Lucy") @@ -168,8 +149,8 @@ async def test_model_iterator_uneven_number_of_relations(): @pytest.mark.asyncio async def test_model_iterator_uuid_pk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): tom = await User2.objects.create(name="Tom") jane = await User2.objects.create(name="Jane") lucy = await User2.objects.create(name="Lucy") @@ -180,8 +161,8 @@ async def test_model_iterator_uuid_pk(): @pytest.mark.asyncio async def test_model_iterator_filter_uuid_pk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): tom = await User2.objects.create(name="Tom") await User2.objects.create(name="Jane") await User2.objects.create(name="Lucy") @@ -192,8 +173,8 @@ async def test_model_iterator_filter_uuid_pk(): @pytest.mark.asyncio async def test_model_iterator_relations_uuid_pk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): tom = await User2.objects.create(name="Tom") jane = await User2.objects.create(name="Jane") lucy = await User2.objects.create(name="Lucy") @@ -212,8 +193,8 @@ async def test_model_iterator_relations_uuid_pk(): @pytest.mark.asyncio async def test_model_iterator_relations_queryset_proxy_uuid_pk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): tom = await User2.objects.create(name="Tom") jane = await User2.objects.create(name="Jane") @@ -238,8 +219,8 @@ async def test_model_iterator_relations_queryset_proxy_uuid_pk(): @pytest.mark.asyncio async def test_model_iterator_uneven_number_of_relations_uuid_pk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): tom = await User2.objects.create(name="Tom") jane = await User2.objects.create(name="Jane") lucy = await User2.objects.create(name="Lucy") @@ -262,7 +243,7 @@ async def test_model_iterator_uneven_number_of_relations_uuid_pk(): @pytest.mark.asyncio async def test_model_iterator_with_prefetch_raises_error(): - async with database: + async with base_ormar_config.database: with pytest.raises(QueryDefinitionError): async for user in User.objects.prefetch_related(User.tasks).iterate(): pass # pragma: no cover diff --git a/tests/test_model_definition/test_model_construct.py b/tests/test_model_definition/test_model_construct.py index e2d7876..b4f7ce3 100644 --- a/tests/test_model_definition/test_model_construct.py +++ b/tests/test_model_definition/test_model_construct.py @@ -1,38 +1,27 @@ from typing import List -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class NickNames(ormar.Model): - class Meta: - tablename = "nicks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="nicks") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") class NicksHq(ormar.Model): - class Meta: - tablename = "nicks_x_hq" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="nicks_x_hq") class HQ(ormar.Model): - class Meta: - tablename = "hqs" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="hqs") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -40,10 +29,7 @@ class HQ(ormar.Model): class Company(ormar.Model): - class Meta: - tablename = "companies" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="companies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="company_name") @@ -51,47 +37,47 @@ class Company(ormar.Model): hq: HQ = ormar.ForeignKey(HQ) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_construct_with_empty_relation(): - async with database: - async with database.transaction(force_rollback=True): - hq = await HQ.objects.create(name="Main") + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): + await HQ.objects.create(name="Main") comp = Company(name="Banzai", hq=None, founded=1988) - comp2 = Company.construct(**dict(name="Banzai", hq=None, founded=1988)) - assert comp.dict() == comp2.dict() + comp2 = Company.model_construct( + **dict(name="Banzai", hq=None, founded=1988) + ) + assert comp.model_dump() == comp2.model_dump() @pytest.mark.asyncio async def test_init_and_construct_has_same_effect(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): hq = await HQ.objects.create(name="Main") comp = Company(name="Banzai", hq=hq, founded=1988) - comp2 = Company.construct(**dict(name="Banzai", hq=hq, founded=1988)) - assert comp.dict() == comp2.dict() + comp2 = Company.model_construct(**dict(name="Banzai", hq=hq, founded=1988)) + assert comp.model_dump() == comp2.model_dump() - comp3 = Company.construct(**dict(name="Banzai", hq=hq.dict(), founded=1988)) - assert comp.dict() == comp3.dict() + comp3 = Company.model_construct( + **dict(name="Banzai", hq=hq.model_dump(), founded=1988) + ) + assert comp.model_dump() == comp3.model_dump() @pytest.mark.asyncio async def test_init_and_construct_has_same_effect_with_m2m(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): n1 = await NickNames(name="test").save() n2 = await NickNames(name="test2").save() hq = HQ(name="Main", nicks=[n1, n2]) - hq2 = HQ.construct(**dict(name="Main", nicks=[n1, n2])) - assert hq.dict() == hq2.dict() + hq2 = HQ.model_construct(**dict(name="Main", nicks=[n1, n2])) + assert hq.model_dump() == hq2.model_dump() - hq3 = HQ.construct(**dict(name="Main", nicks=[n1.dict(), n2.dict()])) - assert hq.dict() == hq3.dict() + hq3 = HQ.model_construct( + **dict(name="Main", nicks=[n1.model_dump(), n2.model_dump()]) + ) + assert hq.model_dump() == hq3.model_dump() diff --git a/tests/test_model_definition/test_model_definition.py b/tests/test_model_definition/test_model_definition.py index b0b2adf..d3ca604 100644 --- a/tests/test_model_definition/test_model_definition.py +++ b/tests/test_model_definition/test_model_definition.py @@ -1,30 +1,23 @@ # type: ignore -import asyncio import datetime import decimal - -import databases -import pydantic -import pytest -import pytest_asyncio -import sqlalchemy import typing import ormar +import pydantic +import pytest +import sqlalchemy from ormar.exceptions import ModelDefinitionError from ormar.models import Model -from tests.settings import DATABASE_URL -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) +base_ormar_config = create_config() class ExampleModel(Model): - class Meta: - tablename = "example" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="example") test: int = ormar.Integer(primary_key=True) test_string: str = ormar.String(max_length=250) @@ -55,21 +48,13 @@ fields_to_check = [ class ExampleModel2(Model): - class Meta: - tablename = "examples" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="example2") test: int = ormar.Integer(primary_key=True) test_string: str = ormar.String(max_length=250) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.fixture() @@ -84,7 +69,7 @@ def example(): def test_not_nullable_field_is_required(): - with pytest.raises(pydantic.error_wrappers.ValidationError): + with pytest.raises(pydantic.ValidationError): ExampleModel(test=1, test_string="test") @@ -116,9 +101,10 @@ def test_missing_metadata(): with pytest.raises(ModelDefinitionError): class JsonSample2(ormar.Model): - class Meta: - tablename = "jsons2" - database = database + ormar_config = ormar.OrmarConfig( + tablename="jsons2", + database=base_ormar_config.database, + ) id: int = ormar.Integer(primary_key=True) test_json = ormar.JSON(nullable=True) @@ -128,8 +114,18 @@ def test_missing_database(): with pytest.raises(ModelDefinitionError): class JsonSample3(ormar.Model): - class Meta: - tablename = "jsons3" + ormar_config = ormar.OrmarConfig(tablename="jsons3") + + id: int = ormar.Integer(primary_key=True) + test_json = ormar.JSON(nullable=True) + + +def test_wrong_pydantic_config(): + with pytest.raises(ModelDefinitionError): + + class ErrorSample(ormar.Model): + model_config = ["test"] + ormar_config = ormar.OrmarConfig(tablename="jsons3") id: int = ormar.Integer(primary_key=True) test_json = ormar.JSON(nullable=True) @@ -150,13 +146,15 @@ def test_primary_key_access_and_setting(example): def test_pydantic_model_is_created(example): assert issubclass(example.__class__, pydantic.BaseModel) - assert all([field in example.__fields__ for field in fields_to_check]) + assert all([field in example.model_fields for field in fields_to_check]) assert example.test == 1 def test_sqlalchemy_table_is_created(example): - assert issubclass(example.Meta.table.__class__, sqlalchemy.Table) - assert all([field in example.Meta.table.columns for field in fields_to_check]) + assert issubclass(example.ormar_config.table.__class__, sqlalchemy.Table) + assert all( + [field in example.ormar_config.table.columns for field in fields_to_check] + ) @typing.no_type_check @@ -164,10 +162,7 @@ def test_no_pk_in_model_definition(): with pytest.raises(ModelDefinitionError): # type: ignore class ExampleModel2(Model): # type: ignore - class Meta: - tablename = "example2" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="example2") test_string: str = ormar.String(max_length=250) # type: ignore @@ -178,37 +173,18 @@ def test_two_pks_in_model_definition(): @typing.no_type_check class ExampleModel2(Model): - class Meta: - tablename = "example3" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="example3") id: int = ormar.Integer(primary_key=True) test_string: str = ormar.String(max_length=250, primary_key=True) -@typing.no_type_check -def test_setting_pk_column_as_pydantic_only_in_model_definition(): - with pytest.raises(ModelDefinitionError): - - class ExampleModel2(Model): - class Meta: - tablename = "example4" - database = database - metadata = metadata - - test: int = ormar.Integer(primary_key=True, pydantic_only=True) - - @typing.no_type_check def test_decimal_error_in_model_definition(): with pytest.raises(ModelDefinitionError): class ExampleModel2(Model): - class Meta: - tablename = "example5" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="example5") test: decimal.Decimal = ormar.Decimal(primary_key=True) @@ -218,10 +194,7 @@ def test_binary_error_without_length_model_definition(): with pytest.raises(ModelDefinitionError): class ExampleModel2(Model): - class Meta: - tablename = "example6" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="example6") test: bytes = ormar.LargeBinary(primary_key=True, max_length=-1) @@ -231,10 +204,7 @@ def test_string_error_in_model_definition(): with pytest.raises(ModelDefinitionError): class ExampleModel2(Model): - class Meta: - tablename = "example6" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="example6") test: str = ormar.String(primary_key=True, max_length=0) diff --git a/tests/test_model_definition/test_models.py b/tests/test_model_definition/test_models.py index d5e911a..df35bff 100644 --- a/tests/test_model_definition/test_models.py +++ b/tests/test_model_definition/test_models.py @@ -3,26 +3,22 @@ import base64 import datetime import os import uuid -from typing import List +from enum import Enum -import databases +import ormar import pydantic import pytest import sqlalchemy - -import ormar from ormar.exceptions import ModelError, NoMatch, QueryDefinitionError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class JsonSample(ormar.Model): - class Meta: - tablename = "jsons" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="jsons") id: int = ormar.Integer(primary_key=True) test_json = ormar.JSON(nullable=True) @@ -33,13 +29,10 @@ blob2 = b"test2icac89uc98" class LargeBinarySample(ormar.Model): - class Meta: - tablename = "my_bolbs" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="my_bolbs") id: int = ormar.Integer(primary_key=True) - test_binary: bytes = ormar.LargeBinary(max_length=100000, choices=[blob, blob2]) + test_binary: bytes = ormar.LargeBinary(max_length=100000) blob3 = os.urandom(64) @@ -47,47 +40,34 @@ blob4 = os.urandom(100) class LargeBinaryStr(ormar.Model): - class Meta: - tablename = "my_str_blobs" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="my_str_bolbs") id: int = ormar.Integer(primary_key=True) test_binary: str = ormar.LargeBinary( - max_length=100000, choices=[blob3, blob4], represent_as_base64_str=True + max_length=100000, represent_as_base64_str=True ) class LargeBinaryNullableStr(ormar.Model): - class Meta: - tablename = "my_str_blobs2" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="my_str_bolbs2") id: int = ormar.Integer(primary_key=True) test_binary: str = ormar.LargeBinary( max_length=100000, - choices=[blob3, blob4], represent_as_base64_str=True, nullable=True, ) class UUIDSample(ormar.Model): - class Meta: - tablename = "uuids" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="uuids") id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4) test_text: str = ormar.Text() class UUIDSample2(ormar.Model): - class Meta: - tablename = "uuids2" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="uuids2") id: uuid.UUID = ormar.UUID( primary_key=True, default=uuid.uuid4, uuid_format="string" @@ -96,95 +76,78 @@ class UUIDSample2(ormar.Model): class User(ormar.Model): - class Meta: - tablename = "users" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="users") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, default="") class User2(ormar.Model): - class Meta: - tablename = "users2" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="users2") id: str = ormar.String(primary_key=True, max_length=100) name: str = ormar.String(max_length=100, default="") class Product(ormar.Model): - class Meta: - tablename = "product" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="product") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) rating: int = ormar.Integer(minimum=1, maximum=5) in_stock: bool = ormar.Boolean(default=False) - last_delivery: datetime.date = ormar.Date(default=datetime.datetime.now) + last_delivery: datetime.date = ormar.Date(default=datetime.date.today) -country_name_choices = ("Canada", "Algeria", "United States", "Belize") -country_taxed_choices = (True,) -country_country_code_choices = (-10, 1, 213, 1200) +class CountryNameEnum(Enum): + CANADA = "Canada" + ALGERIA = "Algeria" + USA = "United States" + BELIZE = "Belize" + + +class CountryCodeEnum(int, Enum): + MINUS_TEN = -10 + ONE = 1 + TWO_HUNDRED_THIRTEEN = 213 + THOUSAND_TWO_HUNDRED = 1200 class Country(ormar.Model): - class Meta: - tablename = "country" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="country") id: int = ormar.Integer(primary_key=True) - name: str = ormar.String( - max_length=9, choices=country_name_choices, default="Canada" - ) - taxed: bool = ormar.Boolean(choices=country_taxed_choices, default=True) - country_code: int = ormar.Integer( - minimum=0, maximum=1000, choices=country_country_code_choices, default=1 - ) + name: CountryNameEnum = ormar.Enum(enum_class=CountryNameEnum, default="Canada") + taxed: bool = ormar.Boolean(default=True) + country_code: int = ormar.Enum(enum_class=CountryCodeEnum, default=1) class NullableCountry(ormar.Model): - class Meta: - tablename = "country2" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="country2") id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=9, choices=country_name_choices, nullable=True) + name: CountryNameEnum = ormar.Enum(enum_class=CountryNameEnum, nullable=True) class NotNullableCountry(ormar.Model): - class Meta: - tablename = "country3" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="country3") id: int = ormar.Integer(primary_key=True) - name: str = ormar.String(max_length=9, choices=country_name_choices, nullable=False) + name: CountryNameEnum = ormar.Enum(enum_class=CountryNameEnum, nullable=False) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_model_class(): - assert list(User.Meta.model_fields.keys()) == ["id", "name"] - assert issubclass(User.Meta.model_fields["id"].__class__, pydantic.fields.FieldInfo) - assert User.Meta.model_fields["id"].primary_key is True - assert isinstance(User.Meta.model_fields["name"], pydantic.fields.FieldInfo) - assert User.Meta.model_fields["name"].max_length == 100 - assert isinstance(User.Meta.table, sqlalchemy.Table) + assert list(User.ormar_config.model_fields.keys()) == ["id", "name"] + assert issubclass( + User.ormar_config.model_fields["id"].__class__, pydantic.fields.FieldInfo + ) + assert User.ormar_config.model_fields["id"].primary_key is True + assert isinstance(User.ormar_config.model_fields["name"], pydantic.fields.FieldInfo) + assert User.ormar_config.model_fields["name"].max_length == 100 + assert isinstance(User.ormar_config.table, sqlalchemy.Table) def test_wrong_field_name(): @@ -200,8 +163,8 @@ def test_model_pk(): @pytest.mark.asyncio async def test_json_column(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await JsonSample.objects.create(test_json=dict(aa=12)) await JsonSample.objects.create(test_json='{"aa": 12}') @@ -216,8 +179,8 @@ async def test_json_column(): @pytest.mark.asyncio async def test_binary_column(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await LargeBinarySample.objects.create(test_binary=blob) await LargeBinarySample.objects.create(test_binary=blob2) @@ -232,8 +195,8 @@ async def test_binary_column(): @pytest.mark.asyncio async def test_binary_str_column(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await LargeBinaryStr(test_binary=blob3).save() await LargeBinaryStr.objects.create(test_binary=blob4) @@ -248,8 +211,8 @@ async def test_binary_str_column(): @pytest.mark.asyncio async def test_binary_nullable_str_column(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await LargeBinaryNullableStr().save() await LargeBinaryNullableStr.objects.create() items = await LargeBinaryNullableStr.objects.all() @@ -279,8 +242,8 @@ async def test_binary_nullable_str_column(): @pytest.mark.asyncio async def test_uuid_column(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): u1 = await UUIDSample.objects.create(test_text="aa") u2 = await UUIDSample.objects.create(test_text="bb") @@ -303,7 +266,7 @@ async def test_uuid_column(): assert item2.id == item3.id assert isinstance(item3.id, uuid.UUID) - u3 = await UUIDSample2(**u1.dict()).save() + u3 = await UUIDSample2(**u1.model_dump()).save() u1_2 = await UUIDSample.objects.get(pk=u3.id) assert u1_2 == u1 @@ -314,8 +277,8 @@ async def test_uuid_column(): @pytest.mark.asyncio async def test_model_crud(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): users = await User.objects.all() assert users == [] @@ -341,8 +304,8 @@ async def test_model_crud(): @pytest.mark.asyncio async def test_model_get(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): with pytest.raises(ormar.NoMatch): await User.objects.get() @@ -366,8 +329,8 @@ async def test_model_get(): @pytest.mark.asyncio async def test_model_filter(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await User.objects.create(name="Tom") await User.objects.create(name="Jane") await User.objects.create(name="Lucy") @@ -422,7 +385,7 @@ async def test_model_filter(): @pytest.mark.asyncio async def test_wrong_query_contains_model(): - async with database: + async with base_ormar_config.database: with pytest.raises(QueryDefinitionError): product = Product(name="90%-Cotton", rating=2) await Product.objects.filter(name__contains=product).count() @@ -430,8 +393,8 @@ async def test_wrong_query_contains_model(): @pytest.mark.asyncio async def test_model_exists(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await User.objects.create(name="Tom") assert await User.objects.filter(name="Tom").exists() is True assert await User.objects.filter(name="Jane").exists() is False @@ -439,8 +402,8 @@ async def test_model_exists(): @pytest.mark.asyncio async def test_model_count(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await User.objects.create(name="Tom") await User.objects.create(name="Jane") await User.objects.create(name="Lucy") @@ -451,8 +414,8 @@ async def test_model_count(): @pytest.mark.asyncio async def test_model_limit(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await User.objects.create(name="Tom") await User.objects.create(name="Jane") await User.objects.create(name="Lucy") @@ -462,8 +425,8 @@ async def test_model_limit(): @pytest.mark.asyncio async def test_model_limit_with_filter(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await User.objects.create(name="Tom") await User.objects.create(name="Tom") await User.objects.create(name="Tom") @@ -475,8 +438,8 @@ async def test_model_limit_with_filter(): @pytest.mark.asyncio async def test_offset(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await User.objects.create(name="Tom") await User.objects.create(name="Jane") @@ -486,8 +449,8 @@ async def test_offset(): @pytest.mark.asyncio async def test_model_first(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): tom = await User.objects.create(name="Tom") jane = await User.objects.create(name="Jane") @@ -500,27 +463,11 @@ async def test_model_first(): assert await User.objects.order_by("name").first() == jane -def not_contains(a, b): - return a not in b - - -def contains(a, b): - return a in b - - -def check_choices(values: tuple, ops: List): - ops_dict = {"in": contains, "out": not_contains} - checks = (country_name_choices, country_taxed_choices, country_country_code_choices) - assert all( - [ops_dict[op](value, check) for value, op, check in zip(values, ops, checks)] - ) - - @pytest.mark.asyncio async def test_model_choices(): - """Test that choices work properly for various types of fields.""" - async with database: - # Test valid choices. + """Test that enum work properly for various types of fields.""" + async with base_ormar_config.database: + # Test valid enums values. await asyncio.gather( Country.objects.create(name="Canada", taxed=True, country_code=1), Country.objects.create(name="Algeria", taxed=True, country_code=213), @@ -529,54 +476,12 @@ async def test_model_choices(): with pytest.raises(ValueError): name, taxed, country_code = "Saudi Arabia", True, 1 - check_choices((name, taxed, country_code), ["out", "in", "in"]) - await Country.objects.create( - name=name, taxed=taxed, country_code=country_code - ) - - with pytest.raises(ValueError): - name, taxed, country_code = "Algeria", False, 1 - check_choices((name, taxed, country_code), ["in", "out", "in"]) await Country.objects.create( name=name, taxed=taxed, country_code=country_code ) with pytest.raises(ValueError): name, taxed, country_code = "Algeria", True, 967 - check_choices((name, taxed, country_code), ["in", "in", "out"]) - await Country.objects.create( - name=name, taxed=taxed, country_code=country_code - ) - - with pytest.raises(ValueError): - name, taxed, country_code = ( - "United States", - True, - 1, - ) # name is too long but is a valid choice - check_choices((name, taxed, country_code), ["in", "in", "in"]) - await Country.objects.create( - name=name, taxed=taxed, country_code=country_code - ) - - with pytest.raises(ValueError): - name, taxed, country_code = ( - "Algeria", - True, - -10, - ) # country code is too small but is a valid choice - check_choices((name, taxed, country_code), ["in", "in", "in"]) - await Country.objects.create( - name=name, taxed=taxed, country_code=country_code - ) - - with pytest.raises(ValueError): - name, taxed, country_code = ( - "Algeria", - True, - 1200, - ) # country code is too large but is a valid choice - check_choices((name, taxed, country_code), ["in", "in", "in"]) await Country.objects.create( name=name, taxed=taxed, country_code=country_code ) @@ -584,34 +489,24 @@ async def test_model_choices(): # test setting after init also triggers validation with pytest.raises(ValueError): name, taxed, country_code = "Algeria", True, 967 - check_choices((name, taxed, country_code), ["in", "in", "out"]) country = Country() country.country_code = country_code with pytest.raises(ValueError): name, taxed, country_code = "Saudi Arabia", True, 1 - check_choices((name, taxed, country_code), ["out", "in", "in"]) country = Country() country.name = name - with pytest.raises(ValueError): - name, taxed, country_code = "Algeria", False, 1 - check_choices((name, taxed, country_code), ["in", "out", "in"]) - country = Country() - country.taxed = taxed - # check also update from queryset with pytest.raises(ValueError): - name, taxed, country_code = "Algeria", False, 1 - check_choices((name, taxed, country_code), ["in", "out", "in"]) await Country(name="Belize").save() await Country.objects.filter(name="Belize").update(name="Vietnam") @pytest.mark.asyncio -async def test_nullable_field_model_choices(): - """Test that choices work properly for according to nullable setting""" - async with database: +async def test_nullable_field_model_enum(): + """Test that enum work properly for according to nullable setting""" + async with base_ormar_config.database: c1 = await NullableCountry(name=None).save() assert c1.name is None @@ -621,8 +516,8 @@ async def test_nullable_field_model_choices(): @pytest.mark.asyncio async def test_start_and_end_filters(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await User.objects.create(name="Markos Uj") await User.objects.create(name="Maqua Bigo") await User.objects.create(name="maqo quidid") @@ -651,8 +546,8 @@ async def test_start_and_end_filters(): @pytest.mark.asyncio async def test_get_and_first(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await User.objects.create(name="Tom") await User.objects.create(name="Jane") await User.objects.create(name="Lucy") @@ -681,4 +576,4 @@ async def test_get_and_first(): def test_constraints(): with pytest.raises(pydantic.ValidationError) as e: Product(name="T-Shirt", rating=50, in_stock=True) - assert "ensure this value is less than or equal to 5" in str(e.value) + assert "Input should be less than or equal to 5 " in str(e.value) diff --git a/tests/test_model_definition/test_models_are_pickable.py b/tests/test_model_definition/test_models_are_pickable.py index bb05e02..4ce7415 100644 --- a/tests/test_model_definition/test_models_are_pickable.py +++ b/tests/test_model_definition/test_models_are_pickable.py @@ -1,22 +1,17 @@ import pickle from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class User(ormar.Model): - class Meta: - tablename = "users" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="users") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -24,28 +19,19 @@ class User(ormar.Model): class Post(ormar.Model): - class Meta: - tablename = "posts" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="posts") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) created_by: Optional[User] = ormar.ForeignKey(User) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_dumping_and_loading_model_works(): - async with database: + async with base_ormar_config.database: user = await User(name="Test", properties={"aa": "bb"}).save() post = Post(name="Test post") await user.posts.add(post) diff --git a/tests/test_model_definition/test_overwriting_pydantic_field_type.py b/tests/test_model_definition/test_overwriting_pydantic_field_type.py index bb015a3..d2d4fb0 100644 --- a/tests/test_model_definition/test_overwriting_pydantic_field_type.py +++ b/tests/test_model_definition/test_overwriting_pydantic_field_type.py @@ -1,52 +1,44 @@ from typing import Dict, Optional -import databases +import ormar import pytest -import sqlalchemy from pydantic import Json, PositiveInt, ValidationError -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class OverwriteTest(ormar.Model): - class Meta: - tablename = "overwrites" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="overwrites") id: int = ormar.Integer(primary_key=True) my_int: int = ormar.Integer(overwrite_pydantic_type=PositiveInt) constraint_dict: Json = ormar.JSON( - overwrite_pydantic_type=Optional[Json[Dict[str, int]]] # type: ignore - ) + overwrite_pydantic_type=Optional[Json[Dict[str, int]]] + ) # type: ignore -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_constraints(): with pytest.raises(ValidationError) as e: OverwriteTest(my_int=-10) - assert "ensure this value is greater than 0" in str(e.value) + assert "Input should be greater than 0" in str(e.value) with pytest.raises(ValidationError) as e: OverwriteTest(my_int=10, constraint_dict={"aa": "ab"}) - assert "value is not a valid integer" in str(e.value) + assert ( + "Input should be a valid integer, unable to parse string as an integer" + in str(e.value) + ) @pytest.mark.asyncio async def test_saving(): - async with database: + async with base_ormar_config.database: await OverwriteTest(my_int=5, constraint_dict={"aa": 123}).save() test = await OverwriteTest.objects.get() diff --git a/tests/test_model_definition/test_overwriting_sql_nullable.py b/tests/test_model_definition/test_overwriting_sql_nullable.py index f98b45b..6992921 100644 --- a/tests/test_model_definition/test_overwriting_sql_nullable.py +++ b/tests/test_model_definition/test_overwriting_sql_nullable.py @@ -2,28 +2,19 @@ import sqlite3 from typing import Optional import asyncpg -import databases -import pymysql -import sqlalchemy -from sqlalchemy import create_engine, text - import ormar +import pymysql import pytest +from sqlalchemy import text -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -db = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = db +base_ormar_config = create_config() class PrimaryModel(ormar.Model): - class Meta(BaseMeta): - tablename = "primary_models" + ormar_config = base_ormar_config.copy(tablename="primary_models") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=255, index=True) @@ -33,17 +24,12 @@ class PrimaryModel(ormar.Model): ) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_create_models(): - async with db: + async with base_ormar_config.database: primary = await PrimaryModel( name="Foo", some_text="Bar", some_other_text="Baz" ).save() diff --git a/tests/test_model_definition/test_pk_field_is_always_not_null.py b/tests/test_model_definition/test_pk_field_is_always_not_null.py index 22b03a3..4737a4b 100644 --- a/tests/test_model_definition/test_pk_field_is_always_not_null.py +++ b/tests/test_model_definition/test_pk_field_is_always_not_null.py @@ -1,39 +1,32 @@ -import databases -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class AutoincrementModel(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) class NonAutoincrementModel(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True, autoincrement=False) class ExplicitNullableModel(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True, nullable=True) +create_test_database = init_tests(base_ormar_config) + + def test_pk_field_is_not_null(): for model in [AutoincrementModel, NonAutoincrementModel, ExplicitNullableModel]: - assert not model.Meta.table.c.get("id").nullable + assert not model.ormar_config.table.c.get("id").nullable diff --git a/tests/test_model_definition/test_properties.py b/tests/test_model_definition/test_properties.py index 6456c6a..863f093 100644 --- a/tests/test_model_definition/test_properties.py +++ b/tests/test_model_definition/test_properties.py @@ -1,79 +1,68 @@ # type: ignore -import databases -import pytest -import sqlalchemy - import ormar -from ormar import ModelDefinitionError, property_field -from tests.settings import DATABASE_URL +import pytest +from pydantic import PydanticUserError, computed_field -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Song(ormar.Model): - class Meta: - tablename = "songs" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="songs") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) sort_order: int = ormar.Integer() - @property_field - def sorted_name(self): + @computed_field + def sorted_name(self) -> str: return f"{self.sort_order}: {self.name}" - @property_field - def sample(self): + @computed_field + def sample(self) -> str: return "sample" - @property_field - def sample2(self): + @computed_field + def sample2(self) -> str: return "sample2" -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_sort_order_on_main_model(): - async with database: + async with base_ormar_config.database: await Song.objects.create(name="Song 3", sort_order=3) await Song.objects.create(name="Song 1", sort_order=1) await Song.objects.create(name="Song 2", sort_order=2) songs = await Song.objects.all() - song_dict = [song.dict() for song in songs] + song_dict = [song.model_dump() for song in songs] assert all("sorted_name" in x for x in song_dict) assert all( x["sorted_name"] == f"{x['sort_order']}: {x['name']}" for x in song_dict ) - song_json = [song.json() for song in songs] + song_json = [song.model_dump_json() for song in songs] assert all("sorted_name" in x for x in song_json) - check_include = songs[0].dict(include={"sample"}) + check_include = songs[0].model_dump(include={"sample"}) assert "sample" in check_include assert "sample2" not in check_include assert "sorted_name" not in check_include - check_include = songs[0].dict(exclude={"sample"}) + check_include = songs[0].model_dump(exclude={"sample"}) assert "sample" not in check_include assert "sample2" in check_include assert "sorted_name" in check_include def test_wrong_definition(): - with pytest.raises(ModelDefinitionError): + with pytest.raises(PydanticUserError): class WrongModel(ormar.Model): # pragma: no cover - @property_field + @computed_field def test(self, aa=10, bb=30): pass diff --git a/tests/test_model_definition/test_pydantic_fields.py b/tests/test_model_definition/test_pydantic_fields.py index 12d5753..8fec91c 100644 --- a/tests/test_model_definition/test_pydantic_fields.py +++ b/tests/test_model_definition/test_pydantic_fields.py @@ -1,31 +1,24 @@ import random from typing import Optional -import databases -import pytest -import sqlalchemy -from pydantic import BaseModel, Field, HttpUrl, PaymentCardNumber - import ormar -from tests.settings import DATABASE_URL +import pytest +from pydantic import BaseModel, Field, HttpUrl +from pydantic_extra_types.payment import PaymentCardNumber -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class ModelTest(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200) url: HttpUrl = "https://www.example.com" # type: ignore - number: Optional[PaymentCardNumber] + number: Optional[PaymentCardNumber] = None CARD_NUMBERS = [ @@ -42,8 +35,7 @@ def get_number(): class ModelTest2(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200) @@ -57,8 +49,7 @@ class PydanticTest(BaseModel): class ModelTest3(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() def __init__(self, **kwargs): kwargs["number"] = get_number() @@ -72,18 +63,12 @@ class ModelTest3(ormar.Model): pydantic_test: PydanticTest -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_working_with_pydantic_fields(): - async with database: + async with base_ormar_config.database: test = ModelTest(name="Test") assert test.name == "Test" assert test.url == "https://www.example.com" @@ -103,7 +88,7 @@ async def test_working_with_pydantic_fields(): @pytest.mark.asyncio async def test_default_factory_for_pydantic_fields(): - async with database: + async with base_ormar_config.database: test = ModelTest2(name="Test2", number="4000000000000002") assert test.name == "Test2" assert test.url == "https://www.example2.com" @@ -123,7 +108,7 @@ async def test_default_factory_for_pydantic_fields(): @pytest.mark.asyncio async def test_init_setting_for_pydantic_fields(): - async with database: + async with base_ormar_config.database: test = ModelTest3(name="Test3") assert test.name == "Test3" assert test.url == "https://www.example3.com" diff --git a/tests/test_model_definition/test_pydantic_only_fields.py b/tests/test_model_definition/test_pydantic_only_fields.py index ee58177..49800b0 100644 --- a/tests/test_model_definition/test_pydantic_only_fields.py +++ b/tests/test_model_definition/test_pydantic_only_fields.py @@ -1,32 +1,28 @@ import datetime -import databases -import pytest -import sqlalchemy - import ormar -from ormar import property_field -from tests.settings import DATABASE_URL +import pydantic +import pytest +from pydantic import computed_field -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="albums") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) - timestamp: datetime.datetime = ormar.DateTime(pydantic_only=True) + timestamp: datetime.datetime = pydantic.Field(default=None) - @property_field + @computed_field def name10(self) -> str: return self.name + "_10" - @property_field + @computed_field def name20(self) -> str: return self.name + "_20" @@ -34,24 +30,18 @@ class Album(ormar.Model): def name30(self) -> str: return self.name + "_30" - @property_field + @computed_field def name40(self) -> str: return self.name + "_40" -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_pydantic_only_fields(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): album = await Album.objects.create(name="Hitchcock") assert album.pk is not None assert album.saved @@ -63,14 +53,14 @@ async def test_pydantic_only_fields(): album = await Album.objects.fields({"name", "timestamp"}).get() assert album.timestamp is None - test_dict = album.dict() + test_dict = album.model_dump() assert "timestamp" in test_dict assert test_dict["timestamp"] is None assert album.name30 == "Hitchcock_30" album.timestamp = datetime.datetime.now() - test_dict = album.dict() + test_dict = album.model_dump() assert "timestamp" in test_dict assert test_dict["timestamp"] is not None assert test_dict.get("name10") == "Hitchcock_10" diff --git a/tests/test_model_definition/test_pydantic_private_attributes.py b/tests/test_model_definition/test_pydantic_private_attributes.py index 2765f60..64d7f83 100644 --- a/tests/test_model_definition/test_pydantic_private_attributes.py +++ b/tests/test_model_definition/test_pydantic_private_attributes.py @@ -1,24 +1,16 @@ from typing import List -import databases -import sqlalchemy +import ormar from pydantic import PrivateAttr -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Subscription(ormar.Model): - class Meta(BaseMeta): - tablename = "subscriptions" + ormar_config = base_ormar_config.copy(tablename="subscriptions") id: int = ormar.Integer(primary_key=True) stripe_subscription_id: str = ormar.String(nullable=False, max_length=256) @@ -29,6 +21,9 @@ class Subscription(ormar.Model): self._add_payments.append(payment) +create_test_database = init_tests(base_ormar_config) + + def test_private_attribute(): sub = Subscription(stripe_subscription_id="2312312sad231") sub.add_payment("test") diff --git a/tests/test_model_definition/test_save_status.py b/tests/test_model_definition/test_save_status.py index b104072..a2bab8f 100644 --- a/tests/test_model_definition/test_save_status.py +++ b/tests/test_model_definition/test_save_status.py @@ -1,22 +1,17 @@ from typing import List -import databases -import pytest -import sqlalchemy - import ormar +import pytest from ormar.exceptions import ModelPersistenceError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class NickNames(ormar.Model): - class Meta: - tablename = "nicks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="nicks") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -24,17 +19,11 @@ class NickNames(ormar.Model): class NicksHq(ormar.Model): - class Meta: - tablename = "nicks_x_hq" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="nicks_x_hq") class HQ(ormar.Model): - class Meta: - tablename = "hqs" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="hqs") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -42,10 +31,7 @@ class HQ(ormar.Model): class Company(ormar.Model): - class Meta: - tablename = "companies" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="companies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="company_name") @@ -53,19 +39,13 @@ class Company(ormar.Model): hq: HQ = ormar.ForeignKey(HQ) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_instantiation_false_save_true(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): comp = Company(name="Banzai", founded=1988) assert not comp.saved await comp.save() @@ -74,8 +54,8 @@ async def test_instantiation_false_save_true(): @pytest.mark.asyncio async def test_saved_edited_not_saved(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): comp = await Company.objects.create(name="Banzai", founded=1988) assert comp.saved comp.name = "Banzai2" @@ -96,8 +76,8 @@ async def test_saved_edited_not_saved(): @pytest.mark.asyncio async def test_adding_related_gets_dirty(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): hq = await HQ.objects.create(name="Main") comp = await Company.objects.create(name="Banzai", founded=1988) assert comp.saved @@ -123,8 +103,8 @@ async def test_adding_related_gets_dirty(): @pytest.mark.asyncio async def test_adding_many_to_many_does_not_gets_dirty(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): nick1 = await NickNames.objects.create(name="Bazinga", is_lame=False) nick2 = await NickNames.objects.create(name="Bazinga2", is_lame=True) @@ -152,8 +132,8 @@ async def test_adding_many_to_many_does_not_gets_dirty(): @pytest.mark.asyncio async def test_delete(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): comp = await Company.objects.create(name="Banzai", founded=1988) assert comp.saved await comp.delete() @@ -165,8 +145,8 @@ async def test_delete(): @pytest.mark.asyncio async def test_load(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): comp = await Company.objects.create(name="Banzai", founded=1988) assert comp.saved comp.name = "AA" @@ -179,8 +159,8 @@ async def test_load(): @pytest.mark.asyncio async def test_queryset_methods(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await Company.objects.create(name="Banzai", founded=1988) await Company.objects.create(name="Yuhu", founded=1989) await Company.objects.create(name="Konono", founded=1990) @@ -208,7 +188,7 @@ async def test_queryset_methods(): assert comp3.pk == comp.pk assert created is False - update_dict = comp.dict() + update_dict = comp.model_dump() update_dict["founded"] = 2010 comp = await Company.objects.update_or_create(**update_dict) assert comp.saved @@ -222,8 +202,8 @@ async def test_queryset_methods(): @pytest.mark.asyncio async def test_bulk_methods(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): c1 = Company(name="Banzai", founded=1988) c2 = Company(name="Yuhu", founded=1989) diff --git a/tests/test_model_definition/test_saving_nullable_fields.py b/tests/test_model_definition/test_saving_nullable_fields.py index 0803b20..4567814 100644 --- a/tests/test_model_definition/test_saving_nullable_fields.py +++ b/tests/test_model_definition/test_saving_nullable_fields.py @@ -1,23 +1,16 @@ from typing import Optional -import databases -import sqlalchemy -from sqlalchemy import create_engine - import ormar import pytest -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -db = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class PrimaryModel(ormar.Model): - class Meta: - metadata = metadata - database = db - tablename = "primary_models" + ormar_config = base_ormar_config.copy(tablename="primary_models") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=255, index=True) @@ -27,10 +20,7 @@ class PrimaryModel(ormar.Model): class SecondaryModel(ormar.Model): - class Meta: - metadata = metadata - database = db - tablename = "secondary_models" + ormar_config = base_ormar_config.copy(tablename="secondary_models") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -39,18 +29,13 @@ class SecondaryModel(ormar.Model): ) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_create_models(): - async with db: - async with db.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): primary = await PrimaryModel( name="Foo", some_text="Bar", some_other_text="Baz" ).save() diff --git a/tests/test_model_definition/test_server_default.py b/tests/test_model_definition/test_server_default.py index a953068..a181d79 100644 --- a/tests/test_model_definition/test_server_default.py +++ b/tests/test_model_definition/test_server_default.py @@ -1,24 +1,18 @@ -import asyncio import time from datetime import datetime -import databases +import ormar import pytest -import sqlalchemy from sqlalchemy import func, text -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class Product(ormar.Model): - class Meta: - tablename = "product" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="product") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -27,25 +21,21 @@ class Product(ormar.Model): created: datetime = ormar.DateTime(server_default=func.now()) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_table_defined_properly(): - assert Product.Meta.model_fields["created"].nullable - assert not Product.__fields__["created"].required - assert Product.Meta.table.columns["created"].server_default.arg.name == "now" + assert Product.ormar_config.model_fields["created"].nullable + assert not Product.model_fields["created"].is_required() + assert ( + Product.ormar_config.table.columns["created"].server_default.arg.name == "now" + ) @pytest.mark.asyncio async def test_model_creation(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): p1 = Product(name="Test") assert p1.created is None await p1.save() diff --git a/tests/test_model_definition/test_setting_comments_in_db.py b/tests/test_model_definition/test_setting_comments_in_db.py index 588cdf1..30bc336 100644 --- a/tests/test_model_definition/test_setting_comments_in_db.py +++ b/tests/test_model_definition/test_setting_comments_in_db.py @@ -1,36 +1,25 @@ -import databases -import pytest -import sqlalchemy - import ormar +import pytest from ormar.models import Model -from tests.settings import DATABASE_URL -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) +base_ormar_config = create_config() class Comment(Model): - class Meta(ormar.ModelMeta): - tablename = "comments" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="comments") test: int = ormar.Integer(primary_key=True, comment="primary key of comments") test_string: str = ormar.String(max_length=250, comment="test that it works") -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_comments_are_set_in_db(): - columns = Comment.Meta.table.c + columns = Comment.ormar_config.table.c for c in columns: - assert c.comment == Comment.Meta.model_fields[c.name].comment + assert c.comment == Comment.ormar_config.model_fields[c.name].comment diff --git a/tests/test_model_methods/test_excludes_in_load_all.py b/tests/test_model_methods/test_excludes_in_load_all.py index 66ee834..6b9b087 100644 --- a/tests/test_model_methods/test_excludes_in_load_all.py +++ b/tests/test_model_methods/test_excludes_in_load_all.py @@ -1,26 +1,16 @@ -import asyncio import uuid +import ormar import pytest -import ormar -import sqlalchemy -import databases +from tests.lifespan import init_tests +from tests.settings import create_config -from tests.settings import DATABASE_URL - -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() - - -class BaseMeta: - metadata = metadata - database = database +base_ormar_config = create_config(force_rollback=True) class JimmyUser(ormar.Model): - class Meta(BaseMeta): - tablename = "jimmy_users" + ormar_config = base_ormar_config.copy(tablename="jimmy_users") id: uuid.UUID = ormar.UUID( primary_key=True, default=uuid.uuid4(), uuid_format="string" @@ -28,42 +18,31 @@ class JimmyUser(ormar.Model): class JimmyProfile(ormar.Model): - class Meta(BaseMeta): - tablename = "jimmy_profiles" + ormar_config = base_ormar_config.copy(tablename="jimmy_profiles") id: uuid.UUID = ormar.UUID( primary_key=True, default=uuid.uuid4(), uuid_format="string" ) name = ormar.String(max_length=42, default="JimmyProfile") - user: JimmyUser = ormar.ForeignKey(to=JimmyUser) class JimmyAccount(ormar.Model): - class Meta(BaseMeta): - tablename = "jimmy_accounts" + ormar_config = base_ormar_config.copy(tablename="jimmy_accounts") id: uuid.UUID = ormar.UUID( primary_key=True, default=uuid.uuid4(), uuid_format="string" ) - name = ormar.String(max_length=42, default="JimmyAccount") - user: JimmyUser = ormar.ForeignKey(to=JimmyUser) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_excluding_one_relation(): - async with database: + async with base_ormar_config.database: user = JimmyUser() await user.save() @@ -77,7 +56,7 @@ async def test_excluding_one_relation(): @pytest.mark.asyncio async def test_excluding_other_relation(): - async with database: + async with base_ormar_config.database: user = JimmyUser() await user.save() diff --git a/tests/test_model_methods/test_load_all.py b/tests/test_model_methods/test_load_all.py index 0095654..99d3ed2 100644 --- a/tests/test_model_methods/test_load_all.py +++ b/tests/test_model_methods/test_load_all.py @@ -1,24 +1,16 @@ from typing import List -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() class Language(ormar.Model): - class Meta(BaseMeta): - tablename = "languages" + ormar_config = base_ormar_config.copy(tablename="languages") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -26,8 +18,7 @@ class Language(ormar.Model): class CringeLevel(ormar.Model): - class Meta(BaseMeta): - tablename = "levels" + ormar_config = base_ormar_config.copy(tablename="levels") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -35,8 +26,7 @@ class CringeLevel(ormar.Model): class NickName(ormar.Model): - class Meta(BaseMeta): - tablename = "nicks" + ormar_config = base_ormar_config.copy(tablename="nicks") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -45,8 +35,7 @@ class NickName(ormar.Model): class HQ(ormar.Model): - class Meta(BaseMeta): - tablename = "hqs" + ormar_config = base_ormar_config.copy(tablename="hqs") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -54,8 +43,7 @@ class HQ(ormar.Model): class Company(ormar.Model): - class Meta(BaseMeta): - tablename = "companies" + ormar_config = base_ormar_config.copy(tablename="companies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="company_name") @@ -63,19 +51,13 @@ class Company(ormar.Model): hq: HQ = ormar.ForeignKey(HQ, related_name="companies") -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_load_all_fk_rel(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): hq = await HQ.objects.create(name="Main") company = await Company.objects.create(name="Banzai", founded=1988, hq=hq) @@ -94,8 +76,8 @@ async def test_load_all_fk_rel(): @pytest.mark.asyncio async def test_load_all_many_to_many(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): nick1 = await NickName.objects.create(name="BazingaO", is_lame=False) nick2 = await NickName.objects.create(name="Bazinga20", is_lame=True) hq = await HQ.objects.create(name="Main") @@ -120,8 +102,8 @@ async def test_load_all_many_to_many(): @pytest.mark.asyncio async def test_load_all_with_order(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): nick1 = await NickName.objects.create(name="Barry", is_lame=False) nick2 = await NickName.objects.create(name="Joe", is_lame=True) hq = await HQ.objects.create(name="Main") @@ -154,8 +136,8 @@ async def test_load_all_with_order(): @pytest.mark.asyncio async def test_loading_reversed_relation(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): hq = await HQ.objects.create(name="Main") await Company.objects.create(name="Banzai", founded=1988, hq=hq) @@ -170,8 +152,8 @@ async def test_loading_reversed_relation(): @pytest.mark.asyncio async def test_loading_nested(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): language = await Language.objects.create(name="English") level = await CringeLevel.objects.create(name="High", language=language) level2 = await CringeLevel.objects.create(name="Low", language=language) diff --git a/tests/test_model_methods/test_populate_default_values.py b/tests/test_model_methods/test_populate_default_values.py index b3cd2b6..8675f67 100644 --- a/tests/test_model_methods/test_populate_default_values.py +++ b/tests/test_model_methods/test_populate_default_values.py @@ -1,31 +1,26 @@ -import databases -import pytest -import sqlalchemy +import ormar from sqlalchemy import text -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() class Task(ormar.Model): - class Meta(BaseMeta): - tablename = "tasks" + ormar_config = base_ormar_config.copy(tablename="tasks") id: int = ormar.Integer(primary_key=True) name: str = ormar.String( - max_length=255, minimum=0, server_default=text("Default Name"), nullable=False + max_length=255, minimum=0, server_default=text("'Default Name'"), nullable=False ) points: int = ormar.Integer( default=0, minimum=0, server_default=text("0"), nullable=False ) + score: int = ormar.Integer(default=5) + + +create_test_database = init_tests(base_ormar_config) def test_populate_default_values(): @@ -39,3 +34,4 @@ def test_populate_default_values(): assert result["id"] is None assert result["name"] == "" assert result["points"] == 0 + assert result["score"] == 5 diff --git a/tests/test_model_methods/test_save_related.py b/tests/test_model_methods/test_save_related.py index 207c53d..f12d3d1 100644 --- a/tests/test_model_methods/test_save_related.py +++ b/tests/test_model_methods/test_save_related.py @@ -1,31 +1,23 @@ from typing import List -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class CringeLevel(ormar.Model): - class Meta: - tablename = "levels" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="levels") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class NickName(ormar.Model): - class Meta: - tablename = "nicks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="nicks") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -34,17 +26,11 @@ class NickName(ormar.Model): class NicksHq(ormar.Model): - class Meta: - tablename = "nicks_x_hq" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="nicks_x_hq") class HQ(ormar.Model): - class Meta: - tablename = "hqs" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="hqs") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -52,10 +38,7 @@ class HQ(ormar.Model): class Company(ormar.Model): - class Meta: - tablename = "companies" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="companies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="company_name") @@ -63,19 +46,13 @@ class Company(ormar.Model): hq: HQ = ormar.ForeignKey(HQ, related_name="companies") -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_saving_related_fk_rel(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): hq = await HQ.objects.create(name="Main") comp = await Company.objects.create(name="Banzai", founded=1988, hq=hq) assert comp.saved @@ -102,8 +79,8 @@ async def test_saving_related_fk_rel(): @pytest.mark.asyncio async def test_saving_many_to_many(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): nick1 = await NickName.objects.create(name="BazingaO", is_lame=False) nick2 = await NickName.objects.create(name="Bazinga20", is_lame=True) @@ -144,8 +121,8 @@ async def test_saving_many_to_many(): @pytest.mark.asyncio async def test_saving_reversed_relation(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): hq = await HQ.objects.create(name="Main") await Company.objects.create(name="Banzai", founded=1988, hq=hq) @@ -185,8 +162,8 @@ async def test_saving_reversed_relation(): @pytest.mark.asyncio async def test_saving_nested(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): level = await CringeLevel.objects.create(name="High") level2 = await CringeLevel.objects.create(name="Low") nick1 = await NickName.objects.create( diff --git a/tests/test_model_methods/test_save_related_from_dict.py b/tests/test_model_methods/test_save_related_from_dict.py index a545092..c6a4e7e 100644 --- a/tests/test_model_methods/test_save_related_from_dict.py +++ b/tests/test_model_methods/test_save_related_from_dict.py @@ -1,31 +1,23 @@ from typing import List -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class CringeLevel(ormar.Model): - class Meta: - tablename = "levels" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="levels") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class NickName(ormar.Model): - class Meta: - tablename = "nicks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="nicks") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -34,20 +26,14 @@ class NickName(ormar.Model): class NicksHq(ormar.Model): - class Meta: - tablename = "nicks_x_hq" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="nicks_x_hq") id: int = ormar.Integer(primary_key=True) new_field: str = ormar.String(max_length=200, nullable=True) class HQ(ormar.Model): - class Meta: - tablename = "hqs" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="hqs") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -55,10 +41,7 @@ class HQ(ormar.Model): class Company(ormar.Model): - class Meta: - tablename = "companies" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="companies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="company_name") @@ -66,19 +49,13 @@ class Company(ormar.Model): hq: HQ = ormar.ForeignKey(HQ, related_name="companies") -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_saving_related_reverse_fk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): payload = {"companies": [{"name": "Banzai"}], "name": "Main"} hq = HQ(**payload) count = await hq.save_related(follow=True, save_all=True) @@ -94,8 +71,8 @@ async def test_saving_related_reverse_fk(): @pytest.mark.asyncio async def test_saving_related_reverse_fk_multiple(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): payload = { "companies": [{"name": "Banzai"}, {"name": "Yamate"}], "name": "Main", @@ -116,8 +93,8 @@ async def test_saving_related_reverse_fk_multiple(): @pytest.mark.asyncio async def test_saving_related_fk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): payload = {"hq": {"name": "Main"}, "name": "Banzai"} comp = Company(**payload) count = await comp.save_related(follow=True, save_all=True) @@ -132,8 +109,8 @@ async def test_saving_related_fk(): @pytest.mark.asyncio async def test_saving_many_to_many_wo_through(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): payload = { "name": "Main", "nicks": [ @@ -155,9 +132,9 @@ async def test_saving_many_to_many_wo_through(): @pytest.mark.asyncio async def test_saving_many_to_many_with_through(): - async with database: - async with database.transaction(force_rollback=True): - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): + async with base_ormar_config.database.transaction(force_rollback=True): payload = { "name": "Main", "nicks": [ @@ -189,8 +166,8 @@ async def test_saving_many_to_many_with_through(): @pytest.mark.asyncio async def test_saving_nested_with_m2m_and_rev_fk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): payload = { "name": "Main", "nicks": [ @@ -214,8 +191,8 @@ async def test_saving_nested_with_m2m_and_rev_fk(): @pytest.mark.asyncio async def test_saving_nested_with_m2m_and_rev_fk_and_through(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): payload = { "hq": { "name": "Yoko", diff --git a/tests/test_model_methods/test_save_related_uuid.py b/tests/test_model_methods/test_save_related_uuid.py index 602d1bd..5b425c6 100644 --- a/tests/test_model_methods/test_save_related_uuid.py +++ b/tests/test_model_methods/test_save_related_uuid.py @@ -1,30 +1,24 @@ import uuid from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Department(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4) department_name: str = ormar.String(max_length=100) class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4) course_name: str = ormar.String(max_length=100) @@ -33,27 +27,19 @@ class Course(ormar.Model): class Student(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4) name: str = ormar.String(max_length=100) courses = ormar.ManyToMany(Course) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_uuid_pk_in_save_related(): - async with database: + async with base_ormar_config.database: to_save = { "department_name": "Ormar", "courses": [ @@ -80,4 +66,4 @@ async def test_uuid_pk_in_save_related(): "id": ..., "courses": {"id": ..., "students": {"id", "studentcourse"}}, } - assert department_check.dict(exclude=to_exclude) == to_save + assert department_check.model_dump(exclude=to_exclude) == to_save diff --git a/tests/test_model_methods/test_update.py b/tests/test_model_methods/test_update.py index 391baf7..f2cbd33 100644 --- a/tests/test_model_methods/test_update.py +++ b/tests/test_model_methods/test_update.py @@ -1,21 +1,16 @@ from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Director(ormar.Model): - class Meta: - tablename = "directors" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="directors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="first_name") @@ -23,10 +18,7 @@ class Director(ormar.Model): class Movie(ormar.Model): - class Meta: - tablename = "movies" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="movies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="title") @@ -35,18 +27,12 @@ class Movie(ormar.Model): director: Optional[Director] = ormar.ForeignKey(Director) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_updating_selected_columns(): - async with database: + async with base_ormar_config.database: director1 = await Director(name="Peter", last_name="Jackson").save() director2 = await Director(name="James", last_name="Cameron").save() @@ -82,7 +68,7 @@ async def test_updating_selected_columns(): @pytest.mark.asyncio async def test_not_passing_columns_or_empty_list_saves_all(): - async with database: + async with base_ormar_config.database: director = await Director(name="James", last_name="Cameron").save() terminator = await Movie( name="Terminator", year=1984, director=director, profit=0.078 diff --git a/tests/test_model_methods/test_upsert.py b/tests/test_model_methods/test_upsert.py index 178ae72..249054c 100644 --- a/tests/test_model_methods/test_upsert.py +++ b/tests/test_model_methods/test_upsert.py @@ -1,21 +1,16 @@ from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Director(ormar.Model): - class Meta: - tablename = "directors" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="directors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="first_name") @@ -23,10 +18,7 @@ class Director(ormar.Model): class Movie(ormar.Model): - class Meta: - tablename = "movies" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="movies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="title") @@ -35,18 +27,12 @@ class Movie(ormar.Model): director: Optional[Director] = ormar.ForeignKey(Director) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_updating_selected_columns(): - async with database: + async with base_ormar_config.database: director1 = await Director(name="Peter", last_name="Jackson").save() await Movie( diff --git a/tests/test_ordering/test_default_model_order.py b/tests/test_ordering/test_default_model_order.py index 1b84ce0..4a7f4aa 100644 --- a/tests/test_ordering/test_default_model_order.py +++ b/tests/test_ordering/test_default_model_order.py @@ -1,35 +1,26 @@ from typing import Optional -import databases +import ormar import pytest import pytest_asyncio -import sqlalchemy -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" - orders_by = ["-name"] + ormar_config = base_ormar_config.copy(tablename="authors", order_by=["-name"]) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" - orders_by = ["year", "-ranking"] + ormar_config = base_ormar_config.copy( + tablename="books", order_by=["year", "-ranking"] + ) id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey(Author) @@ -38,26 +29,20 @@ class Book(ormar.Model): ranking: int = ormar.Integer(nullable=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest_asyncio.fixture(autouse=True, scope="function") async def cleanup(): yield - async with database: + async with base_ormar_config.database: await Book.objects.delete(each=True) await Author.objects.delete(each=True) @pytest.mark.asyncio async def test_default_orders_is_applied(): - async with database: + async with base_ormar_config.database: tolkien = await Author(name="J.R.R. Tolkien").save() sapkowski = await Author(name="Andrzej Sapkowski").save() king = await Author(name="Stephen King").save() @@ -78,7 +63,7 @@ async def test_default_orders_is_applied(): @pytest.mark.asyncio async def test_default_orders_is_applied_on_related(): - async with database: + async with base_ormar_config.database: tolkien = await Author(name="J.R.R. Tolkien").save() silmarillion = await Book( author=tolkien, title="The Silmarillion", year=1977 @@ -101,7 +86,7 @@ async def test_default_orders_is_applied_on_related(): @pytest.mark.asyncio async def test_default_orders_is_applied_on_related_two_fields(): - async with database: + async with base_ormar_config.database: sanders = await Author(name="Brandon Sanderson").save() twok = await Book( author=sanders, title="The Way of Kings", year=2010, ranking=10 diff --git a/tests/test_ordering/test_default_relation_order.py b/tests/test_ordering/test_default_relation_order.py index 077646f..6e395a0 100644 --- a/tests/test_ordering/test_default_relation_order.py +++ b/tests/test_ordering/test_default_relation_order.py @@ -1,34 +1,25 @@ from typing import List, Optional from uuid import UUID, uuid4 -import databases +import ormar import pytest import pytest_asyncio -import sqlalchemy -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" + ormar_config = base_ormar_config.copy(tablename="books") id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey( @@ -40,8 +31,7 @@ class Book(ormar.Model): class Animal(ormar.Model): - class Meta(BaseMeta): - tablename = "animals" + ormar_config = base_ormar_config.copy(tablename="animals") id: UUID = ormar.UUID(primary_key=True, default=uuid4) name: str = ormar.String(max_length=200) @@ -49,8 +39,7 @@ class Animal(ormar.Model): class Human(ormar.Model): - class Meta(BaseMeta): - tablename = "humans" + ormar_config = base_ormar_config.copy(tablename="humans") id: UUID = ormar.UUID(primary_key=True, default=uuid4) name: str = ormar.Text(default="") @@ -62,26 +51,20 @@ class Human(ormar.Model): ) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest_asyncio.fixture(autouse=True, scope="function") async def cleanup(): yield - async with database: + async with base_ormar_config.database: await Book.objects.delete(each=True) await Author.objects.delete(each=True) @pytest.mark.asyncio async def test_default_orders_is_applied_from_reverse_relation(): - async with database: + async with base_ormar_config.database: tolkien = await Author(name="J.R.R. Tolkien").save() hobbit = await Book(author=tolkien, title="The Hobbit", year=1933).save() silmarillion = await Book( @@ -99,7 +82,7 @@ async def test_default_orders_is_applied_from_reverse_relation(): @pytest.mark.asyncio async def test_default_orders_is_applied_from_relation(): - async with database: + async with base_ormar_config.database: bret = await Author(name="Peter V. Bret").save() tds = await Book( author=bret, title="The Desert Spear", year=2010, ranking=9 @@ -116,7 +99,7 @@ async def test_default_orders_is_applied_from_relation(): @pytest.mark.asyncio async def test_default_orders_is_applied_from_relation_on_m2m(): - async with database: + async with base_ormar_config.database: alice = await Human(name="Alice").save() spot = await Animal(name="Spot", specie="Cat").save() @@ -135,8 +118,7 @@ async def test_default_orders_is_applied_from_relation_on_m2m(): @pytest.mark.asyncio async def test_default_orders_is_applied_from_reverse_relation_on_m2m(): - async with database: - + async with base_ormar_config.database: max = await Animal(name="Max", specie="Dog").save() joe = await Human(name="Joe").save() zack = await Human(name="Zack").save() diff --git a/tests/test_ordering/test_default_through_relation_order.py b/tests/test_ordering/test_default_through_relation_order.py index f695426..ed0889b 100644 --- a/tests/test_ordering/test_default_through_relation_order.py +++ b/tests/test_ordering/test_default_through_relation_order.py @@ -1,27 +1,25 @@ from typing import Any, Dict, List, Tuple, Type, cast from uuid import UUID, uuid4 -import databases -import pytest -import sqlalchemy - import ormar -from ormar import ModelDefinitionError, Model, QuerySet, pre_relation_remove, pre_update -from ormar import pre_save -from tests.settings import DATABASE_URL +import pytest +from ormar import ( + Model, + ModelDefinitionError, + QuerySet, + pre_relation_remove, + pre_save, + pre_update, +) -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Animal(ormar.Model): - class Meta(BaseMeta): - tablename = "animals" + ormar_config = base_ormar_config.copy(tablename="animals") id: UUID = ormar.UUID(primary_key=True, default=uuid4) name: str = ormar.Text(default="") @@ -29,8 +27,7 @@ class Animal(ormar.Model): class Link(ormar.Model): - class Meta(BaseMeta): - tablename = "link_table" + ormar_config = base_ormar_config.copy(tablename="link_table") id: UUID = ormar.UUID(primary_key=True, default=uuid4) animal_order: int = ormar.Integer(nullable=True) @@ -38,8 +35,7 @@ class Link(ormar.Model): class Human(ormar.Model): - class Meta(BaseMeta): - tablename = "humans" + ormar_config = base_ormar_config.copy(tablename="humans") id: UUID = ormar.UUID(primary_key=True, default=uuid4) name: str = ormar.Text(default="") @@ -53,8 +49,7 @@ class Human(ormar.Model): class Human2(ormar.Model): - class Meta(BaseMeta): - tablename = "humans2" + ormar_config = base_ormar_config.copy(tablename="humans2") id: UUID = ormar.UUID(primary_key=True, default=uuid4) name: str = ormar.Text(default="") @@ -63,18 +58,12 @@ class Human2(ormar.Model): ) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_ordering_by_through_fail(): - async with database: + async with base_ormar_config.database: alice = await Human2(name="Alice").save() spot = await Animal(name="Spot").save() await alice.favoriteAnimals.add(spot) @@ -99,8 +88,8 @@ def _get_through_model_relations( sender: Type[Model], instance: Model ) -> Tuple[Type[Model], Type[Model]]: relations = list(instance.extract_related_names()) - rel_one = sender.Meta.model_fields[relations[0]].to - rel_two = sender.Meta.model_fields[relations[1]].to + rel_one = sender.ormar_config.model_fields[relations[0]].to + rel_two = sender.ormar_config.model_fields[relations[1]].to return rel_one, rel_two @@ -223,7 +212,7 @@ async def reorder_links_on_remove( Note that if classes have many relations you need to check if current one is ordered """ - through_class = sender.Meta.model_fields[relation_name].through + through_class = sender.ormar_config.model_fields[relation_name].through through_instance = getattr(instance, through_class.get_name()) if not through_instance: parent_pk = instance.pk @@ -249,7 +238,7 @@ async def reorder_links_on_remove( @pytest.mark.asyncio async def test_ordering_by_through_on_m2m_field(): - async with database: + async with base_ormar_config.database: def verify_order(instance, expected): field_name = ( diff --git a/tests/test_ordering/test_proper_order_of_sorting_apply.py b/tests/test_ordering/test_proper_order_of_sorting_apply.py index 1a2186b..f6f8051 100644 --- a/tests/test_ordering/test_proper_order_of_sorting_apply.py +++ b/tests/test_ordering/test_proper_order_of_sorting_apply.py @@ -1,34 +1,24 @@ from typing import Optional -import databases +import ormar import pytest import pytest_asyncio -import sqlalchemy -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" - orders_by = ["-ranking"] + ormar_config = base_ormar_config.copy(tablename="books", order_by=["-ranking"]) id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey( @@ -39,26 +29,20 @@ class Book(ormar.Model): ranking: int = ormar.Integer(nullable=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest_asyncio.fixture(autouse=True, scope="function") async def cleanup(): yield - async with database: + async with base_ormar_config.database: await Book.objects.delete(each=True) await Author.objects.delete(each=True) @pytest.mark.asyncio async def test_default_orders_is_applied_from_reverse_relation(): - async with database: + async with base_ormar_config.database: tolkien = await Author(name="J.R.R. Tolkien").save() hobbit = await Book(author=tolkien, title="The Hobbit", year=1933).save() silmarillion = await Book( diff --git a/tests/test_queries/test_adding_related.py b/tests/test_queries/test_adding_related.py index 271a13c..84a3f22 100644 --- a/tests/test_queries/test_adding_related.py +++ b/tests/test_queries/test_adding_related.py @@ -1,31 +1,23 @@ from typing import Optional -import databases -import pytest -import sqlalchemy -import asyncio - import ormar +import pytest -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class Department(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -33,18 +25,12 @@ class Course(ormar.Model): department: Optional[Department] = ormar.ForeignKey(Department) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_adding_relation_to_reverse_saves_the_child(): - async with database: + async with base_ormar_config.database: department = await Department(name="Science").save() course = Course(name="Math", completed=False) diff --git a/tests/test_queries/test_aggr_functions.py b/tests/test_queries/test_aggr_functions.py index ae41c63..ba8199d 100644 --- a/tests/test_queries/test_aggr_functions.py +++ b/tests/test_queries/test_aggr_functions.py @@ -1,36 +1,27 @@ from typing import Optional -import databases +import ormar import pytest import pytest_asyncio -import sqlalchemy - -import ormar from ormar.exceptions import QueryDefinitionError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" - order_by = ["-name"] + ormar_config = base_ormar_config.copy(tablename="authors", order_by=["-name"]) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" - order_by = ["year", "-ranking"] + ormar_config = base_ormar_config.copy( + tablename="books", order_by=["year", "-ranking"] + ) id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey(Author) @@ -39,19 +30,13 @@ class Book(ormar.Model): ranking: int = ormar.Integer(nullable=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest_asyncio.fixture(autouse=True, scope="function") async def cleanup(): yield - async with database: + async with base_ormar_config.database: await Book.objects.delete(each=True) await Author.objects.delete(each=True) @@ -65,7 +50,7 @@ async def sample_data(): @pytest.mark.asyncio async def test_min_method(): - async with database: + async with base_ormar_config.database: await sample_data() assert await Book.objects.min("year") == 1920 result = await Book.objects.min(["year", "ranking"]) @@ -89,7 +74,7 @@ async def test_min_method(): @pytest.mark.asyncio async def test_max_method(): - async with database: + async with base_ormar_config.database: await sample_data() assert await Book.objects.max("year") == 1930 result = await Book.objects.max(["year", "ranking"]) @@ -113,7 +98,7 @@ async def test_max_method(): @pytest.mark.asyncio async def test_sum_method(): - async with database: + async with base_ormar_config.database: await sample_data() assert await Book.objects.sum("year") == 5773 result = await Book.objects.sum(["year", "ranking"]) @@ -138,7 +123,7 @@ async def test_sum_method(): @pytest.mark.asyncio async def test_avg_method(): - async with database: + async with base_ormar_config.database: await sample_data() assert round(float(await Book.objects.avg("year")), 2) == 1924.33 result = await Book.objects.avg(["year", "ranking"]) @@ -166,7 +151,7 @@ async def test_avg_method(): @pytest.mark.asyncio async def test_queryset_method(): - async with database: + async with base_ormar_config.database: await sample_data() author = await Author.objects.select_related("books").get() assert await author.books.min("year") == 1920 @@ -180,7 +165,7 @@ async def test_queryset_method(): @pytest.mark.asyncio async def test_count_method(): - async with database: + async with base_ormar_config.database: await sample_data() count = await Author.objects.select_related("books").count() diff --git a/tests/test_queries/test_deep_relations_select_all.py b/tests/test_queries/test_deep_relations_select_all.py index 948b81f..8e8eabc 100644 --- a/tests/test_queries/test_deep_relations_select_all.py +++ b/tests/test_queries/test_deep_relations_select_all.py @@ -1,20 +1,15 @@ -import databases +import ormar import pytest from sqlalchemy import func -import ormar -import sqlalchemy -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class Chart(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "charts" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="charts") chart_id = ormar.Integer(primary_key=True, autoincrement=True) name = ormar.String(max_length=200, unique=True, index=True) @@ -28,10 +23,7 @@ class Chart(ormar.Model): class Report(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "reports" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="reports") report_id = ormar.Integer(primary_key=True, autoincrement=True) name = ormar.String(max_length=200, unique=True, index=True) @@ -40,10 +32,7 @@ class Report(ormar.Model): class Language(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "languages" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="languages") language_id = ormar.Integer(primary_key=True, autoincrement=True) code = ormar.String(max_length=5) @@ -51,20 +40,14 @@ class Language(ormar.Model): class TranslationNode(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "translation_nodes" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="translation_nodes") node_id = ormar.Integer(primary_key=True, autoincrement=True) node_type = ormar.String(max_length=200) class Translation(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "translations" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="translations") translation_id = ormar.Integer(primary_key=True, autoincrement=True) node_id = ormar.ForeignKey(TranslationNode, related_name="translations") @@ -73,10 +56,7 @@ class Translation(ormar.Model): class Filter(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "filters" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="filters") filter_id = ormar.Integer(primary_key=True, autoincrement=True) name = ormar.String(max_length=200, unique=True, index=True) @@ -90,10 +70,7 @@ class Filter(ormar.Model): class FilterValue(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "filter_values" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="filter_values") value_id = ormar.Integer(primary_key=True, autoincrement=True) value = ormar.String(max_length=300) @@ -103,10 +80,7 @@ class FilterValue(ormar.Model): class FilterXReport(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "filters_x_reports" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="filters_x_reports") filter_x_report_id = ormar.Integer(primary_key=True) filter = ormar.ForeignKey(Filter, name="filter_id", related_name="reports") @@ -117,10 +91,7 @@ class FilterXReport(ormar.Model): class ChartXReport(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "charts_x_reports" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="charts_x_reports") chart_x_report_id = ormar.Integer(primary_key=True) chart = ormar.ForeignKey(Chart, name="chart_id", related_name="reports") @@ -130,10 +101,7 @@ class ChartXReport(ormar.Model): class ChartColumn(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "charts_columns" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="charts_columns") column_id = ormar.Integer(primary_key=True, autoincrement=True) chart = ormar.ForeignKey(Chart, name="chart_id", related_name="columns") @@ -142,17 +110,11 @@ class ChartColumn(ormar.Model): translation = ormar.ForeignKey(TranslationNode, name="translation_node_id") -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_saving_related_fk_rel(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await Report.objects.select_all(follow=True).all() diff --git a/tests/test_queries/test_filter_groups.py b/tests/test_queries/test_filter_groups.py index 2e8a26b..5fd8c39 100644 --- a/tests/test_queries/test_filter_groups.py +++ b/tests/test_queries/test_filter_groups.py @@ -1,31 +1,22 @@ from typing import Optional -import databases -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" + ormar_config = base_ormar_config.copy(tablename="books") id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey(Author) @@ -33,6 +24,9 @@ class Book(ormar.Model): year: int = ormar.Integer(nullable=True) +create_test_database = init_tests(base_ormar_config) + + def test_or_group(): result = ormar.or_(name="aa", books__title="bb") result.resolve(model_cls=Author) diff --git a/tests/test_queries/test_indirect_relations_to_self.py b/tests/test_queries/test_indirect_relations_to_self.py index 4ca7ade..ca0e9a8 100644 --- a/tests/test_queries/test_indirect_relations_to_self.py +++ b/tests/test_queries/test_indirect_relations_to_self.py @@ -1,21 +1,16 @@ from datetime import datetime -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Node(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "node" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="node") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=120) @@ -24,10 +19,7 @@ class Node(ormar.Model): class Edge(ormar.Model): - class Meta(ormar.ModelMeta): - tablename = "edge" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="edge") id: str = ormar.String(primary_key=True, max_length=12) src_node: Node = ormar.ForeignKey(Node, related_name="next_edges") @@ -36,18 +28,12 @@ class Edge(ormar.Model): created_at: datetime = ormar.DateTime(timezone=True, default=datetime.now) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_sort_order_on_main_model(): - async with database: + async with base_ormar_config.database: node1 = await Node(name="Node 1").save() node2 = await Node(name="Node 2").save() node3 = await Node(name="Node 3").save() diff --git a/tests/test_queries/test_isnull_filter.py b/tests/test_queries/test_isnull_filter.py index 4c368e1..0f166ca 100644 --- a/tests/test_queries/test_isnull_filter.py +++ b/tests/test_queries/test_isnull_filter.py @@ -1,32 +1,23 @@ from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" + ormar_config = base_ormar_config.copy(tablename="books") id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey(Author) @@ -35,10 +26,7 @@ class Book(ormar.Model): class JsonModel(ormar.Model): - class Meta(ormar.ModelMeta): - metadata = metadata - database = database - tablename = "jsons" + ormar_config = base_ormar_config.copy(tablename="jsons") id = ormar.Integer(primary_key=True) text_field = ormar.Text(nullable=True) @@ -46,18 +34,12 @@ class JsonModel(ormar.Model): json_not_null = ormar.JSON() -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_is_null(): - async with database: + async with base_ormar_config.database: tolkien = await Author.objects.create(name="J.R.R. Tolkien") await Book.objects.create(author=tolkien, title="The Hobbit") await Book.objects.create( @@ -99,7 +81,7 @@ async def test_is_null(): @pytest.mark.asyncio async def test_isnull_json(): - async with database: + async with base_ormar_config.database: author = await JsonModel.objects.create(json_not_null=None) assert author.json_field is None non_null_text_fields = await JsonModel.objects.all(text_field__isnull=False) diff --git a/tests/test_queries/test_nested_reverse_relations.py b/tests/test_queries/test_nested_reverse_relations.py index 914adc5..0c8069f 100644 --- a/tests/test_queries/test_nested_reverse_relations.py +++ b/tests/test_queries/test_nested_reverse_relations.py @@ -1,32 +1,23 @@ from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class DataSource(ormar.Model): - class Meta(BaseMeta): - tablename = "datasources" + ormar_config = base_ormar_config.copy(tablename="datasources") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200, unique=True, index=True) class DataSourceTable(ormar.Model): - class Meta(BaseMeta): - tablename = "source_tables" + ormar_config = base_ormar_config.copy(tablename="source_tables") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200, index=True) @@ -36,8 +27,7 @@ class DataSourceTable(ormar.Model): class DataSourceTableColumn(ormar.Model): - class Meta(BaseMeta): - tablename = "source_columns" + ormar_config = base_ormar_config.copy(tablename="source_columns") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200, index=True) @@ -47,18 +37,12 @@ class DataSourceTableColumn(ormar.Model): ) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): # pragma: no cover - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_double_nested_reverse_relation(): - async with database: + async with base_ormar_config.database: data_source = await DataSource(name="local").save() test_tables = [ { diff --git a/tests/test_queries/test_non_relation_fields_not_merged.py b/tests/test_queries/test_non_relation_fields_not_merged.py index 38cda87..84500cb 100644 --- a/tests/test_queries/test_non_relation_fields_not_merged.py +++ b/tests/test_queries/test_non_relation_fields_not_merged.py @@ -1,49 +1,34 @@ -from typing import Dict, List, Optional - -import databases -import pytest -import sqlalchemy +from typing import Optional import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Chart(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) datasets = ormar.JSON() class Config(ormar.Model): - class Meta(BaseMeta): - tablename = "books" + ormar_config = base_ormar_config.copy(tablename="books") id: int = ormar.Integer(primary_key=True) chart: Optional[Chart] = ormar.ForeignKey(Chart) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_list_field_that_is_not_relation_is_not_merged(): - async with database: + async with base_ormar_config.database: chart = await Chart.objects.create(datasets=[{"test": "ok"}]) await Config.objects.create(chart=chart) await Config.objects.create(chart=chart) diff --git a/tests/test_queries/test_or_filters.py b/tests/test_queries/test_or_filters.py index 370c848..db61646 100644 --- a/tests/test_queries/test_or_filters.py +++ b/tests/test_queries/test_or_filters.py @@ -1,33 +1,24 @@ from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar +import pytest from ormar.exceptions import QueryDefinitionError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" + ormar_config = base_ormar_config.copy(tablename="books") id: int = ormar.Integer(primary_key=True) author: Optional[Author] = ormar.ForeignKey(Author) @@ -35,18 +26,12 @@ class Book(ormar.Model): year: int = ormar.Integer(nullable=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_or_filters(): - async with database: + async with base_ormar_config.database: tolkien = await Author(name="J.R.R. Tolkien").save() await Book(author=tolkien, title="The Hobbit", year=1933).save() await Book(author=tolkien, title="The Lord of the Rings", year=1955).save() diff --git a/tests/test_queries/test_order_by.py b/tests/test_queries/test_order_by.py index d32cf68..5f8ac9f 100644 --- a/tests/test_queries/test_order_by.py +++ b/tests/test_queries/test_order_by.py @@ -1,21 +1,16 @@ from typing import List, Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Song(ormar.Model): - class Meta: - tablename = "songs" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="songs") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -23,30 +18,21 @@ class Song(ormar.Model): class Owner(ormar.Model): - class Meta: - tablename = "owners" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="owners") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class AliasNested(ormar.Model): - class Meta: - tablename = "aliases_nested" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="aliases_nested") id: int = ormar.Integer(name="alias_id", primary_key=True) name: str = ormar.String(name="alias_name", max_length=100) class AliasTest(ormar.Model): - class Meta: - tablename = "aliases" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="aliases") id: int = ormar.Integer(name="alias_id", primary_key=True) name: str = ormar.String(name="alias_name", max_length=100) @@ -54,10 +40,7 @@ class AliasTest(ormar.Model): class Toy(ormar.Model): - class Meta: - tablename = "toys" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="toys") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -65,20 +48,14 @@ class Toy(ormar.Model): class Factory(ormar.Model): - class Meta: - tablename = "factories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="factories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Car(ormar.Model): - class Meta: - tablename = "cars" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="cars") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -86,28 +63,19 @@ class Car(ormar.Model): class User(ormar.Model): - class Meta: - tablename = "users" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="users") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) cars: List[Car] = ormar.ManyToMany(Car) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_sort_order_on_main_model(): - async with database: + async with base_ormar_config.database: await Song.objects.create(name="Song 3", sort_order=3) await Song.objects.create(name="Song 1", sort_order=1) await Song.objects.create(name="Song 2", sort_order=2) @@ -166,7 +134,7 @@ async def test_sort_order_on_main_model(): @pytest.mark.asyncio async def test_sort_order_on_related_model(): - async with database: + async with base_ormar_config.database: aphrodite = await Owner.objects.create(name="Aphrodite") hermes = await Owner.objects.create(name="Hermes") zeus = await Owner.objects.create(name="Zeus") @@ -252,7 +220,7 @@ async def test_sort_order_on_related_model(): @pytest.mark.asyncio async def test_sort_order_on_many_to_many(): - async with database: + async with base_ormar_config.database: factory1 = await Factory.objects.create(name="Factory 1") factory2 = await Factory.objects.create(name="Factory 2") @@ -326,7 +294,7 @@ async def test_sort_order_on_many_to_many(): @pytest.mark.asyncio async def test_sort_order_with_aliases(): - async with database: + async with base_ormar_config.database: al1 = await AliasTest.objects.create(name="Test4") al2 = await AliasTest.objects.create(name="Test2") al3 = await AliasTest.objects.create(name="Test1") diff --git a/tests/test_queries/test_pagination.py b/tests/test_queries/test_pagination.py index 9650736..790945b 100644 --- a/tests/test_queries/test_pagination.py +++ b/tests/test_queries/test_pagination.py @@ -1,56 +1,39 @@ -import databases -import pytest -import sqlalchemy - import ormar -from ormar import ModelMeta +import pytest from ormar.exceptions import QueryDefinitionError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Car(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class UsersCar(ormar.Model): - class Meta(BaseMeta): - tablename = "cars_x_users" + ormar_config = base_ormar_config.copy(tablename="cars_x_users") class User(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) cars = ormar.ManyToMany(Car, through=UsersCar) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_limit_zero(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): for i in range(5): await Car(name=f"{i}").save() @@ -61,8 +44,8 @@ async def test_limit_zero(): @pytest.mark.asyncio async def test_pagination_errors(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): with pytest.raises(QueryDefinitionError): await Car.objects.paginate(0).all() @@ -72,8 +55,8 @@ async def test_pagination_errors(): @pytest.mark.asyncio async def test_pagination_on_single_model(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): for i in range(20): await Car(name=f"{i}").save() @@ -96,8 +79,8 @@ async def test_pagination_on_single_model(): @pytest.mark.asyncio async def test_proxy_pagination(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): user = await User(name="Jon").save() for i in range(20): diff --git a/tests/test_queries/test_queryproxy_on_m2m_models.py b/tests/test_queries/test_queryproxy_on_m2m_models.py index 17d9233..23de3d7 100644 --- a/tests/test_queries/test_queryproxy_on_m2m_models.py +++ b/tests/test_queries/test_queryproxy_on_m2m_models.py @@ -1,33 +1,24 @@ -import asyncio from typing import List, Optional, Union -import databases -import pytest -import sqlalchemy - import ormar +import pytest from ormar.exceptions import QueryDefinitionError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Subject(ormar.Model): - class Meta: - tablename = "subjects" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="subjects") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=80) class Author(ormar.Model): - class Meta: - tablename = "authors" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) first_name: str = ormar.String(max_length=80) @@ -35,10 +26,7 @@ class Author(ormar.Model): class Category(ormar.Model): - class Meta: - tablename = "categories" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) @@ -47,17 +35,11 @@ class Category(ormar.Model): class PostCategory(ormar.Model): - class Meta: - tablename = "posts_categories" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="posts_categories") class Post(ormar.Model): - class Meta: - tablename = "posts" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="posts") id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -67,18 +49,13 @@ class Post(ormar.Model): author: Optional[Author] = ormar.ForeignKey(Author) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_queryset_methods(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): guido = await Author.objects.create( first_name="Guido", last_name="Van Rossum" ) @@ -183,8 +160,8 @@ async def test_queryset_methods(): @pytest.mark.asyncio async def test_queryset_update(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): guido = await Author.objects.create( first_name="Guido", last_name="Van Rossum" ) diff --git a/tests/test_queries/test_queryset_level_methods.py b/tests/test_queries/test_queryset_level_methods.py index e0f4f88..801b028 100644 --- a/tests/test_queries/test_queryset_level_methods.py +++ b/tests/test_queries/test_queryset_level_methods.py @@ -1,23 +1,21 @@ from enum import Enum from typing import Optional -import databases +import ormar import pydantic import pytest -import sqlalchemy -from pydantic import Json - -import ormar from ormar import QuerySet from ormar.exceptions import ( + ModelListEmptyError, ModelPersistenceError, QueryDefinitionError, - ModelListEmptyError, ) -from tests.settings import DATABASE_URL +from pydantic import Json -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config(force_rollback=True) class MySize(Enum): @@ -26,10 +24,7 @@ class MySize(Enum): class Book(ormar.Model): - class Meta: - tablename = "books" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="books") id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -37,15 +32,11 @@ class Book(ormar.Model): genre: str = ormar.String( max_length=100, default="Fiction", - choices=["Fiction", "Adventure", "Historic", "Fantasy"], ) class ToDo(ormar.Model): - class Meta: - tablename = "todos" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="todos") id: int = ormar.Integer(primary_key=True) text: str = ormar.String(max_length=500) @@ -55,20 +46,14 @@ class ToDo(ormar.Model): class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=500) class Note(ormar.Model): - class Meta: - tablename = "notes" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="notes") id: int = ormar.Integer(primary_key=True) text: str = ormar.String(max_length=500) @@ -76,10 +61,7 @@ class Note(ormar.Model): class ItemConfig(ormar.Model): - class Meta(ormar.ModelMeta): - metadata = metadata - database = database - tablename = "item_config" + ormar_config = base_ormar_config.copy(tablename="item_config") id: Optional[int] = ormar.Integer(primary_key=True) item_id: str = ormar.String(max_length=32, index=True) @@ -97,39 +79,29 @@ class QuerySetCls(QuerySet): class Customer(ormar.Model): - class Meta: - metadata = metadata - database = database - tablename = "customer" - queryset_class = QuerySetCls + ormar_config = base_ormar_config.copy( + tablename="customer", + queryset_class=QuerySetCls, + ) id: Optional[int] = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=32) class JsonTestModel(ormar.Model): - class Meta(ormar.ModelMeta): - metadata = metadata - database = database - tablename = "test_model" + ormar_config = base_ormar_config.copy(tablename="test_model") id: int = ormar.Integer(primary_key=True) json_field: Json = ormar.JSON() -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_delete_and_update(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await Book.objects.create( title="Tom Sawyer", author="Twain, Mark", genre="Adventure" ) @@ -184,7 +156,7 @@ async def test_delete_and_update(): @pytest.mark.asyncio async def test_get_or_create(): - async with database: + async with base_ormar_config.database: tom, created = await Book.objects.get_or_create( title="Volume I", author="Anonymous", genre="Fiction" ) @@ -210,7 +182,7 @@ async def test_get_or_create(): @pytest.mark.asyncio async def test_get_or_create_with_defaults(): - async with database: + async with base_ormar_config.database: book, created = await Book.objects.get_or_create( title="Nice book", _defaults={"author": "Mojix", "genre": "Historic"} ) @@ -246,7 +218,7 @@ async def test_get_or_create_with_defaults(): @pytest.mark.asyncio async def test_update_or_create(): - async with database: + async with base_ormar_config.database: tom = await Book.objects.update_or_create( title="Volume I", author="Anonymous", genre="Fiction" ) @@ -269,7 +241,7 @@ async def test_update_or_create(): @pytest.mark.asyncio async def test_bulk_create(): - async with database: + async with base_ormar_config.database: await ToDo.objects.bulk_create( [ ToDo(text="Buy the groceries."), @@ -292,7 +264,7 @@ async def test_bulk_create(): @pytest.mark.asyncio async def test_bulk_create_json_field(): - async with database: + async with base_ormar_config.database: json_value = {"a": 1} test_model_1 = JsonTestModel(id=1, json_field=json_value) test_model_2 = JsonTestModel(id=2, json_field=json_value) @@ -308,11 +280,11 @@ async def test_bulk_create_json_field(): assert test_model_1.json_field == test_model_2.json_field # True # try to query the json field - table = JsonTestModel.Meta.table + table = JsonTestModel.ormar_config.table query = table.select().where(table.c.json_field["a"].as_integer() == 1) res = [ JsonTestModel.from_row(record, source_model=JsonTestModel) - for record in await database.fetch_all(query) + for record in await base_ormar_config.database.fetch_all(query) ] assert test_model_1 in res @@ -322,7 +294,7 @@ async def test_bulk_create_json_field(): @pytest.mark.asyncio async def test_bulk_create_with_relation(): - async with database: + async with base_ormar_config.database: category = await Category.objects.create(name="Sample Category") await Note.objects.bulk_create( @@ -340,7 +312,7 @@ async def test_bulk_create_with_relation(): @pytest.mark.asyncio async def test_bulk_update(): - async with database: + async with base_ormar_config.database: await ToDo.objects.bulk_create( [ ToDo(text="Buy the groceries."), @@ -371,7 +343,7 @@ async def test_bulk_update(): @pytest.mark.asyncio async def test_bulk_update_with_only_selected_columns(): - async with database: + async with base_ormar_config.database: await ToDo.objects.bulk_create( [ ToDo(text="Reset the world simulation.", completed=False), @@ -400,7 +372,7 @@ async def test_bulk_update_with_only_selected_columns(): @pytest.mark.asyncio async def test_bulk_update_with_relation(): - async with database: + async with base_ormar_config.database: category = await Category.objects.create(name="Sample Category") category2 = await Category.objects.create(name="Sample II Category") @@ -429,7 +401,7 @@ async def test_bulk_update_with_relation(): @pytest.mark.asyncio async def test_bulk_update_not_saved_objts(): - async with database: + async with base_ormar_config.database: category = await Category.objects.create(name="Sample Category") with pytest.raises(ModelPersistenceError): await Note.objects.bulk_update( @@ -445,7 +417,7 @@ async def test_bulk_update_not_saved_objts(): @pytest.mark.asyncio async def test_bulk_operations_with_json(): - async with database: + async with base_ormar_config.database: items = [ ItemConfig(item_id="test1"), ItemConfig(item_id="test2"), @@ -469,18 +441,18 @@ async def test_bulk_operations_with_json(): items = await ItemConfig.objects.filter(ItemConfig.id > 1).all() assert all(x.pairs == {"b": 2} for x in items) - table = ItemConfig.Meta.table + table = ItemConfig.ormar_config.table query = table.select().where(table.c.pairs["b"].as_integer() == 2) res = [ ItemConfig.from_row(record, source_model=ItemConfig) - for record in await database.fetch_all(query) + for record in await base_ormar_config.database.fetch_all(query) ] assert len(res) == 2 @pytest.mark.asyncio async def test_custom_queryset_cls(): - async with database: + async with base_ormar_config.database: with pytest.raises(ValueError): await Customer.objects.first_or_404(id=1) @@ -491,7 +463,7 @@ async def test_custom_queryset_cls(): @pytest.mark.asyncio async def test_filter_enum(): - async with database: + async with base_ormar_config.database: it = ItemConfig(item_id="test_1") await it.save() diff --git a/tests/test_queries/test_quoting_table_names_in_on_join_clause.py b/tests/test_queries/test_quoting_table_names_in_on_join_clause.py index 9421f01..e74a9f6 100644 --- a/tests/test_queries/test_quoting_table_names_in_on_join_clause.py +++ b/tests/test_queries/test_quoting_table_names_in_on_join_clause.py @@ -2,24 +2,17 @@ import datetime import uuid from typing import Dict, Optional, Union -import databases -import pytest -import sqlalchemy -from sqlalchemy import create_engine - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() -engine = create_engine(DATABASE_URL) +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Team(ormar.Model): - class Meta: - tablename: str = "team" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="team") id: uuid.UUID = ormar.UUID(default=uuid.uuid4, primary_key=True, index=True) name = ormar.Text(nullable=True) @@ -29,10 +22,7 @@ class Team(ormar.Model): class User(ormar.Model): - class Meta: - tablename: str = "user" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="user") id: uuid.UUID = ormar.UUID(default=uuid.uuid4, primary_key=True, index=True) client_user_id = ormar.Text() @@ -41,23 +31,16 @@ class User(ormar.Model): class Order(ormar.Model): - class Meta: - tablename: str = "order" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="order") id: uuid.UUID = ormar.UUID(default=uuid.uuid4, primary_key=True, index=True) user: Optional[Union[User, Dict]] = ormar.ForeignKey(User) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_quoting_on_clause_without_prefix(): - async with database: + async with base_ormar_config.database: await User.objects.select_related("orders").all() diff --git a/tests/test_queries/test_reserved_sql_keywords_escaped.py b/tests/test_queries/test_reserved_sql_keywords_escaped.py index b881b3b..0998233 100644 --- a/tests/test_queries/test_reserved_sql_keywords_escaped.py +++ b/tests/test_queries/test_reserved_sql_keywords_escaped.py @@ -1,23 +1,14 @@ -import databases -import pytest -import sqlalchemy - import ormar +import pytest -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config(force_rollback=True) class User(ormar.Model): - class Meta(BaseMeta): - tablename = "user" + ormar_config = base_ormar_config.copy(tablename="user") id: int = ormar.Integer(primary_key=True, autoincrement=True, nullable=False) user: str = ormar.String( @@ -33,26 +24,19 @@ class User(ormar.Model): class Task(ormar.Model): - class Meta(BaseMeta): - tablename = "task" + ormar_config = base_ormar_config.copy(tablename="task") id: int = ormar.Integer(primary_key=True, autoincrement=True, nullable=False) from_: str = ormar.String(name="from", nullable=True, max_length=200) user = ormar.ForeignKey(User) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_single_model_quotes(): - async with database: + async with base_ormar_config.database: await User.objects.create( user="test", first="first", @@ -68,7 +52,7 @@ async def test_single_model_quotes(): @pytest.mark.asyncio async def test_two_model_quotes(): - async with database: + async with base_ormar_config.database: user = await User.objects.create( user="test", first="first", diff --git a/tests/test_queries/test_reverse_fk_queryset.py b/tests/test_queries/test_reverse_fk_queryset.py index 25f54a0..2893708 100644 --- a/tests/test_queries/test_reverse_fk_queryset.py +++ b/tests/test_queries/test_reverse_fk_queryset.py @@ -1,22 +1,17 @@ from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar +import pytest from ormar import NoMatch -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="albums") id: int = ormar.Integer(primary_key=True, name="album_id") name: str = ormar.String(max_length=100) @@ -24,20 +19,14 @@ class Album(ormar.Model): class Writer(ormar.Model): - class Meta: - tablename = "writers" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="writers") id: int = ormar.Integer(primary_key=True, name="writer_id") name: str = ormar.String(max_length=100) class Track(ormar.Model): - class Meta: - tablename = "tracks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="tracks") id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album, name="album_id") @@ -67,19 +56,13 @@ async def get_sample_data(): return album, [track1, track2, tracks3] -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_quering_by_reverse_fk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): sample_data = await get_sample_data() track1 = sample_data[1][0] album = await Album.objects.first() @@ -133,8 +116,8 @@ async def test_quering_by_reverse_fk(): @pytest.mark.asyncio async def test_getting(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): sample_data = await get_sample_data() album = sample_data[0] track1 = await album.tracks.fields(["album", "title", "position"]).get( @@ -203,8 +186,8 @@ async def test_getting(): @pytest.mark.asyncio async def test_cleaning_related(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): sample_data = await get_sample_data() album = sample_data[0] await album.tracks.clear(keep_reversed=False) @@ -218,8 +201,8 @@ async def test_cleaning_related(): @pytest.mark.asyncio async def test_loading_related(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): sample_data = await get_sample_data() album = sample_data[0] tracks = await album.tracks.select_related("written_by").all() @@ -237,8 +220,8 @@ async def test_loading_related(): @pytest.mark.asyncio async def test_adding_removing(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): sample_data = await get_sample_data() album = sample_data[0] track_new = await Track(title="Rainbow", position=5, play_count=300).save() diff --git a/tests/test_queries/test_selecting_subset_of_columns.py b/tests/test_queries/test_selecting_subset_of_columns.py index da5c000..d7b9439 100644 --- a/tests/test_queries/test_selecting_subset_of_columns.py +++ b/tests/test_queries/test_selecting_subset_of_columns.py @@ -1,25 +1,20 @@ import asyncio import itertools -from typing import Optional, List +from typing import List, Optional -import databases +import ormar import pydantic import pytest import pytest_asyncio -import sqlalchemy -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class NickNames(ormar.Model): - class Meta: - tablename = "nicks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="nicks") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -27,17 +22,11 @@ class NickNames(ormar.Model): class NicksHq(ormar.Model): - class Meta: - tablename = "nicks_x_hq" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="nicks_x_hq") class HQ(ormar.Model): - class Meta: - tablename = "hqs" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="hqs") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="hq_name") @@ -45,10 +34,7 @@ class HQ(ormar.Model): class Company(ormar.Model): - class Meta: - tablename = "companies" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="companies") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=False, name="company_name") @@ -57,10 +43,7 @@ class Company(ormar.Model): class Car(ormar.Model): - class Meta: - tablename = "cars" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="cars") id: int = ormar.Integer(primary_key=True) manufacturer: Optional[Company] = ormar.ForeignKey(Company) @@ -71,13 +54,7 @@ class Car(ormar.Model): aircon_type: str = ormar.String(max_length=20, nullable=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.fixture(scope="module") @@ -89,7 +66,7 @@ def event_loop(): @pytest_asyncio.fixture(autouse=True, scope="module") async def sample_data(event_loop, create_test_database): - async with database: + async with base_ormar_config.database: nick1 = await NickNames.objects.create(name="Nippon", is_lame=False) nick2 = await NickNames.objects.create(name="EroCherry", is_lame=True) hq = await HQ.objects.create(name="Japan") @@ -126,8 +103,8 @@ async def sample_data(event_loop, create_test_database): @pytest.mark.asyncio async def test_selecting_subset(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): all_cars = ( await Car.objects.select_related(["manufacturer__hq__nicks"]) .fields( @@ -226,7 +203,7 @@ async def test_selecting_subset(): assert all_cars_dummy[0].manufacturer.founded is None - with pytest.raises(pydantic.error_wrappers.ValidationError): + with pytest.raises(pydantic.ValidationError): # cannot exclude mandatory model columns - company__name in this example await Car.objects.select_related("manufacturer").fields( ["id", "name", "manufacturer__founded"] @@ -235,7 +212,7 @@ async def test_selecting_subset(): @pytest.mark.asyncio async def test_selecting_subset_of_through_model(): - async with database: + async with base_ormar_config.database: car = ( await Car.objects.select_related(["manufacturer__hq__nicks"]) .fields( diff --git a/tests/test_queries/test_values_and_values_list.py b/tests/test_queries/test_values_and_values_list.py index fdad81b..c090e4a 100644 --- a/tests/test_queries/test_values_and_values_list.py +++ b/tests/test_queries/test_values_and_values_list.py @@ -1,35 +1,26 @@ import asyncio from typing import List, Optional -import databases +import ormar import pytest import pytest_asyncio -import sqlalchemy - -import ormar from ormar.exceptions import QueryDefinitionError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() class User(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Role(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -37,8 +28,7 @@ class Role(ormar.Model): class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) @@ -47,20 +37,14 @@ class Category(ormar.Model): class Post(ormar.Model): - class Meta(BaseMeta): - tablename = "posts" + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=200) category: Optional[Category] = ormar.ForeignKey(Category) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.fixture(scope="module") @@ -72,7 +56,7 @@ def event_loop(): @pytest_asyncio.fixture(autouse=True, scope="module") async def sample_data(event_loop, create_test_database): - async with database: + async with base_ormar_config.database: creator = await User(name="Anonymous").save() admin = await Role(name="admin").save() editor = await Role(name="editor").save() @@ -86,7 +70,7 @@ async def sample_data(event_loop, create_test_database): @pytest.mark.asyncio async def test_simple_queryset_values(): - async with database: + async with base_ormar_config.database: posts = await Post.objects.values() assert posts == [ {"id": 1, "name": "Ormar strikes again!", "category": 1}, @@ -97,7 +81,7 @@ async def test_simple_queryset_values(): @pytest.mark.asyncio async def test_queryset_values_nested_relation(): - async with database: + async with base_ormar_config.database: posts = await Post.objects.select_related("category__created_by").values() assert posts == [ { @@ -138,7 +122,7 @@ async def test_queryset_values_nested_relation(): @pytest.mark.asyncio async def test_queryset_values_nested_relation_subset_of_fields(): - async with database: + async with base_ormar_config.database: posts = await Post.objects.select_related("category__created_by").values( ["name", "category__name", "category__created_by__name"] ) @@ -163,7 +147,7 @@ async def test_queryset_values_nested_relation_subset_of_fields(): @pytest.mark.asyncio async def test_queryset_simple_values_list(): - async with database: + async with base_ormar_config.database: posts = await Post.objects.values_list() assert posts == [ (1, "Ormar strikes again!", 1), @@ -174,7 +158,7 @@ async def test_queryset_simple_values_list(): @pytest.mark.asyncio async def test_queryset_nested_relation_values_list(): - async with database: + async with base_ormar_config.database: posts = await Post.objects.select_related("category__created_by").values_list() assert posts == [ (1, "Ormar strikes again!", 1, 1, "News", 0, 1, 1, "Anonymous"), @@ -195,7 +179,7 @@ async def test_queryset_nested_relation_values_list(): @pytest.mark.asyncio async def test_queryset_nested_relation_subset_of_fields_values_list(): - async with database: + async with base_ormar_config.database: posts = await Post.objects.select_related("category__created_by").values_list( ["name", "category__name", "category__created_by__name"] ) @@ -208,7 +192,7 @@ async def test_queryset_nested_relation_subset_of_fields_values_list(): @pytest.mark.asyncio async def test_m2m_values(): - async with database: + async with base_ormar_config.database: user = await User.objects.select_related("roles").values() assert user == [ { @@ -234,7 +218,7 @@ async def test_m2m_values(): @pytest.mark.asyncio async def test_nested_m2m_values(): - async with database: + async with base_ormar_config.database: user = ( await Role.objects.select_related("users__categories") .filter(name="admin") @@ -259,7 +243,7 @@ async def test_nested_m2m_values(): @pytest.mark.asyncio async def test_nested_m2m_values_without_through_explicit(): - async with database: + async with base_ormar_config.database: user = ( await Role.objects.select_related("users__categories") .filter(name="admin") @@ -278,7 +262,7 @@ async def test_nested_m2m_values_without_through_explicit(): @pytest.mark.asyncio async def test_nested_m2m_values_without_through_param(): - async with database: + async with base_ormar_config.database: user = ( await Role.objects.select_related("users__categories") .filter(name="admin") @@ -296,7 +280,7 @@ async def test_nested_m2m_values_without_through_param(): @pytest.mark.asyncio async def test_nested_m2m_values_no_through_and_m2m_models_but_keep_end_model(): - async with database: + async with base_ormar_config.database: user = ( await Role.objects.select_related("users__categories") .filter(name="admin") @@ -309,7 +293,7 @@ async def test_nested_m2m_values_no_through_and_m2m_models_but_keep_end_model(): @pytest.mark.asyncio async def test_nested_flatten_and_exception(): - async with database: + async with base_ormar_config.database: with pytest.raises(QueryDefinitionError): (await Role.objects.fields({"name", "id"}).values_list(flatten=True)) @@ -319,7 +303,7 @@ async def test_nested_flatten_and_exception(): @pytest.mark.asyncio async def test_empty_result(): - async with database: + async with base_ormar_config.database: roles = await Role.objects.filter(Role.name == "test").values_list() roles2 = await Role.objects.filter(Role.name == "test").values() assert roles == roles2 == [] @@ -327,7 +311,7 @@ async def test_empty_result(): @pytest.mark.asyncio async def test_queryset_values_multiple_select_related(): - async with database: + async with base_ormar_config.database: posts = ( await Category.objects.select_related(["created_by__roles", "posts"]) .filter(Category.created_by.roles.name == "editor") @@ -360,7 +344,7 @@ async def test_queryset_values_multiple_select_related(): @pytest.mark.asyncio async def test_querysetproxy_values(): - async with database: + async with base_ormar_config.database: role = ( await Role.objects.select_related("users__categories") .filter(name="admin") @@ -406,7 +390,7 @@ async def test_querysetproxy_values(): @pytest.mark.asyncio async def test_querysetproxy_values_list(): - async with database: + async with base_ormar_config.database: role = ( await Role.objects.select_related("users__categories") .filter(name="admin") diff --git a/tests/test_relations/test_cascades.py b/tests/test_relations/test_cascades.py index 49c4a03..22c079a 100644 --- a/tests/test_relations/test_cascades.py +++ b/tests/test_relations/test_cascades.py @@ -1,41 +1,30 @@ from typing import Optional -import databases +import ormar import pytest import pytest_asyncio -import sqlalchemy -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class Band(ormar.Model): - class Meta: - tablename = "bands" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="bands") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class ArtistsBands(ormar.Model): - class Meta: - tablename = "artists_x_bands" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="artists_x_bands") id: int = ormar.Integer(primary_key=True) class Artist(ormar.Model): - class Meta: - tablename = "artists" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="artists") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -43,10 +32,7 @@ class Artist(ormar.Model): class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="albums") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -54,36 +40,27 @@ class Album(ormar.Model): class Track(ormar.Model): - class Meta: - tablename = "tracks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="tracks") id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album, ondelete="CASCADE") title: str = ormar.String(max_length=100) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest_asyncio.fixture(scope="function") async def cleanup(): yield - async with database: + async with base_ormar_config.database: await Band.objects.delete(each=True) await Artist.objects.delete(each=True) @pytest.mark.asyncio async def test_simple_cascade(cleanup): - async with database: + async with base_ormar_config.database: artist = await Artist(name="Dr Alban").save() await Album(name="Jamaica", artist=artist).save() await Artist.objects.delete(id=artist.id) @@ -96,7 +73,7 @@ async def test_simple_cascade(cleanup): @pytest.mark.asyncio async def test_nested_cascade(cleanup): - async with database: + async with base_ormar_config.database: artist = await Artist(name="Dr Alban").save() album = await Album(name="Jamaica", artist=artist).save() await Track(title="Yuhu", album=album).save() @@ -115,7 +92,7 @@ async def test_nested_cascade(cleanup): @pytest.mark.asyncio async def test_many_to_many_cascade(cleanup): - async with database: + async with base_ormar_config.database: artist = await Artist(name="Dr Alban").save() band = await Band(name="Scorpions").save() await artist.bands.add(band) @@ -137,7 +114,7 @@ async def test_many_to_many_cascade(cleanup): @pytest.mark.asyncio async def test_reverse_many_to_many_cascade(cleanup): - async with database: + async with base_ormar_config.database: artist = await Artist(name="Dr Alban").save() band = await Band(name="Scorpions").save() await artist.bands.add(band) diff --git a/tests/test_relations/test_customizing_through_model_relation_names.py b/tests/test_relations/test_customizing_through_model_relation_names.py index ff99065..d752906 100644 --- a/tests/test_relations/test_customizing_through_model_relation_names.py +++ b/tests/test_relations/test_customizing_through_model_relation_names.py @@ -1,27 +1,21 @@ -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -metadata = sqlalchemy.MetaData() -database = databases.Database(DATABASE_URL, force_rollback=True) +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Course(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) course_name: str = ormar.String(max_length=100) class Student(ormar.Model): - class Meta: - database = database - metadata = metadata + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -32,27 +26,21 @@ class Student(ormar.Model): ) -# create db and tables -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_tables_columns(): - through_meta = Student.Meta.model_fields["courses"].through.Meta - assert "course_id" in through_meta.table.c - assert "student_id" in through_meta.table.c - assert "course_id" in through_meta.model_fields - assert "student_id" in through_meta.model_fields + through_config = Student.ormar_config.model_fields["courses"].through.ormar_config + assert "course_id" in through_config.table.c + assert "student_id" in through_config.table.c + assert "course_id" in through_config.model_fields + assert "student_id" in through_config.model_fields @pytest.mark.asyncio async def test_working_with_changed_through_names(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): to_save = { "course_name": "basic1", "students": [{"name": "Jack"}, {"name": "Abi"}], diff --git a/tests/test_relations/test_database_fk_creation.py b/tests/test_relations/test_database_fk_creation.py index 7f8e348..390adce 100644 --- a/tests/test_relations/test_database_fk_creation.py +++ b/tests/test_relations/test_database_fk_creation.py @@ -1,33 +1,25 @@ from typing import Optional -import databases +import ormar import pytest import sqlalchemy - -import ormar from ormar.fields.foreign_key import validate_referential_action -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() -engine = sqlalchemy.create_engine(DATABASE_URL) +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Artist(ormar.Model): - class Meta: - tablename = "artists" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="artists") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="albums") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -35,18 +27,14 @@ class Album(ormar.Model): class A(ormar.Model): - class Meta: - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=64, nullalbe=False) class B(ormar.Model): - class Meta: - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=64, nullalbe=False) @@ -54,25 +42,18 @@ class B(ormar.Model): class C(ormar.Model): - class Meta: - metadata = metadata - database = database + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=64, nullalbe=False) b: B = ormar.ForeignKey(to=B, ondelete=ormar.ReferentialAction.CASCADE) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_simple_cascade(): - inspector = sqlalchemy.inspect(engine) + inspector = sqlalchemy.inspect(base_ormar_config.engine) columns = inspector.get_columns("albums") assert len(columns) == 3 col_names = [col.get("name") for col in columns] @@ -88,7 +69,7 @@ def test_simple_cascade(): def test_validations_referential_action(): CASCADE = ormar.ReferentialAction.CASCADE.value - assert validate_referential_action(None) == None + assert validate_referential_action(None) is None assert validate_referential_action("cascade") == CASCADE assert validate_referential_action(ormar.ReferentialAction.CASCADE) == CASCADE @@ -98,11 +79,11 @@ def test_validations_referential_action(): @pytest.mark.asyncio async def test_cascade_clear(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): a = await A.objects.create(name="a") b = await B.objects.create(name="b", a=a) - c = await C.objects.create(name="c", b=b) + await C.objects.create(name="c", b=b) await a.bs.clear(keep_reversed=False) diff --git a/tests/test_relations/test_foreign_keys.py b/tests/test_relations/test_foreign_keys.py index e3412ed..49c1ae5 100644 --- a/tests/test_relations/test_foreign_keys.py +++ b/tests/test_relations/test_foreign_keys.py @@ -1,22 +1,17 @@ from typing import Optional -import databases -import pytest -import sqlalchemy - import ormar -from ormar.exceptions import NoMatch, MultipleMatches, RelationshipInstanceError -from tests.settings import DATABASE_URL +import pytest +from ormar.exceptions import MultipleMatches, NoMatch, RelationshipInstanceError -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="albums") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -24,10 +19,7 @@ class Album(ormar.Model): class Track(ormar.Model): - class Meta: - tablename = "tracks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="tracks") id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -38,10 +30,7 @@ class Track(ormar.Model): class Cover(ormar.Model): - class Meta: - tablename = "covers" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="covers") id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album, related_name="cover_pictures") @@ -49,20 +38,14 @@ class Cover(ormar.Model): class Organisation(ormar.Model): - class Meta: - tablename = "org" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="org") id: int = ormar.Integer(primary_key=True) - ident: str = ormar.String(max_length=100, choices=["ACME Ltd", "Other ltd"]) + ident: str = ormar.String(max_length=100) class Team(ormar.Model): - class Meta: - tablename = "teams" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="teams") id: int = ormar.Integer(primary_key=True) org: Optional[Organisation] = ormar.ForeignKey(Organisation) @@ -70,43 +53,34 @@ class Team(ormar.Model): class Member(ormar.Model): - class Meta: - tablename = "members" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="members") id: int = ormar.Integer(primary_key=True) team: Optional[Team] = ormar.ForeignKey(Team) email: str = ormar.String(max_length=100) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_wrong_query_foreign_key_type(): - async with database: + async with base_ormar_config.database: with pytest.raises(RelationshipInstanceError): Track(title="The Error", album="wrong_pk_type") @pytest.mark.asyncio async def test_setting_explicitly_empty_relation(): - async with database: + async with base_ormar_config.database: track = Track(album=None, title="The Bird", position=1) assert track.album is None @pytest.mark.asyncio async def test_related_name(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): album = await Album.objects.create(name="Vanilla") await Cover.objects.create(album=album, title="The cover file") assert len(album.cover_pictures) == 1 @@ -114,8 +88,8 @@ async def test_related_name(): @pytest.mark.asyncio async def test_model_crud(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): album = Album(name="Jamaica") await album.save() track1 = Track(album=album, title="The Bird", position=1) @@ -146,8 +120,8 @@ async def test_model_crud(): @pytest.mark.asyncio async def test_select_related(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): album = Album(name="Malibu") await album.save() track1 = Track(album=album, title="The Bird", position=1) @@ -175,8 +149,8 @@ async def test_select_related(): @pytest.mark.asyncio async def test_model_removal_from_relations(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): album = Album(name="Chichi") await album.save() track1 = Track(album=album, title="The Birdman", position=1) @@ -217,8 +191,8 @@ async def test_model_removal_from_relations(): @pytest.mark.asyncio async def test_fk_filter(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): malibu = Album(name="Malibu%") await malibu.save() await Track.objects.create(album=malibu, title="The Bird", position=1) @@ -277,8 +251,8 @@ async def test_fk_filter(): @pytest.mark.asyncio async def test_multiple_fk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): acme = await Organisation.objects.create(ident="ACME Ltd") red_team = await Team.objects.create(org=acme, name="Red Team") blue_team = await Team.objects.create(org=acme, name="Blue Team") @@ -301,18 +275,10 @@ async def test_multiple_fk(): assert member.team.org.ident == "ACME Ltd" -@pytest.mark.asyncio -async def test_wrong_choices(): - async with database: - async with database.transaction(force_rollback=True): - with pytest.raises(ValueError): - await Organisation.objects.create(ident="Test 1") - - @pytest.mark.asyncio async def test_pk_filter(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): fantasies = await Album.objects.create(name="Test") track = await Track.objects.create( album=fantasies, title="Test1", position=1 @@ -334,8 +300,8 @@ async def test_pk_filter(): @pytest.mark.asyncio async def test_limit_and_offset(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): fantasies = await Album.objects.create(name="Limitless") await Track.objects.create( id=None, album=fantasies, title="Sample", position=1 @@ -366,8 +332,8 @@ async def test_limit_and_offset(): @pytest.mark.asyncio async def test_get_exceptions(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): fantasies = await Album.objects.create(name="Test") with pytest.raises(NoMatch): @@ -382,8 +348,8 @@ async def test_get_exceptions(): @pytest.mark.asyncio async def test_wrong_model_passed_as_fk(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): with pytest.raises(RelationshipInstanceError): org = await Organisation.objects.create(ident="ACME Ltd") await Track.objects.create(album=org, title="Test1", position=1) @@ -391,8 +357,8 @@ async def test_wrong_model_passed_as_fk(): @pytest.mark.asyncio async def test_bulk_update_model_with_no_children(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): album = await Album.objects.create(name="Test") album.name = "Test2" await Album.objects.bulk_update([album], columns=["name"]) @@ -403,8 +369,8 @@ async def test_bulk_update_model_with_no_children(): @pytest.mark.asyncio async def test_bulk_update_model_with_children(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): best_seller = await Album.objects.create(name="to_be_best_seller") best_seller2 = await Album.objects.create(name="to_be_best_seller2") not_best_seller = await Album.objects.create(name="unpopular") diff --git a/tests/test_relations/test_m2m_through_fields.py b/tests/test_relations/test_m2m_through_fields.py index 75f279e..82b1d62 100644 --- a/tests/test_relations/test_m2m_through_fields.py +++ b/tests/test_relations/test_m2m_through_fields.py @@ -1,33 +1,23 @@ -from typing import Any, Sequence, cast - -import databases -import pytest -import sqlalchemy -from pydantic.typing import ForwardRef +from typing import Any, ForwardRef import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config(force_rollback=True) class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id = ormar.Integer(primary_key=True) name = ormar.String(max_length=40) class PostCategory(ormar.Model): - class Meta(BaseMeta): - tablename = "posts_x_categories" + ormar_config = base_ormar_config.copy(tablename="posts_x_categories") id: int = ormar.Integer(primary_key=True) sort_order: int = ormar.Integer(nullable=True) @@ -35,16 +25,14 @@ class PostCategory(ormar.Model): class Blog(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) class Post(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -52,26 +40,18 @@ class Post(ormar.Model): blog = ormar.ForeignKey(Blog) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) class PostCategory2(ormar.Model): - class Meta(BaseMeta): - tablename = "posts_x_categories2" + ormar_config = base_ormar_config.copy(tablename="posts_x_categories2") id: int = ormar.Integer(primary_key=True) sort_order: int = ormar.Integer(nullable=True) class Post2(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -80,16 +60,16 @@ class Post2(ormar.Model): @pytest.mark.asyncio async def test_forward_ref_is_updated(): - async with database: - assert Post2.Meta.requires_ref_update + async with base_ormar_config.database: + assert Post2.ormar_config.requires_ref_update Post2.update_forward_refs() - assert Post2.Meta.model_fields["postcategory2"].to == PostCategory2 + assert Post2.ormar_config.model_fields["postcategory2"].to == PostCategory2 @pytest.mark.asyncio async def test_setting_fields_on_through_model(): - async with database: + async with base_ormar_config.database: post = await Post(title="Test post").save() category = await Category(name="Test category").save() await post.categories.add(category) @@ -100,7 +80,7 @@ async def test_setting_fields_on_through_model(): @pytest.mark.asyncio async def test_setting_additional_fields_on_through_model_in_add(): - async with database: + async with base_ormar_config.database: post = await Post(title="Test post").save() category = await Category(name="Test category").save() await post.categories.add(category, sort_order=1) @@ -110,7 +90,7 @@ async def test_setting_additional_fields_on_through_model_in_add(): @pytest.mark.asyncio async def test_setting_additional_fields_on_through_model_in_create(): - async with database: + async with base_ormar_config.database: post = await Post(title="Test post").save() await post.categories.create( name="Test category2", postcategory={"sort_order": 2} @@ -121,7 +101,7 @@ async def test_setting_additional_fields_on_through_model_in_create(): @pytest.mark.asyncio async def test_getting_additional_fields_from_queryset() -> Any: - async with database: + async with base_ormar_config.database: post = await Post(title="Test post").save() await post.categories.create( name="Test category1", postcategory={"sort_order": 1} @@ -143,7 +123,7 @@ async def test_getting_additional_fields_from_queryset() -> Any: @pytest.mark.asyncio async def test_only_one_side_has_through() -> Any: - async with database: + async with base_ormar_config.database: post = await Post(title="Test post").save() await post.categories.create( name="Test category1", postcategory={"sort_order": 1} @@ -168,7 +148,7 @@ async def test_only_one_side_has_through() -> Any: @pytest.mark.asyncio async def test_filtering_by_through_model() -> Any: - async with database: + async with base_ormar_config.database: post = await Post(title="Test post").save() await post.categories.create( name="Test category1", @@ -195,7 +175,7 @@ async def test_filtering_by_through_model() -> Any: @pytest.mark.asyncio async def test_deep_filtering_by_through_model() -> Any: - async with database: + async with base_ormar_config.database: blog = await Blog(title="My Blog").save() post = await Post(title="Test post", blog=blog).save() @@ -226,7 +206,7 @@ async def test_deep_filtering_by_through_model() -> Any: @pytest.mark.asyncio async def test_ordering_by_through_model() -> Any: - async with database: + async with base_ormar_config.database: post = await Post(title="Test post").save() await post.categories.create( name="Test category1", @@ -261,7 +241,7 @@ async def test_ordering_by_through_model() -> Any: @pytest.mark.asyncio async def test_update_through_models_from_queryset_on_through() -> Any: - async with database: + async with base_ormar_config.database: post = await Post(title="Test post").save() await post.categories.create( name="Test category1", @@ -290,7 +270,7 @@ async def test_update_through_models_from_queryset_on_through() -> Any: @pytest.mark.asyncio async def test_update_through_model_after_load() -> Any: - async with database: + async with base_ormar_config.database: post = await Post(title="Test post").save() await post.categories.create( name="Test category1", @@ -309,7 +289,7 @@ async def test_update_through_model_after_load() -> Any: @pytest.mark.asyncio async def test_update_through_from_related() -> Any: - async with database: + async with base_ormar_config.database: post = await Post(title="Test post").save() await post.categories.create( name="Test category1", @@ -338,7 +318,7 @@ async def test_update_through_from_related() -> Any: @pytest.mark.asyncio async def test_excluding_fields_on_through_model() -> Any: - async with database: + async with base_ormar_config.database: post = await Post(title="Test post").save() await post.categories.create( name="Test category1", diff --git a/tests/test_relations/test_many_to_many.py b/tests/test_relations/test_many_to_many.py index 120bc1f..e52b560 100644 --- a/tests/test_relations/test_many_to_many.py +++ b/tests/test_relations/test_many_to_many.py @@ -1,24 +1,18 @@ -import asyncio from typing import List, Optional -import databases +import ormar import pytest import pytest_asyncio -import sqlalchemy - -import ormar from ormar.exceptions import ModelPersistenceError, NoMatch, RelationshipInstanceError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config(force_rollback=True) class Author(ormar.Model): - class Meta: - tablename = "authors" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) first_name: str = ormar.String(max_length=80) @@ -26,20 +20,14 @@ class Author(ormar.Model): class Category(ormar.Model): - class Meta: - tablename = "categories" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) class Post(ormar.Model): - class Meta: - tablename = "posts" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="posts") id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -47,26 +35,14 @@ class Post(ormar.Model): author: Optional[Author] = ormar.ForeignKey(Author) -@pytest.fixture(scope="module") -def event_loop(): - loop = asyncio.get_event_loop() - yield loop - loop.close() - - -@pytest_asyncio.fixture(autouse=True, scope="module") -async def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest_asyncio.fixture(scope="function") async def cleanup(): yield - async with database: - PostCategory = Post.Meta.model_fields["categories"].through + async with base_ormar_config.database: + PostCategory = Post.ormar_config.model_fields["categories"].through await PostCategory.objects.delete(each=True) await Post.objects.delete(each=True) await Category.objects.delete(each=True) @@ -75,7 +51,7 @@ async def cleanup(): @pytest.mark.asyncio async def test_not_saved_raises_error(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author(first_name="Guido", last_name="Van Rossum").save() post = await Post.objects.create(title="Hello, M2M", author=guido) news = Category(name="News") @@ -86,7 +62,7 @@ async def test_not_saved_raises_error(cleanup): @pytest.mark.asyncio async def test_not_existing_raises_error(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author(first_name="Guido", last_name="Van Rossum").save() post = await Post.objects.create(title="Hello, M2M", author=guido) @@ -98,7 +74,7 @@ async def test_not_existing_raises_error(cleanup): @pytest.mark.asyncio async def test_assigning_related_objects(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum") post = await Post.objects.create(title="Hello, M2M", author=guido) news = await Category.objects.create(name="News") @@ -121,7 +97,7 @@ async def test_assigning_related_objects(cleanup): @pytest.mark.asyncio async def test_quering_of_the_m2m_models(cleanup): - async with database: + async with base_ormar_config.database: # orm can do this already. guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum") post = await Post.objects.create(title="Hello, M2M", author=guido) @@ -156,7 +132,7 @@ async def test_quering_of_the_m2m_models(cleanup): @pytest.mark.asyncio async def test_removal_of_the_relations(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum") post = await Post.objects.create(title="Hello, M2M", author=guido) news = await Category.objects.create(name="News") @@ -183,7 +159,7 @@ async def test_removal_of_the_relations(cleanup): @pytest.mark.asyncio async def test_selecting_related(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum") post = await Post.objects.create(title="Hello, M2M", author=guido) news = await Category.objects.create(name="News") @@ -210,7 +186,7 @@ async def test_selecting_related(cleanup): @pytest.mark.asyncio async def test_selecting_related_fail_without_saving(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum") post = Post(title="Hello, M2M", author=guido) with pytest.raises(RelationshipInstanceError): @@ -219,7 +195,7 @@ async def test_selecting_related_fail_without_saving(cleanup): @pytest.mark.asyncio async def test_adding_unsaved_related(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum") post = await Post.objects.create(title="Hello, M2M", author=guido) news = Category(name="News") @@ -233,7 +209,7 @@ async def test_adding_unsaved_related(cleanup): @pytest.mark.asyncio async def test_removing_unsaved_related(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum") post = await Post.objects.create(title="Hello, M2M", author=guido) news = Category(name="News") diff --git a/tests/test_relations/test_postgress_select_related_with_limit.py b/tests/test_relations/test_postgress_select_related_with_limit.py index 0369fa4..14ff849 100644 --- a/tests/test_relations/test_postgress_select_related_with_limit.py +++ b/tests/test_relations/test_postgress_select_related_with_limit.py @@ -4,19 +4,13 @@ from datetime import date from enum import Enum from typing import Optional -from pydantic import EmailStr - -import databases -import sqlalchemy -from sqlalchemy import create_engine - import ormar import pytest -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config(force_rollback=True) class PrimaryKeyMixin: @@ -28,26 +22,18 @@ class Level(Enum): STAFF = "1" -class MainMeta(ormar.ModelMeta): - database = database - metadata = metadata - - class User(PrimaryKeyMixin, ormar.Model): """User Model Class to Implement Method for Operations of User Entity""" mobile: str = ormar.String(unique=True, index=True, max_length=10) password: str = ormar.String(max_length=128) - level: str = ormar.String( - max_length=1, choices=list(Level), default=Level.STAFF.value - ) + level: Level = ormar.Enum(default=Level.STAFF, enum_class=Level) email: Optional[str] = ormar.String(max_length=255, nullable=True, default=None) avatar: Optional[str] = ormar.String(max_length=255, nullable=True, default=None) fullname: Optional[str] = ormar.String(max_length=64, nullable=True, default=None) is_active: bool = ormar.Boolean(index=True, nullable=False, default=True) - class Meta(MainMeta): - orders_by = ["-is_active", "-level"] + ormar_config = base_ormar_config.copy(order_by=["-is_active", "-level"]) class Task(PrimaryKeyMixin, ormar.Model): @@ -60,24 +46,20 @@ class Task(PrimaryKeyMixin, ormar.Model): is_halted: bool = ormar.Boolean(index=True, nullable=False, default=True) user: User = ormar.ForeignKey(to=User) - class Meta(MainMeta): - orders_by = ["-end_date", "-start_date"] - constraints = [ + ormar_config = base_ormar_config.copy( + order_by=["-end_date", "-start_date"], + constraints=[ ormar.UniqueColumns("user", "name"), - ] + ], + ) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_selecting_related_with_limit(): - async with database: + async with base_ormar_config.database: user1 = await User(mobile="9928917653", password="pass1").save() user2 = await User(mobile="9928917654", password="pass2").save() await Task(name="one", user=user1).save() diff --git a/tests/test_relations/test_prefetch_related.py b/tests/test_relations/test_prefetch_related.py index 9347c3a..cb7b17d 100644 --- a/tests/test_relations/test_prefetch_related.py +++ b/tests/test_relations/test_prefetch_related.py @@ -1,31 +1,23 @@ from typing import List, Optional -import databases -import pytest -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config(force_rollback=True) class RandomSet(ormar.Model): - class Meta: - tablename = "randoms" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="randoms") id: int = ormar.Integer(name="random_id", primary_key=True) name: str = ormar.String(max_length=100) class Tonation(ormar.Model): - class Meta: - tablename = "tonations" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="tonations") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(name="tonation_name", max_length=100) @@ -33,20 +25,14 @@ class Tonation(ormar.Model): class Division(ormar.Model): - class Meta: - tablename = "divisions" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="divisions") id: int = ormar.Integer(name="division_id", primary_key=True) name: str = ormar.String(max_length=100, nullable=True) class Shop(ormar.Model): - class Meta: - tablename = "shops" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="shops") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=True) @@ -54,17 +40,11 @@ class Shop(ormar.Model): class AlbumShops(ormar.Model): - class Meta: - tablename = "albums_x_shops" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="albums_x_shops") class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="albums") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100, nullable=True) @@ -72,10 +52,7 @@ class Album(ormar.Model): class Track(ormar.Model): - class Meta: - tablename = "tracks" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="tracks") id: int = ormar.Integer(name="track_id", primary_key=True) album: Optional[Album] = ormar.ForeignKey(Album) @@ -85,10 +62,7 @@ class Track(ormar.Model): class Cover(ormar.Model): - class Meta: - tablename = "covers" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="covers") id: int = ormar.Integer(primary_key=True) album: Optional[Album] = ormar.ForeignKey( @@ -98,19 +72,13 @@ class Cover(ormar.Model): artist: str = ormar.String(max_length=200, nullable=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_prefetch_related(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): album = Album(name="Malibu") await album.save() ton1 = await Tonation.objects.create(name="B-mol") @@ -188,8 +156,8 @@ async def test_prefetch_related(): @pytest.mark.asyncio async def test_prefetch_related_with_many_to_many(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): div = await Division.objects.create(name="Div 1") shop1 = await Shop.objects.create(name="Shop 1", division=div) shop2 = await Shop.objects.create(name="Shop 2", division=div) @@ -237,8 +205,8 @@ async def test_prefetch_related_with_many_to_many(): @pytest.mark.asyncio async def test_prefetch_related_empty(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): await Track.objects.create(title="The Bird", position=1) track = await Track.objects.prefetch_related(["album__cover_pictures"]).get( title="The Bird" @@ -249,8 +217,8 @@ async def test_prefetch_related_empty(): @pytest.mark.asyncio async def test_prefetch_related_with_select_related(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): div = await Division.objects.create(name="Div 1") shop1 = await Shop.objects.create(name="Shop 1", division=div) shop2 = await Shop.objects.create(name="Shop 2", division=div) @@ -322,8 +290,8 @@ async def test_prefetch_related_with_select_related(): @pytest.mark.asyncio async def test_prefetch_related_with_select_related_and_fields(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): div = await Division.objects.create(name="Div 1") shop1 = await Shop.objects.create(name="Shop 1", division=div) shop2 = await Shop.objects.create(name="Shop 2", division=div) diff --git a/tests/test_relations/test_prefetch_related_multiple_models_relation.py b/tests/test_relations/test_prefetch_related_multiple_models_relation.py index e0e52f9..2ffbab2 100644 --- a/tests/test_relations/test_prefetch_related_multiple_models_relation.py +++ b/tests/test_relations/test_prefetch_related_multiple_models_relation.py @@ -1,42 +1,29 @@ from typing import List, Optional -import databases -import sqlalchemy -from sqlalchemy import create_engine - import ormar import pytest -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -db = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class User(ormar.Model): - class Meta: - metadata = metadata - database = db - tablename = "test_users" + ormar_config = base_ormar_config.copy(tablename="test_users") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50) class Signup(ormar.Model): - class Meta: - metadata = metadata - database = db - tablename = "test_signup" + ormar_config = base_ormar_config.copy(tablename="test_signup") id: int = ormar.Integer(primary_key=True) class Session(ormar.Model): - class Meta: - metadata = metadata - database = db - tablename = "test_sessions" + ormar_config = base_ormar_config.copy(tablename="test_sessions") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=255, index=True) @@ -50,17 +37,12 @@ class Session(ormar.Model): ) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_add_students(): - async with db: + async with base_ormar_config.database: for user_id in [1, 2, 3, 4, 5]: await User.objects.create(name=f"User {user_id}") diff --git a/tests/test_relations/test_python_style_relations.py b/tests/test_relations/test_python_style_relations.py index 50738ef..bfdb4b1 100644 --- a/tests/test_relations/test_python_style_relations.py +++ b/tests/test_relations/test_python_style_relations.py @@ -1,22 +1,17 @@ from typing import List, Optional -import databases +import ormar import pytest import pytest_asyncio -import sqlalchemy -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class Author(ormar.Model): - class Meta: - tablename = "authors" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) first_name: str = ormar.String(max_length=80) @@ -24,20 +19,14 @@ class Author(ormar.Model): class Category(ormar.Model): - class Meta: - tablename = "categories" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) class Post(ormar.Model): - class Meta: - tablename = "posts" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="posts") id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -45,19 +34,14 @@ class Post(ormar.Model): author: Optional[Author] = ormar.ForeignKey(Author, related_name="author_posts") -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest_asyncio.fixture(scope="function") async def cleanup(): yield - async with database: - PostCategory = Post.Meta.model_fields["categories"].through + async with base_ormar_config.database: + PostCategory = Post.ormar_config.model_fields["categories"].through await PostCategory.objects.delete(each=True) await Post.objects.delete(each=True) await Category.objects.delete(each=True) @@ -66,7 +50,7 @@ async def cleanup(): @pytest.mark.asyncio async def test_selecting_related(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum") post = await Post.objects.create(title="Hello, M2M", author=guido) news = await Category.objects.create(name="News") diff --git a/tests/test_relations/test_relations_default_exception.py b/tests/test_relations/test_relations_default_exception.py index 1573e13..0e18ca1 100644 --- a/tests/test_relations/test_relations_default_exception.py +++ b/tests/test_relations/test_relations_default_exception.py @@ -1,23 +1,18 @@ # type: ignore from typing import List, Optional -import databases -import pytest -import sqlalchemy - import ormar +import pytest from ormar.exceptions import ModelDefinitionError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Author(ormar.Model): - class Meta: - tablename = "authors" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="authors") id: int = ormar.Integer(primary_key=True) first_name: str = ormar.String(max_length=80) @@ -25,23 +20,20 @@ class Author(ormar.Model): class Category(ormar.Model): - class Meta: - tablename = "categories" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) +create_test_database = init_tests(base_ormar_config) + + def test_fk_error(): with pytest.raises(ModelDefinitionError): class Post(ormar.Model): - class Meta: - tablename = "posts" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="posts") id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -53,10 +45,7 @@ def test_m2m_error(): with pytest.raises(ModelDefinitionError): class Post(ormar.Model): - class Meta: - tablename = "posts" - database = database - metadata = metadata + ormar_config = base_ormar_config.copy(tablename="posts") id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) diff --git a/tests/test_relations/test_replacing_models_with_copy.py b/tests/test_relations/test_replacing_models_with_copy.py new file mode 100644 index 0000000..cf70e33 --- /dev/null +++ b/tests/test_relations/test_replacing_models_with_copy.py @@ -0,0 +1,43 @@ +from typing import Any, Optional, Tuple, Union + +import ormar +import pytest + +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() + + +class Album(ormar.Model): + ormar_config = base_ormar_config.copy(tablename="albums") + + id: int = ormar.Integer(primary_key=True) + name: str = ormar.String(max_length=100) + is_best_seller: bool = ormar.Boolean(default=False) + properties: Tuple[str, Any] + score: Union[str, int] + + +class Track(ormar.Model): + ormar_config = base_ormar_config.copy(tablename="tracks") + + id: int = ormar.Integer(primary_key=True) + album: Optional[Album] = ormar.ForeignKey(Album) + title: str = ormar.String(max_length=100) + position: int = ormar.Integer() + play_count: int = ormar.Integer(nullable=True, default=0) + is_disabled: bool = ormar.Boolean(default=False) + properties: Tuple[str, Any] + + +create_test_database = init_tests(base_ormar_config) + + +@pytest.mark.asyncio +async def test_model_is_replaced_by_a_copy(): + assert Album.model_fields["tracks"].annotation.__args__[1] != Track + assert ( + Album.model_fields["tracks"].annotation.__args__[1].model_fields.keys() + == Track.model_fields.keys() + ) diff --git a/tests/test_relations/test_saving_related.py b/tests/test_relations/test_saving_related.py index 388e290..24668b9 100644 --- a/tests/test_relations/test_saving_related.py +++ b/tests/test_relations/test_saving_related.py @@ -1,23 +1,17 @@ from typing import Union -import databases -import pytest -import sqlalchemy as sa -from sqlalchemy import create_engine - import ormar +import pytest from ormar.exceptions import ModelPersistenceError -from tests.settings import DATABASE_URL -metadata = sa.MetaData() -db = databases.Database(DATABASE_URL) +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class Category(ormar.Model): - class Meta: - tablename = "categories" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50, unique=True, index=True) @@ -25,10 +19,7 @@ class Category(ormar.Model): class Workshop(ormar.Model): - class Meta: - tablename = "workshops" - metadata = metadata - database = db + ormar_config = base_ormar_config.copy(tablename="workshops") id: int = ormar.Integer(primary_key=True) topic: str = ormar.String(max_length=255, index=True) @@ -37,18 +28,13 @@ class Workshop(ormar.Model): ) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_model_relationship(): - async with db: - async with db.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): cat = await Category(name="Foo", code=123).save() ws = await Workshop(topic="Topic 1", category=cat).save() @@ -66,8 +52,8 @@ async def test_model_relationship(): @pytest.mark.asyncio async def test_model_relationship_with_not_saved(): - async with db: - async with db.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): cat = Category(name="Foo", code=123) with pytest.raises(ModelPersistenceError): await Workshop(topic="Topic 1", category=cat).save() diff --git a/tests/test_relations/test_select_related_with_limit.py b/tests/test_relations/test_select_related_with_limit.py index 47aa808..2bbe4ee 100644 --- a/tests/test_relations/test_select_related_with_limit.py +++ b/tests/test_relations/test_select_related_with_limit.py @@ -1,42 +1,29 @@ from typing import List, Optional -import databases -import sqlalchemy -from sqlalchemy import create_engine - import ormar import pytest -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -db = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +base_ormar_config = create_config() class Keyword(ormar.Model): - class Meta: - metadata = metadata - database = db - tablename = "keywords" + ormar_config = base_ormar_config.copy(tablename="keywords") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50) class KeywordPrimaryModel(ormar.Model): - class Meta: - metadata = metadata - database = db - tablename = "primary_models_keywords" + ormar_config = base_ormar_config.copy(tablename="primary_models_keywords") id: int = ormar.Integer(primary_key=True) class PrimaryModel(ormar.Model): - class Meta: - metadata = metadata - database = db - tablename = "primary_models" + ormar_config = base_ormar_config.copy(tablename="primary_models") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=255, index=True) @@ -48,10 +35,7 @@ class PrimaryModel(ormar.Model): class SecondaryModel(ormar.Model): - class Meta: - metadata = metadata - database = db - tablename = "secondary_models" + ormar_config = base_ormar_config.copy(tablename="secondary_models") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -62,7 +46,7 @@ class SecondaryModel(ormar.Model): @pytest.mark.asyncio async def test_create_primary_models(): - async with db: + async with base_ormar_config.database: for name, some_text, some_other_text in [ ("Primary 1", "Some text 1", "Some other text 1"), ("Primary 2", "Some text 2", "Some other text 2"), @@ -150,9 +134,4 @@ async def test_create_primary_models(): assert len(models5[2].keywords) == 0 -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) diff --git a/tests/test_relations/test_select_related_with_m2m_and_pk_name_set.py b/tests/test_relations/test_select_related_with_m2m_and_pk_name_set.py index 85cdea6..5ebaef7 100644 --- a/tests/test_relations/test_select_related_with_m2m_and_pk_name_set.py +++ b/tests/test_relations/test_select_related_with_m2m_and_pk_name_set.py @@ -2,27 +2,19 @@ from datetime import date from typing import List, Optional, Union -import databases +import ormar import pytest import sqlalchemy -from sqlalchemy import create_engine - -import ormar from ormar import ModelDefinitionError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config - -class MainMeta(ormar.ModelMeta): - metadata = metadata - database = database +base_ormar_config = create_config() class Role(ormar.Model): - class Meta(MainMeta): - pass + ormar_config = base_ormar_config.copy() name: str = ormar.String(primary_key=True, max_length=1000) order: int = ormar.Integer(default=0, name="sort_order") @@ -30,20 +22,17 @@ class Role(ormar.Model): class Company(ormar.Model): - class Meta(MainMeta): - pass + ormar_config = base_ormar_config.copy() name: str = ormar.String(primary_key=True, max_length=1000) class UserRoleCompany(ormar.Model): - class Meta(MainMeta): - pass + ormar_config = base_ormar_config.copy() class User(ormar.Model): - class Meta(MainMeta): - pass + ormar_config = base_ormar_config.copy() registrationnumber: str = ormar.String(primary_key=True, max_length=1000) company: Company = ormar.ForeignKey(Company) @@ -56,20 +45,14 @@ class User(ormar.Model): lastupdate: date = ormar.DateTime(server_default=sqlalchemy.func.now()) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) def test_wrong_model(): with pytest.raises(ModelDefinitionError): class User(ormar.Model): - class Meta(MainMeta): - pass + ormar_config = base_ormar_config.copy() registrationnumber: str = ormar.Text(primary_key=True) company: Company = ormar.ForeignKey(Company) @@ -78,7 +61,7 @@ def test_wrong_model(): @pytest.mark.asyncio async def test_create_primary_models(): - async with database: + async with base_ormar_config.database: await Role.objects.create( name="user", order=0, description="no administration right" ) diff --git a/tests/test_relations/test_selecting_proper_table_prefix.py b/tests/test_relations/test_selecting_proper_table_prefix.py index c4e93ec..9dfe3cd 100644 --- a/tests/test_relations/test_selecting_proper_table_prefix.py +++ b/tests/test_relations/test_selecting_proper_table_prefix.py @@ -1,41 +1,29 @@ from typing import List, Optional -import databases -import pytest -import sqlalchemy -from sqlalchemy import create_engine - import ormar -from tests.settings import DATABASE_URL +import pytest -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class User(ormar.Model): - class Meta: - metadata = metadata - database = database - tablename = "test_users" + ormar_config = base_ormar_config.copy(tablename="test_users") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=50) class Signup(ormar.Model): - class Meta: - metadata = metadata - database = database - tablename = "test_signup" + ormar_config = base_ormar_config.copy(tablename="test_signup") id: int = ormar.Integer(primary_key=True) class Session(ormar.Model): - class Meta: - metadata = metadata - database = database - tablename = "test_sessions" + ormar_config = base_ormar_config.copy(tablename="test_sessions") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=255, index=True) @@ -44,17 +32,12 @@ class Session(ormar.Model): students: Optional[List[User]] = ormar.ManyToMany(User, through=Signup) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest.mark.asyncio async def test_list_sessions_for_user(): - async with database: + async with base_ormar_config.database: for user_id in [1, 2, 3, 4, 5]: await User.objects.create(name=f"User {user_id}") diff --git a/tests/test_relations/test_skipping_reverse.py b/tests/test_relations/test_skipping_reverse.py index 72a4861..947741c 100644 --- a/tests/test_relations/test_skipping_reverse.py +++ b/tests/test_relations/test_skipping_reverse.py @@ -1,25 +1,17 @@ from typing import List, Optional -import databases +import ormar import pytest import pytest_asyncio -import sqlalchemy -import ormar -from tests.settings import DATABASE_URL +from tests.lifespan import init_tests +from tests.settings import create_config -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() - - -class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata +base_ormar_config = create_config() class Author(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) first_name: str = ormar.String(max_length=80) @@ -27,16 +19,14 @@ class Author(ormar.Model): class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=40) class Post(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) @@ -44,19 +34,14 @@ class Post(ormar.Model): author: Optional[Author] = ormar.ForeignKey(Author, skip_reverse=True) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest_asyncio.fixture(scope="function") async def cleanup(): yield - async with database: - PostCategory = Post.Meta.model_fields["categories"].through + async with base_ormar_config.database: + PostCategory = Post.ormar_config.model_fields["categories"].through await PostCategory.objects.delete(each=True) await Post.objects.delete(each=True) await Category.objects.delete(each=True) @@ -83,7 +68,7 @@ def test_model_definition(): @pytest.mark.asyncio async def test_assigning_related_objects(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum") post = await Post.objects.create(title="Hello, M2M", author=guido) news = await Category.objects.create(name="News") @@ -111,7 +96,7 @@ async def test_assigning_related_objects(cleanup): @pytest.mark.asyncio async def test_quering_of_related_model_works_but_no_result(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum") post = await Post.objects.create(title="Hello, M2M", author=guido) news = await Category.objects.create(name="News") @@ -121,7 +106,7 @@ async def test_quering_of_related_model_works_but_no_result(cleanup): post_categories = await post.categories.all() assert len(post_categories) == 1 - assert "posts" not in post.dict().get("categories", [])[0] + assert "posts" not in post.model_dump().get("categories", [])[0] assert news == await post.categories.get(name="News") @@ -135,7 +120,7 @@ async def test_quering_of_related_model_works_but_no_result(cleanup): .get() ) assert category == news - assert "posts" not in category.dict() + assert "posts" not in category.model_dump() # relation not in json category2 = ( @@ -144,14 +129,14 @@ async def test_quering_of_related_model_works_but_no_result(cleanup): .get() ) assert category2 == news - assert "posts" not in category2.json() + assert "posts" not in category2.model_dump_json() - assert "posts" not in Category.schema().get("properties") + assert "posts" not in Category.model_json_schema().get("properties") @pytest.mark.asyncio async def test_removal_of_the_relations(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum") post = await Post.objects.create(title="Hello, M2M", author=guido) news = await Category.objects.create(name="News") @@ -176,7 +161,7 @@ async def test_removal_of_the_relations(cleanup): @pytest.mark.asyncio async def test_selecting_related(cleanup): - async with database: + async with base_ormar_config.database: guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum") guido2 = await Author.objects.create( first_name="Guido2", last_name="Van Rossum" diff --git a/tests/test_relations/test_through_relations_fail.py b/tests/test_relations/test_through_relations_fail.py index 472a8a1..e6fcfef 100644 --- a/tests/test_relations/test_through_relations_fail.py +++ b/tests/test_relations/test_through_relations_fail.py @@ -1,39 +1,30 @@ # type: ignore -import databases -import pytest -import sqlalchemy - import ormar +import pytest from ormar import ModelDefinitionError -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() def test_through_with_relation_fails(): - class BaseMeta(ormar.ModelMeta): - database = database - metadata = metadata - class Category(ormar.Model): - class Meta(BaseMeta): - tablename = "categories" + ormar_config = base_ormar_config.copy(tablename="categories") id = ormar.Integer(primary_key=True) name = ormar.String(max_length=40) class Blog(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) class PostCategory(ormar.Model): - class Meta(BaseMeta): - tablename = "posts_x_categories" + ormar_config = base_ormar_config.copy(tablename="posts_x_categories") id: int = ormar.Integer(primary_key=True) sort_order: int = ormar.Integer(nullable=True) @@ -43,9 +34,11 @@ def test_through_with_relation_fails(): with pytest.raises(ModelDefinitionError): class Post(ormar.Model): - class Meta(BaseMeta): - pass + ormar_config = base_ormar_config.copy() id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=200) categories = ormar.ManyToMany(Category, through=PostCategory) + + +create_test_database = init_tests(base_ormar_config) diff --git a/tests/test_relations/test_weakref_checking.py b/tests/test_relations/test_weakref_checking.py index dded7fc..2af7620 100644 --- a/tests/test_relations/test_weakref_checking.py +++ b/tests/test_relations/test_weakref_checking.py @@ -1,32 +1,20 @@ -from typing import Optional, Type - -import databases -import pytest -import pytest_asyncio -import sqlalchemy - import ormar -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL) -metadata = sqlalchemy.MetaData() +from tests.settings import create_config + +base_ormar_config = create_config() +from tests.lifespan import init_tests class Band(ormar.Model): - class Meta: - tablename = "bands" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="bands") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Artist(ormar.Model): - class Meta: - tablename = "artists" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="artists") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -34,6 +22,9 @@ class Artist(ormar.Model): band: Band = ormar.ForeignKey(Band) +create_test_database = init_tests(base_ormar_config) + + def test_weakref_init(): band = Band(name="Band") artist1 = Artist(name="Artist 1", band=band) @@ -49,4 +40,4 @@ def test_weakref_init(): band.artists # Force it to clean assert len(band.artists) == 1 - assert band.artists[0].name == "Artist 2" + assert band.artists[0].name == artist2.name diff --git a/tests/test_signals/test_signals.py b/tests/test_signals/test_signals.py index 47666fa..7a9f7e0 100644 --- a/tests/test_signals/test_signals.py +++ b/tests/test_signals/test_signals.py @@ -1,12 +1,9 @@ from typing import Optional -import databases +import ormar import pydantic import pytest import pytest_asyncio -import sqlalchemy - -import ormar from ormar import ( post_bulk_update, post_delete, @@ -16,19 +13,17 @@ from ormar import ( pre_save, pre_update, ) -from ormar.signals import SignalEmitter from ormar.exceptions import SignalDefinitionError -from tests.settings import DATABASE_URL +from ormar.signals import SignalEmitter -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class AuditLog(ormar.Model): - class Meta: - tablename = "audits" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="audits") id: int = ormar.Integer(primary_key=True) event_type: str = ormar.String(max_length=100) @@ -36,20 +31,14 @@ class AuditLog(ormar.Model): class Cover(ormar.Model): - class Meta: - tablename = "covers" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="covers") id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=100) class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="albums") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -58,19 +47,13 @@ class Album(ormar.Model): cover: Optional[Cover] = ormar.ForeignKey(Cover) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest_asyncio.fixture(scope="function") async def cleanup(): yield - async with database: + async with base_ormar_config.database: await AuditLog.objects.delete(each=True) @@ -95,49 +78,49 @@ def test_invalid_signal(): @pytest.mark.asyncio async def test_signal_functions(cleanup): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): @pre_save(Album) async def before_save(sender, instance, **kwargs): await AuditLog( event_type=f"PRE_SAVE_{sender.get_name()}", - event_log=instance.json(), + event_log=instance.model_dump_json(), ).save() @post_save(Album) async def after_save(sender, instance, **kwargs): await AuditLog( event_type=f"POST_SAVE_{sender.get_name()}", - event_log=instance.json(), + event_log=instance.model_dump_json(), ).save() @pre_update(Album) async def before_update(sender, instance, **kwargs): await AuditLog( event_type=f"PRE_UPDATE_{sender.get_name()}", - event_log=instance.json(), + event_log=instance.model_dump_json(), ).save() @post_update(Album) async def after_update(sender, instance, **kwargs): await AuditLog( event_type=f"POST_UPDATE_{sender.get_name()}", - event_log=instance.json(), + event_log=instance.model_dump_json(), ).save() @pre_delete(Album) async def before_delete(sender, instance, **kwargs): await AuditLog( event_type=f"PRE_DELETE_{sender.get_name()}", - event_log=instance.json(), + event_log=instance.model_dump_json(), ).save() @post_delete(Album) async def after_delete(sender, instance, **kwargs): await AuditLog( event_type=f"POST_DELETE_{sender.get_name()}", - event_log=instance.json(), + event_log=instance.model_dump_json(), ).save() @post_bulk_update(Album) @@ -145,7 +128,7 @@ async def test_signal_functions(cleanup): for it in instances: await AuditLog( event_type=f"BULK_POST_UPDATE_{sender.get_name()}", - event_log=it.json(), + event_log=it.model_dump_json(), ).save() album = await Album.objects.create(name="Venice") @@ -218,21 +201,21 @@ async def test_signal_functions(cleanup): @pytest.mark.asyncio async def test_multiple_signals(cleanup): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): @pre_save(Album) async def before_save(sender, instance, **kwargs): await AuditLog( event_type=f"PRE_SAVE_{sender.get_name()}", - event_log=instance.json(), + event_log=instance.model_dump_json(), ).save() @pre_save(Album) async def before_save2(sender, instance, **kwargs): await AuditLog( event_type=f"PRE_SAVE_{sender.get_name()}", - event_log=instance.json(), + event_log=instance.model_dump_json(), ).save() album = await Album.objects.create(name="Miami") @@ -249,8 +232,8 @@ async def test_multiple_signals(cleanup): @pytest.mark.asyncio async def test_static_methods_as_signals(cleanup): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): class AlbumAuditor: event_type = "ALBUM_INSTANCE" @@ -260,7 +243,7 @@ async def test_static_methods_as_signals(cleanup): async def before_save(sender, instance, **kwargs): await AuditLog( event_type=f"{AlbumAuditor.event_type}_SAVE", - event_log=instance.json(), + event_log=instance.model_dump_json(), ).save() album = await Album.objects.create(name="Colorado") @@ -274,8 +257,8 @@ async def test_static_methods_as_signals(cleanup): @pytest.mark.asyncio async def test_methods_as_signals(cleanup): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): class AlbumAuditor: def __init__(self): @@ -283,7 +266,8 @@ async def test_methods_as_signals(cleanup): async def before_save(self, sender, instance, **kwargs): await AuditLog( - event_type=f"{self.event_type}_SAVE", event_log=instance.json() + event_type=f"{self.event_type}_SAVE", + event_log=instance.model_dump_json(), ).save() auditor = AlbumAuditor() @@ -300,14 +284,14 @@ async def test_methods_as_signals(cleanup): @pytest.mark.asyncio async def test_multiple_senders_signal(cleanup): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): @pre_save([Album, Cover]) async def before_save(sender, instance, **kwargs): await AuditLog( event_type=f"PRE_SAVE_{sender.get_name()}", - event_log=instance.json(), + event_log=instance.model_dump_json(), ).save() cover = await Cover(title="Blue").save() @@ -318,7 +302,7 @@ async def test_multiple_senders_signal(cleanup): assert audits[0].event_type == "PRE_SAVE_cover" assert audits[0].event_log.get("title") == cover.title assert audits[1].event_type == "PRE_SAVE_album" - assert audits[1].event_log.get("cover") == album.cover.dict( + assert audits[1].event_log.get("cover") == album.cover.model_dump( exclude={"albums"} ) @@ -328,8 +312,8 @@ async def test_multiple_senders_signal(cleanup): @pytest.mark.asyncio async def test_modifing_the_instance(cleanup): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): @pre_update(Album) async def before_update(sender, instance, **kwargs): @@ -354,8 +338,8 @@ async def test_modifing_the_instance(cleanup): @pytest.mark.asyncio async def test_custom_signal(cleanup): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): async def after_update(sender, instance, **kwargs): if instance.play_count > 50 and not instance.is_best_seller: @@ -364,7 +348,7 @@ async def test_custom_signal(cleanup): instance.is_best_seller = False await instance.update() - Album.Meta.signals.custom.connect(after_update) + Album.ormar_config.signals.custom.connect(after_update) # here album.play_count ans is_best_seller get default values album = await Album.objects.create(name="Venice") @@ -379,13 +363,13 @@ async def test_custom_signal(cleanup): album.play_count = 60 await album.update() assert not album.is_best_seller - await Album.Meta.signals.custom.send(sender=Album, instance=album) + await Album.ormar_config.signals.custom.send(sender=Album, instance=album) assert album.is_best_seller album.play_count = 30 await album.update() assert album.is_best_seller - await Album.Meta.signals.custom.send(sender=Album, instance=album) + await Album.ormar_config.signals.custom.send(sender=Album, instance=album) assert not album.is_best_seller - Album.Meta.signals.custom.disconnect(after_update) + Album.ormar_config.signals.custom.disconnect(after_update) diff --git a/tests/test_signals/test_signals_for_relations.py b/tests/test_signals/test_signals_for_relations.py index 8d7599f..2ac2e23 100644 --- a/tests/test_signals/test_signals_for_relations.py +++ b/tests/test_signals/test_signals_for_relations.py @@ -1,29 +1,24 @@ from typing import Optional -import databases +import ormar +import pydantic import pytest import pytest_asyncio -import sqlalchemy - -import ormar from ormar import ( post_relation_add, post_relation_remove, pre_relation_add, pre_relation_remove, ) -import pydantic -from tests.settings import DATABASE_URL -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() class AuditLog(ormar.Model): - class Meta: - tablename = "audits" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="audits") id: int = ormar.Integer(primary_key=True) event_type: str = ormar.String(max_length=100) @@ -31,30 +26,21 @@ class AuditLog(ormar.Model): class Cover(ormar.Model): - class Meta: - tablename = "covers" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="covers") id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=100) class Artist(ormar.Model): - class Meta: - tablename = "artists" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="artists") id: int = ormar.Integer(name="artist_id", primary_key=True) name: str = ormar.String(name="fname", max_length=100) class Album(ormar.Model): - class Meta: - tablename = "albums" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="albums") id: int = ormar.Integer(primary_key=True) title: str = ormar.String(max_length=100) @@ -62,26 +48,20 @@ class Album(ormar.Model): artists = ormar.ManyToMany(Artist) -@pytest.fixture(autouse=True, scope="module") -def create_test_database(): - engine = sqlalchemy.create_engine(DATABASE_URL) - metadata.drop_all(engine) - metadata.create_all(engine) - yield - metadata.drop_all(engine) +create_test_database = init_tests(base_ormar_config) @pytest_asyncio.fixture(autouse=True, scope="function") async def cleanup(): yield - async with database: + async with base_ormar_config.database: await AuditLog.objects.delete(each=True) @pytest.mark.asyncio async def test_relation_signal_functions(): - async with database: - async with database.transaction(force_rollback=True): + async with base_ormar_config.database: + async with base_ormar_config.database.transaction(force_rollback=True): @pre_relation_add([Album, Cover, Artist]) async def before_relation_add( diff --git a/tests/test_types.py b/tests/test_types.py index b5122a8..f9bc3c6 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,34 +1,30 @@ -from typing import Any, Optional, TYPE_CHECKING - import databases +import ormar import pytest import sqlalchemy +from ormar.models.ormar_config import OrmarConfig -import ormar -from ormar.relations.querysetproxy import QuerysetProxy from tests.settings import DATABASE_URL database = databases.Database(DATABASE_URL) metadata = sqlalchemy.MetaData() -class BaseMeta(ormar.ModelMeta): - metadata = metadata - database = database - - class Publisher(ormar.Model): - class Meta(BaseMeta): - tablename = "publishers" + ormar_config = OrmarConfig( + metadata=metadata, + database=database, + tablename="publishers", + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) class Author(ormar.Model): - class Meta(BaseMeta): - tablename = "authors" - order_by = ["-name"] + ormar_config = OrmarConfig( + metadata=metadata, database=database, tablename="authors", order_by=["-name"] + ) id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -36,9 +32,12 @@ class Author(ormar.Model): class Book(ormar.Model): - class Meta(BaseMeta): - tablename = "books" - order_by = ["year", "-ranking"] + ormar_config = OrmarConfig( + metadata=metadata, + database=database, + tablename="books", + order_by=["year", "-ranking"], + ) id: int = ormar.Integer(primary_key=True) author = ormar.ForeignKey(Author) @@ -63,12 +62,10 @@ def assert_type(book: Book): @pytest.mark.asyncio async def test_types() -> None: async with database: - query = Book.objects publisher = await Publisher(name="Test publisher").save() author = await Author.objects.create(name="Test Author") await author.publishers.add(publisher) - author2 = await Author.objects.select_related("publishers").get() - publishers = author2.publishers + await Author.objects.select_related("publishers").get() publisher2 = await Publisher.objects.select_related("authors").get() authors = publisher2.authors assert authors[0] == author @@ -78,8 +75,8 @@ async def test_types() -> None: # reveal_type(author) # iter of relation proxy book = await Book.objects.create(title="Test", author=author) book2 = await Book.objects.select_related("author").get() - books = await Book.objects.select_related("author").all() - author_books = await author.books.all() + await Book.objects.select_related("author").all() + await author.books.all() assert book.author.name == "Test Author" assert book2.author.name == "Test Author" # if TYPE_CHECKING: # pragma: no cover diff --git a/tests/test_utils/test_queryset_utils.py b/tests/test_utils/test_queryset_utils.py index 02ca172..8c6320b 100644 --- a/tests/test_utils/test_queryset_utils.py +++ b/tests/test_utils/test_queryset_utils.py @@ -1,15 +1,16 @@ -import databases -import sqlalchemy - import ormar from ormar.queryset.queries.prefetch_query import sort_models from ormar.queryset.utils import ( subtract_dict, translate_list_to_dict, - update_dict_from_list, update, + update_dict_from_list, ) -from tests.settings import DATABASE_URL + +from tests.lifespan import init_tests +from tests.settings import create_config + +base_ormar_config = create_config() def test_list_to_dict_translation(): @@ -173,15 +174,8 @@ def test_subtracting_with_set_and_dict(): assert test == {"translation": {"translations": {"language": Ellipsis}}} -database = databases.Database(DATABASE_URL, force_rollback=True) -metadata = sqlalchemy.MetaData() - - class SortModel(ormar.Model): - class Meta: - tablename = "sorts" - metadata = metadata - database = database + ormar_config = base_ormar_config.copy(tablename="sorts") id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) @@ -211,3 +205,6 @@ def test_sorting_models(): orders_by = {"sort_order": "asc", "none": ..., "id": "asc", "uu": 2, "aa": None} models = sort_models(models, orders_by) assert [model.id for model in models] == [1, 4, 2, 3, 5, 6] + + +create_test_database = init_tests(base_ormar_config)