pax_global_header 0000666 0000000 0000000 00000000064 15211567312 0014515 g ustar 00root root 0000000 0000000 52 comment=a7e7fde5d1c4e9586bdf4765f2f89bf91f762b06
ormar-orm-ormar-a7e7fde/ 0000775 0000000 0000000 00000000000 15211567312 0015332 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/.codeclimate.yml 0000664 0000000 0000000 00000000545 15211567312 0020410 0 ustar 00root root 0000000 0000000 version: "2"
checks:
method-complexity:
config:
threshold: 8
argument-count:
config:
threshold: 6
method-count:
config:
threshold: 25
method-length:
config:
threshold: 35
file-lines:
config:
threshold: 500
engines:
bandit:
enabled: true
checks:
assert_used:
enabled: false
ormar-orm-ormar-a7e7fde/.codecov.yml 0000664 0000000 0000000 00000000741 15211567312 0017557 0 ustar 00root root 0000000 0000000 coverage:
precision: 2
round: down
range: "80...100"
status:
project: yes
patch: yes
changes: yes
comment:
layout: "reach, diff, flags, files"
behavior: default
require_changes: false # if true: only post the comment if coverage changes
require_base: no # [yes :: must have a base report to post]
require_head: yes # [yes :: must have a head report to post]
branches: # branch names that can post comment
- "master" ormar-orm-ormar-a7e7fde/.coveragerc 0000664 0000000 0000000 00000000231 15211567312 0017447 0 ustar 00root root 0000000 0000000 [run]
source = ormar, tests
omit = ./tests/test.db, *py.typed*
data_file = .coverage
concurrency = greenlet
[report]
omit = ./tests/test.db, *py.typed*
ormar-orm-ormar-a7e7fde/.github/ 0000775 0000000 0000000 00000000000 15211567312 0016672 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/.github/FUNDING.yml 0000664 0000000 0000000 00000000020 15211567312 0020477 0 ustar 00root root 0000000 0000000 github: collerek ormar-orm-ormar-a7e7fde/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15211567312 0021055 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000001505 15211567312 0023550 0 ustar 00root root 0000000 0000000 ---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
(Note: this should be a complete and concise piece of code that allows reproduction of an issue)
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Versions (please complete the following information):**
- Database backend used (mysql/sqlite/postgress)
- Python version
- `ormar` version
- `pydantic` version
- if applicable `fastapi` version
**Additional context**
Add any other context about the problem here.
ormar-orm-ormar-a7e7fde/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000000631 15211567312 0023045 0 ustar 00root root 0000000 0000000 contact_links:
- name: I have a question ❓
url: https://github.com/collerek/ormar/discussions
about: If you have any question about the usage of ormar, please open a discussion first.
- name: I want a new feature 🆕
url: https://github.com/collerek/ormar/discussions
about: If you would like to request or make a change/enhancement that is not trivial, please open a discussion first.
ormar-orm-ormar-a7e7fde/.github/ISSUE_TEMPLATE/feature_request.md 0000664 0000000 0000000 00000001134 15211567312 0024601 0 ustar 00root root 0000000 0000000 ---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
ormar-orm-ormar-a7e7fde/.github/dependabot.yml 0000664 0000000 0000000 00000000533 15211567312 0021523 0 ustar 00root root 0000000 0000000 # Basic set up
# https://help.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#package-ecosystem
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: daily
ormar-orm-ormar-a7e7fde/.github/workflows/ 0000775 0000000 0000000 00000000000 15211567312 0020727 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/.github/workflows/auto-merge-dependabot.yml 0000664 0000000 0000000 00000003235 15211567312 0025625 0 ustar 00root root 0000000 0000000 # Based on https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request
name: Dependabot auto-approve and auto-merge
on: pull_request_target
permissions:
pull-requests: write
contents: write
jobs:
autoapprove:
name: Auto Approve a PR by dependabot
runs-on: ubuntu-latest
steps:
- name: Auto approve
uses: hmarr/auto-approve-action@v4.0.0
if: github.actor == 'dependabot[bot]'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v3.1.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs
# Automatically merge semver-patch and semver-minor PRs
# or black dependency upgrades
if: "${{ steps.metadata.outputs.update-type ==
'version-update:semver-minor' ||
steps.metadata.outputs.update-type ==
'version-update:semver-patch' ||
steps.metadata.outputs.dependency-names ==
'black' }}"
# https://cli.github.com/manual/gh_pr_merge
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
ormar-orm-ormar-a7e7fde/.github/workflows/benchmark.yml 0000664 0000000 0000000 00000001765 15211567312 0023415 0 ustar 00root root 0000000 0000000 name: codspeed-benchmarks
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:
jobs:
benchmarks:
if: github.actor != 'dependabot[bot]'
permissions:
contents: read
id-token: write
runs-on: codspeed-macro
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Install Poetry
uses: snok/install-poetry@v1.4
with:
version: 2.3.2
virtualenvs-create: false
- name: Poetry details
run: |
poetry --version
poetry config --list
- name: Install dependencies
run: poetry install --extras "all"
- name: Run benchmarks
uses: CodSpeedHQ/action@v4
with:
mode: walltime
run: poetry run pytest benchmarks/ --codspeed
ormar-orm-ormar-a7e7fde/.github/workflows/deploy-docs.yml 0000664 0000000 0000000 00000002402 15211567312 0023672 0 ustar 00root root 0000000 0000000 name: Build Documentation using MkDocs
on:
push:
# Pattern matched against refs/tags
tags:
- '**'
workflow_dispatch:
tags:
- '**'
jobs:
build:
name: Build and Deploy Documentation
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Master
uses: actions/checkout@v6
- name: Set up Python 3.9
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install Poetry
uses: snok/install-poetry@v1.4
with:
version: 2.3.2
virtualenvs-create: false
- name: Install dependencies
run: |
poetry install --extras "all"
env:
POETRY_VIRTUALENVS_CREATE: false
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Test
run: |
echo $RELEASE_VERSION
echo ${{ env.RELEASE_VERSION }}
- name: Configure Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Deploy
run: |
mike deploy --push --update-aliases ${{ env.RELEASE_VERSION }} latest
ormar-orm-ormar-a7e7fde/.github/workflows/lint.yml 0000664 0000000 0000000 00000001620 15211567312 0022417 0 ustar 00root root 0000000 0000000 # 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:
pull_request:
branches: [ master ]
jobs:
lint:
name: "Python ${{ matrix.python-version }}"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.11
- name: Install Poetry
uses: snok/install-poetry@v1.4
with:
version: 2.3.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
ormar-orm-ormar-a7e7fde/.github/workflows/python-publish.yml 0000664 0000000 0000000 00000001626 15211567312 0024444 0 ustar 00root root 0000000 0000000 # This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: Upload Python Package
on:
release:
types: [created]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install Poetry
uses: snok/install-poetry@v1.4
with:
version: 2.3.2
virtualenvs-create: true
virtualenvs-in-project: true
- name: Build and publish
run: |
poetry build -vvv
poetry publish -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }}
ormar-orm-ormar-a7e7fde/.github/workflows/test-package.yml 0000664 0000000 0000000 00000007432 15211567312 0024030 0 ustar 00root root 0000000 0000000 # 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
on:
pull_request:
branches: [ master ]
jobs:
tests:
name: "Python ${{ matrix.python-version }}"
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", 3.11, 3.12, 3.13]
fail-fast: false
services:
mysql:
image: mysql:5.7
env:
MYSQL_USER: username
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testsuite
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
postgres:
image: postgres:10.8
env:
POSTGRES_USER: username
POSTGRES_PASSWORD: password
POSTGRES_DB: testsuite
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 --name postgres
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: false
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@v1.4
with:
version: 2.3.2
virtualenvs-create: false
- name: Poetry details
run: |
poetry --version
poetry config --list
- name: Install dependencies
run: poetry install --extras "all"
- name: Grant MySQL global privileges
# The schema-per-model tests create extra databases for cross-schema
# FKs/M2M; the default GH Actions service container only grants the
# user access to MYSQL_DATABASE. Mirror what scripts/mysql-init/ does
# for local docker-compose runs.
run: mysql -h 127.0.0.1 -uroot -ppassword -e "GRANT ALL PRIVILEGES ON *.* TO 'username'@'%'; FLUSH PRIVILEGES;"
- 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
- name: Upload coverage
uses: codecov/codecov-action@v6.0.1
with:
name: codecov-umbrella
token: ${{ secrets.CODCOV_TOKEN }}
verbose: true
- name: Test & publish code coverage
uses: paambaati/codeclimate-action@v9.0.0
if: github.event.pull_request.head.repo.full_name == 'collerek/ormar'
env:
CC_TEST_REPORTER_ID: ${{ secrets.CC_COVERAGE_TOKEN }}
ormar-orm-ormar-a7e7fde/.github/workflows/test_docs.yml 0000664 0000000 0000000 00000001531 15211567312 0023441 0 ustar 00root root 0000000 0000000 # 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:
pull_request:
branches: [ master ]
jobs:
tests_docs:
name: "Python ${{ matrix.python-version }}"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.11
- name: Install Poetry
uses: snok/install-poetry@v1.4
with:
version: 2.3.2
virtualenvs-create: false
- name: Install dependencies
run: |
poetry install --extras "all"
env:
POETRY_VIRTUALENVS_CREATE: false
- name: Test docs
run: bash scripts/test_docs.sh
ormar-orm-ormar-a7e7fde/.github/workflows/type-check.yml 0000664 0000000 0000000 00000001572 15211567312 0023513 0 ustar 00root root 0000000 0000000 # 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:
pull_request:
branches: [ master ]
jobs:
lint:
name: "Python ${{ matrix.python-version }}"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.11
- name: Install Poetry
uses: snok/install-poetry@v1.4
with:
version: 2.3.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
ormar-orm-ormar-a7e7fde/.gitignore 0000664 0000000 0000000 00000000307 15211567312 0017322 0 ustar 00root root 0000000 0000000 p38venv
alembic
alembic.ini
build
.idea
.pytest_cache
.mypy_cache
*.coverage
*.pyc
*.log
test.db
.vscode/
dist
/ormar.egg-info/
site
profile.py
*.db
*.db-journal
*coverage.xml
.benchmarks/
CLAUDE.md
ormar-orm-ormar-a7e7fde/.pre-commit-config.yaml 0000664 0000000 0000000 00000000253 15211567312 0021613 0 ustar 00root root 0000000 0000000 repos:
- repo: local
hooks:
- id: pre-commit-local
name: format
entry: make pre-commit
language: python
pass_filenames: false
ormar-orm-ormar-a7e7fde/LICENSE.md 0000664 0000000 0000000 00000002067 15211567312 0016743 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2020 Radosław Drążkiewicz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. ormar-orm-ormar-a7e7fde/Makefile 0000664 0000000 0000000 00000001735 15211567312 0017000 0 ustar 00root root 0000000 0000000 test_all: test_pg test_mysql test_sqlite
test_pg: export DATABASE_URL=postgresql://username:password@localhost:5432/testsuite
test_pg:
docker-compose -f scripts/docker-compose.yml up -d postgres
bash scripts/test.sh -svv
docker-compose -f scripts/docker-compose.yml stop postgres
test_mysql: export DATABASE_URL=mysql://username:password@127.0.0.1:3306/testsuite
test_mysql:
docker-compose -f "scripts/docker-compose.yml" up -d mysql
bash scripts/test.sh -svv
docker-compose -f scripts/docker-compose.yml stop mysql
test_sqlite:
bash scripts/test.sh -svv
test_docs:
bash scripts/test_docs.sh -svv
test:
pytest -svv tests/
coverage:
pytest --cov=ormar --cov=tests --cov-fail-under=100 --cov-report=term-missing tests
type_check:
mkdir -p .mypy_cache && poetry run python -m mypy ormar tests --ignore-missing-imports --install-types --non-interactive
lint:
poetry run python -m ruff check . --fix
fmt:
poetry run python -m ruff format .
pre-commit: fmt lint type_check ormar-orm-ormar-a7e7fde/README.md 0000664 0000000 0000000 00000062531 15211567312 0016620 0 ustar 00root root 0000000 0000000 # ormar
### Overview
The `ormar` package is an async mini ORM for Python, with support for **Postgres,
MySQL**, and **SQLite**.
The main benefits of using `ormar` are:
* getting an **async ORM that can be used with async frameworks** (fastapi, starlette etc.)
* getting just **one model to maintain** - you don't have to maintain pydantic and other orm models (sqlalchemy, peewee, gino etc.)
The goal was to create a simple ORM that can be **used directly (as request and response models) with [`fastapi`][fastapi]** that bases it's data validation on pydantic.
Ormar - apart from the obvious "ORM" in name - gets its name from _ormar_ in Swedish which means _snakes_, and _ormar_ in Croatian which means _cabinet_.
And what's a better name for python ORM than snakes cabinet :)
**If you like ormar remember to star the repository in [github](https://github.com/collerek/ormar)!**
The bigger community we build, the easier it will be to catch bugs and attract contributors ;)
### Documentation
Check out the [documentation][documentation] for details.
**Note that for brevity most of the documentation snippets omit the creation of the database
and scheduling the execution of functions for asynchronous run.**
If you want more real life examples than in the documentation you can see the [tests][tests] folder,
since they actually have to create and connect to a database in most of the tests.
Yet remember that those are - well - tests and not all solutions are suitable to be used in real life applications.
### 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-crudrouter`](https://github.com/awtkns/fastapi-crudrouter)
* [`fastapi-pagination`](https://github.com/uriyyo/fastapi-pagination)
If you maintain or use a different library and would like it to support `ormar` let us know how we can help.
### Dependencies
Ormar is built with:
* [`sqlalchemy core`][sqlalchemy-core] for query building.
* [`sqlalchemy async`][sqlalchemy-async] for cross-database async support.
* [`pydantic`][pydantic] for data validation.
### License
`ormar` is built as open-sorce software and will remain completely free (MIT license).
As I write open-source code to solve everyday problems in my work or to promote and build strong python
community you can say thank you and buy me a coffee or sponsor me with a monthly amount to help ensure my work remains free and maintained.
Sponsor - Github Sponsors
### Migrating from `sqlalchemy` and existing databases
If you currently use `sqlalchemy` and would like to switch to `ormar` check out the auto-translation
tool that can help you with translating existing sqlalchemy orm models so you do not have to do it manually.
**Beta** versions available at github: [`sqlalchemy-to-ormar`](https://github.com/collerek/sqlalchemy-to-ormar)
or simply `pip install sqlalchemy-to-ormar`
`sqlalchemy-to-ormar` can be used in pair with `sqlacodegen` to auto-map/ generate `ormar` models from existing database, even if you don't use `sqlalchemy` for your project.
### Migrations & Database creation
Because ormar is built on SQLAlchemy core, you can use [`alembic`][alembic] to provide
database migrations (and you really should for production code).
For tests and basic applications the `sqlalchemy` is more than enough:
```python
# note this is just a partial snippet full working example below
# 1. Imports
import sqlalchemy
import ormar
from ormar import DatabaseConnection
# 2. Initialization
DATABASE_URL = "sqlite+aiosqlite:///db.sqlite"
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=DatabaseConnection(DATABASE_URL),
)
# Define models here
# 3. Database creation and tables creation
engine = sqlalchemy.create_engine(DATABASE_URL.replace('+aiosqlite', ''))
base_ormar_config.metadata.create_all(engine)
```
For a sample configuration of alembic and more information regarding migrations and
database creation visit [migrations][migrations] documentation section.
### Package versions
**ormar is still under development:**
We recommend pinning any dependencies (with i.e. `ormar~=0.9.1`)
`ormar` also follows the release numeration that breaking changes bump the major number,
while other changes and fixes bump minor number, so with the latter you should be safe to
update, yet always read the [releases][releases] docs before.
`example: (0.5.2 -> 0.6.0 - breaking, 0.5.2 -> 0.5.3 - non breaking)`.
### Asynchronous Python
Note that `ormar` is an asynchronous ORM, which means that you have to `await` the calls to
the methods, that are scheduled for execution in an event loop. Python has a builtin module
[`asyncio`][asyncio] that allows you to do just that.
Note that most "normal" python interpreters do not allow execution of `await`
outside of a function (because you actually schedule this function for delayed execution
and don't get the result immediately).
In a modern web framework (like `fastapi`), the framework will handle this for you, but if
you plan to do this on your own you need to perform this manually like described in the
quick start below.
### Quick Start
Note that you can find the same script in examples folder on github.
```python
from typing import Optional
import ormar
import pydantic
import sqlalchemy
DATABASE_URL = "sqlite+aiosqlite:///db.sqlite"
# 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
base_ormar_config = ormar.OrmarConfig(
database=ormar.DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
# Note that all type hints are optional
# below is a perfectly valid model declaration
# class Author(ormar.Model):
# 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):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
# note - normally import should be at the beginning of the file
import asyncio
async def setup_database():
"""Create all tables in the database."""
if not base_ormar_config.database.is_connected:
await base_ormar_config.database.connect()
async with base_ormar_config.database.engine.begin() as conn:
await conn.run_sync(base_ormar_config.metadata.drop_all)
await conn.run_sync(base_ormar_config.metadata.create_all)
if base_ormar_config.database.is_connected:
await base_ormar_config.database.disconnect()
# create the database
# note that in production you should use migrations
# note that this is not required if you connect to existing database
# just to be sure we clear the db before
asyncio.run(setup_database())
# all functions below are divided into functionality categories
# note how all functions are defined with async - hence can use await AND needs to
# be awaited on their own
async def create():
# Create some records to work with through QuerySet.create method.
# Note that queryset is exposed on each Model's class as objects
tolkien = await Author.objects.create(name="J.R.R. Tolkien")
await Book.objects.create(author=tolkien, title="The Hobbit", year=1937)
await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955)
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
# alternative creation of object divided into 2 steps
sapkowski = Author(name="Andrzej Sapkowski")
# do some stuff
await sapkowski.save()
# or save() after initialization
await Book(author=sapkowski, title="The Witcher", year=1990).save()
await Book(author=sapkowski, title="The Tower of Fools", year=2002).save()
# to read more about inserting data into the database
# visit: https://collerek.github.io/ormar/queries/create/
async def read():
# Fetch an instance, without loading a foreign key relationship on it.
# Django style
book = await Book.objects.get(title="The Hobbit")
# or python style
book = await Book.objects.get(Book.title == "The Hobbit")
book2 = await Book.objects.first()
# first() fetch the instance with lower primary key value
assert book == book2
# you can access all fields on loaded model
assert book.title == "The Hobbit"
assert book.year == 1937
# when no condition is passed to get()
# it behaves as last() based on primary key column
book3 = await Book.objects.get()
assert book3.title == "The Tower of Fools"
# When you have a relation, ormar always defines a related model for you
# even when all you loaded is a foreign key value like in this example
assert isinstance(book.author, Author)
# primary key is populated from foreign key stored in books table
assert book.author.pk == 1
# since the related model was not loaded all other fields are None
assert book.author.name is None
# Load the relationship from the database when you already have the related model
# alternatively see joins section below
await book.author.load()
assert book.author.name == "J.R.R. Tolkien"
# get all rows for given model
authors = await Author.objects.all()
assert len(authors) == 2
# to read more about reading data from the database
# visit: https://collerek.github.io/ormar/queries/read/
async def update():
# read existing row from db
tolkien = await Author.objects.get(name="J.R.R. Tolkien")
assert tolkien.name == "J.R.R. Tolkien"
tolkien_id = tolkien.id
# change the selected property
tolkien.name = "John Ronald Reuel Tolkien"
# call update on a model instance
await tolkien.update()
# confirm that object was updated
tolkien = await Author.objects.get(name="John Ronald Reuel Tolkien")
assert tolkien.name == "John Ronald Reuel Tolkien"
assert tolkien.id == tolkien_id
# alternatively update data without loading
await Author.objects.filter(name__contains="Tolkien").update(name="J.R.R. Tolkien")
# to read more about updating data in the database
# visit: https://collerek.github.io/ormar/queries/update/
async def delete():
silmarillion = await Book.objects.get(year=1977)
# call delete() on instance
await silmarillion.delete()
# alternatively delete without loading
await Book.objects.delete(title="The Tower of Fools")
# note that when there is no record ormar raises NoMatch exception
try:
await Book.objects.get(year=1977)
except ormar.NoMatch:
print("No book from 1977!")
# to read more about deleting data from the database
# visit: https://collerek.github.io/ormar/queries/delete/
# note that despite the fact that record no longer exists in database
# the object above is still accessible and you can use it (and i.e. save()) again.
tolkien = silmarillion.author
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
async def joins():
# Tho join two models use select_related
# Django style
book = await Book.objects.select_related("author").get(title="The Hobbit")
# Python style
book = await Book.objects.select_related(Book.author).get(
Book.title == "The Hobbit"
)
# now the author is already prefetched
assert book.author.name == "J.R.R. Tolkien"
# By default you also get a second side of the relation
# constructed as lowercase source model name +'s' (books in this case)
# you can also provide custom name with parameter related_name
# Django style
author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien")
# Python style
author = await Author.objects.select_related(Author.books).all(
Author.name == "J.R.R. Tolkien"
)
assert len(author[0].books) == 3
# for reverse and many to many relations you can also prefetch_related
# that executes a separate query for each of related models
# Django style
author = await Author.objects.prefetch_related("books").get(name="J.R.R. Tolkien")
# Python style
author = await Author.objects.prefetch_related(Author.books).get(
Author.name == "J.R.R. Tolkien"
)
assert len(author.books) == 3
# to read more about relations
# visit: https://collerek.github.io/ormar/relations/
# to read more about joins and subqueries
# visit: https://collerek.github.io/ormar/queries/joins-and-subqueries/
async def filter_and_sort():
# to filter the query you can use filter() or pass key-value pars to
# get(), all() etc.
# to use special methods or access related model fields use double
# underscore like to filter by the name of the author use author__name
# Django style
books = await Book.objects.all(author__name="J.R.R. Tolkien")
# python style
books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien")
assert len(books) == 3
# filter can accept special methods also separated with double underscore
# to issue sql query ` where authors.name like "%tolkien%"` that is not
# case sensitive (hence small t in Tolkien)
# Django style
books = await Book.objects.filter(author__name__icontains="tolkien").all()
# python style
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).all()
assert len(books) == 3
# to sort use order_by() function of queryset
# to sort decreasing use hyphen before the field name
# same as with filter you can use double underscores to access related fields
# Django style
books = (
await Book.objects.filter(author__name__icontains="tolkien")
.order_by("-year")
.all()
)
# python style
books = (
await Book.objects.filter(Book.author.name.icontains("tolkien"))
.order_by(Book.year.desc())
.all()
)
assert len(books) == 3
assert books[0].title == "The Silmarillion"
assert books[2].title == "The Hobbit"
# to read more about filtering and ordering
# visit: https://collerek.github.io/ormar/queries/filter-and-sort/
async def subset_of_columns():
# to exclude some columns from loading when querying the database
# you can use fields() method
hobbit = await Book.objects.fields(["title"]).get(title="The Hobbit")
# note that fields not included in fields are empty (set to None)
assert hobbit.year is None
assert hobbit.author is None
# selected field is there
assert hobbit.title == "The Hobbit"
# alternatively you can provide columns you want to exclude
hobbit = await Book.objects.exclude_fields(["year"]).get(title="The Hobbit")
# year is still not set
assert hobbit.year is None
# but author is back
assert hobbit.author is not None
# also you cannot exclude primary key column - it's always there
# even if you EXPLICITLY exclude it it will be there
# note that each model have a shortcut for primary_key column which is pk
# and you can filter/access/set the values by this alias like below
assert hobbit.pk is not None
# note that you cannot exclude fields that are not nullable
# (required) in model definition
try:
await Book.objects.exclude_fields(["title"]).get(title="The Hobbit")
except pydantic.ValidationError:
print("Cannot exclude non nullable field title")
# to read more about selecting subset of columns
# visit: https://collerek.github.io/ormar/queries/select-columns/
async def pagination():
# to limit number of returned rows use limit()
books = await Book.objects.limit(1).all()
assert len(books) == 1
assert books[0].title == "The Hobbit"
# to offset number of returned rows use offset()
books = await Book.objects.limit(1).offset(1).all()
assert len(books) == 1
assert books[0].title == "The Lord of the Rings"
# alternatively use paginate that combines both
books = await Book.objects.paginate(page=2, page_size=2).all()
assert len(books) == 2
# note that we removed one book of Sapkowski in delete()
# and recreated The Silmarillion - by default when no order_by is set
# ordering sorts by primary_key column
assert books[0].title == "The Witcher"
assert books[1].title == "The Silmarillion"
# to read more about pagination and number of rows
# visit: https://collerek.github.io/ormar/queries/pagination-and-rows-number/
async def aggregations():
# count:
assert 2 == await Author.objects.count()
# exists
assert await Book.objects.filter(title="The Hobbit").exists()
# maximum
assert 1990 == await Book.objects.max(columns=["year"])
# minimum
assert 1937 == await Book.objects.min(columns=["year"])
# average
assert 1964.75 == await Book.objects.avg(columns=["year"])
# sum
assert 7859 == await Book.objects.sum(columns=["year"])
# to read more about aggregated functions
# visit: https://collerek.github.io/ormar/queries/aggregations/
async def raw_data():
# extract raw data in a form of dicts or tuples
# note that this skips the validation(!) as models are
# not created from parsed data
# get list of objects as dicts
assert await Book.objects.values() == [
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937},
{"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955},
{"id": 4, "author": 2, "title": "The Witcher", "year": 1990},
{"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977},
]
# get list of objects as tuples
assert await Book.objects.values_list() == [
(1, 1, "The Hobbit", 1937),
(2, 1, "The Lord of the Rings", 1955),
(4, 2, "The Witcher", 1990),
(5, 1, "The Silmarillion", 1977),
]
# filter data - note how you always get a list
assert await Book.objects.filter(title="The Hobbit").values() == [
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}
]
# select only wanted fields
assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [
{"id": 1, "title": "The Hobbit"}
]
# if you select only one column you could flatten it with values_list
assert await Book.objects.values_list("title", flatten=True) == [
"The Hobbit",
"The Lord of the Rings",
"The Witcher",
"The Silmarillion",
]
# to read more about extracting raw values
# visit: https://collerek.github.io/ormar/queries/aggregations/
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 base_ormar_config.database:
await function()
# note that if you use framework like `fastapi` you shouldn't connect
# in your endpoints but have a global connection pool
# check https://collerek.github.io/ormar/fastapi/ and section with db connection
async def cleanup_database():
"""Drop all tables from the database."""
if not base_ormar_config.database.is_connected:
await base_ormar_config.database.connect()
async with base_ormar_config.database.engine.begin() as conn:
await conn.run_sync(base_ormar_config.metadata.drop_all)
if base_ormar_config.database.is_connected:
await base_ormar_config.database.disconnect()
# gather and execute all functions
# note that normally you use gather() function to run several functions
# concurrently but we actually modify the data and we rely on the order of functions
for func in [
create,
read,
update,
delete,
joins,
filter_and_sort,
subset_of_columns,
pagination,
aggregations,
raw_data,
]:
print(f"Executing: {func.__name__}")
asyncio.run(with_connect(func))
# drop the database tables
asyncio.run(cleanup_database())
```
## Ormar Specification
### QuerySet methods
* `create(**kwargs): -> Model`
* `get(*args, **kwargs): -> Model`
* `get_or_none(*args, **kwargs): -> Optional[Model]`
* `get_or_create(_defaults: Optional[dict[str, Any]] = None, *args, **kwargs) -> tuple[Model, bool]`
* `first(*args, **kwargs): -> Model`
* `update(each: bool = False, **kwargs) -> int`
* `update_or_create(**kwargs) -> Model`
* `bulk_create(objects: list[Model]) -> None`
* `bulk_update(objects: list[Model], columns: list[str] = None) -> None`
* `delete(*args, each: bool = False, **kwargs) -> int`
* `all(*args, **kwargs) -> list[Optional[Model]]`
* `iterate(*args, **kwargs) -> AsyncGenerator[Model]`
* `filter(*args, **kwargs) -> QuerySet`
* `exclude(*args, **kwargs) -> QuerySet`
* `select_related(related: Union[list, str]) -> QuerySet`
* `prefetch_related(related: Union[list, str]) -> QuerySet`
* `limit(limit_count: int) -> QuerySet`
* `offset(offset: int) -> QuerySet`
* `count(distinct: bool = True) -> int`
* `exists() -> bool`
* `max(columns: list[str]) -> Any`
* `min(columns: list[str]) -> Any`
* `avg(columns: list[str]) -> Any`
* `sum(columns: list[str]) -> Any`
* `fields(columns: Union[list, str, set, dict]) -> QuerySet`
* `exclude_fields(columns: Union[list, str, set, dict]) -> QuerySet`
* `order_by(columns:Union[list, str]) -> QuerySet`
* `values(fields: Union[list, str, set, dict])`
* `values_list(fields: Union[list, str, set, dict])`
#### Relation types
* One to many - with `ForeignKey(to: Model)`
* Many to many - with `ManyToMany(to: Model, Optional[through]: Model)`
#### Model fields types
Available Model Fields (with required args - optional ones in docs):
* `String(max_length)`
* `Text()`
* `Boolean()`
* `Integer()`
* `Float()`
* `Date()`
* `Time()`
* `DateTime()`
* `JSON()`
* `BigInteger()`
* `SmallInteger()`
* `Decimal(scale, precision)`
* `UUID()`
* `LargeBinary(max_length)`
* `Enum(enum_class)`
* `Enum` like Field - by passing `choices` to any other Field type
* `EncryptedString` - by passing `encrypt_secret` and `encrypt_backend`
* `ForeignKey(to)`
* `ManyToMany(to)`
### Available fields options
The following keyword arguments are supported on all field types.
* `primary_key: bool`
* `nullable: bool`
* `default: Any`
* `server_default: Any`
* `on_update: Any`
* `index: bool`
* `unique: bool`
* `choices: typing.Sequence`
* `name: str`
All fields are required unless one of the following is set:
* `nullable` - Creates a nullable column. Sets the default to `False`. Read the fields common parameters for details.
* `sql_nullable` - Used to set different setting for pydantic and the database. Sets the default to `nullable` value. Read the fields common parameters for details.
* `default` - Set a default value for the field. **Not available for relation fields**
* `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.
### Available signals
Signals allow to trigger your function for a given event on a given Model.
* `pre_save`
* `post_save`
* `pre_update`
* `post_update`
* `pre_delete`
* `post_delete`
* `pre_relation_add`
* `post_relation_add`
* `pre_relation_remove`
* `post_relation_remove`
* `post_bulk_update`
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
[sqlalchemy-async]: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
[pydantic]: https://pydantic-docs.helpmanual.io/
[encode/orm]: https://github.com/encode/orm/
[alembic]: https://alembic.sqlalchemy.org/en/latest/
[fastapi]: https://fastapi.tiangolo.com/
[documentation]: https://collerek.github.io/ormar/
[migrations]: https://collerek.github.io/ormar/models/migrations/
[asyncio]: https://docs.python.org/3/library/asyncio.html
[releases]: https://collerek.github.io/ormar/releases/
[tests]: https://github.com/collerek/ormar/tree/master/tests
ormar-orm-ormar-a7e7fde/SECURITY.md 0000664 0000000 0000000 00000001040 15211567312 0017116 0 ustar 00root root 0000000 0000000 # Security Policy
## Supported Versions
Only the latest stable version of ormar receives security updates. Make sure you are running the most recent release.
## Reporting a Vulnerability
Please do not open public issues for security problems. Instead, use GitHub's "Report a vulnerability" feature in the Security tab of this repository. We will review and respond as soon as possible.
## Security Updates
If we fix a vulnerability, we will announce it in the release notes. Keep your installation up to date to receive the latest fixes.
ormar-orm-ormar-a7e7fde/benchmarks/ 0000775 0000000 0000000 00000000000 15211567312 0017447 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/benchmarks/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0021546 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/benchmarks/conftest.py 0000664 0000000 0000000 00000005657 15211567312 0021663 0 ustar 00root root 0000000 0000000 import asyncio
import random
import string
import nest_asyncio
import pytest
import pytest_asyncio
from tests.lifespan import init_tests
from tests.settings import create_config
import ormar
base_ormar_config = create_config()
nest_asyncio.apply()
pytestmark = pytest.mark.asyncio
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
score: float = ormar.Integer(minimum=0, maximum=100)
class AuthorWithManyFields(Author):
year_born: int = ormar.Integer()
year_died: int = ormar.Integer(nullable=True)
birthplace: str = ormar.String(max_length=255)
class Publisher(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="publishers")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
prestige: int = ormar.Integer(minimum=0, maximum=10)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
author: Author = ormar.ForeignKey(Author, index=True)
publisher: Publisher = ormar.ForeignKey(Publisher, index=True)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
create_test_database = init_tests(base_ormar_config, scope="function")
@pytest_asyncio.fixture(autouse=True, scope="function")
async def connect_database(create_test_database):
if not base_ormar_config.database.is_connected:
await base_ormar_config.database.connect()
yield
if base_ormar_config.database.is_connected:
await base_ormar_config.database.disconnect()
@pytest_asyncio.fixture
async def author():
author = await Author(name="Author", score=10).save()
return author
@pytest_asyncio.fixture
async def publisher():
publisher = await Publisher(name="Publisher", prestige=random.randint(0, 10)).save()
return publisher
@pytest_asyncio.fixture
async def authors_in_db(num_models: int):
authors = [
Author(
name="".join(random.sample(string.ascii_letters, 5)),
score=int(random.random() * 100),
)
for i in range(0, num_models)
]
await Author.objects.bulk_create(authors)
return await Author.objects.all()
@pytest_asyncio.fixture
async def aio_benchmark(benchmark):
def _fixture_wrapper(func):
def _func_wrapper(*args, **kwargs):
if asyncio.iscoroutinefunction(func):
# Get the running event loop instead of requesting it as a fixture
loop = asyncio.get_running_loop()
@benchmark
def benchmarked_func():
a = loop.run_until_complete(func(*args, **kwargs))
return a
return benchmarked_func
else:
return benchmark(func, *args, **kwargs)
return _func_wrapper
return _fixture_wrapper
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_aggregate.py 0000664 0000000 0000000 00000002703 15211567312 0025022 0 ustar 00root root 0000000 0000000 import pytest
from benchmarks.conftest import Author
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_count(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def count():
return await Author.objects.count()
c = count()
assert c == len(authors_in_db)
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_avg(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def avg():
return await Author.objects.avg("score")
average = avg()
assert 0 <= average <= 100
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_sum(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def sum_():
return await Author.objects.sum("score")
s = sum_()
assert 0 <= s <= 100 * num_models
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_min(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def min_():
return await Author.objects.min("score")
m = min_()
assert 0 <= m <= 100
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_max(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def max_():
return await Author.objects.max("score")
m = max_()
assert 0 <= m <= 100
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_alias_lookup.py 0000664 0000000 0000000 00000004133 15211567312 0025555 0 ustar 00root root 0000000 0000000 """
Micro-benchmark for get_column_name_from_alias and related alias functions.
"""
import pytest
from benchmarks.conftest import Author, Book
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("num_lookups", [1000, 10000])
def test_get_column_name_from_alias(benchmark, num_lookups: int) -> None:
"""Benchmark get_column_name_from_alias - O(n) linear scan per call."""
# Get all column aliases for the model
aliases = [col.name for col in Author.ormar_config.table.columns]
def run() -> None:
for _ in range(num_lookups):
for alias in aliases:
Author.get_column_name_from_alias(alias)
benchmark(run)
@pytest.mark.parametrize("num_lookups", [1000, 10000])
def test_get_column_name_from_alias_book(benchmark, num_lookups: int) -> None:
"""Benchmark on Book model (more fields including FKs)."""
aliases = [col.name for col in Book.ormar_config.table.columns]
def run() -> None:
for _ in range(num_lookups):
for alias in aliases:
Book.get_column_name_from_alias(alias)
benchmark(run)
@pytest.mark.parametrize("num_lookups", [1000, 10000])
def test_translate_columns_to_aliases(benchmark, num_lookups: int) -> None:
"""Benchmark translate_columns_to_aliases - dict key remapping."""
def run() -> None:
for _ in range(num_lookups):
kwargs = {"name": "test", "score": 50, "id": 1}
Author.translate_columns_to_aliases(kwargs)
benchmark(run)
@pytest.mark.parametrize("num_lookups", [1000, 10000])
def test_translate_aliases_to_columns(benchmark, num_lookups: int) -> None:
"""Benchmark translate_aliases_to_columns - reverse remapping."""
# Get aliases
aliases = {
field.get_alias(): "value"
for field_name, field in Author.ormar_config.model_fields.items()
if field.get_alias()
}
if not aliases:
aliases = {"name": "test", "score": 50, "id": 1}
def run() -> None:
for _ in range(num_lookups):
kwargs = dict(aliases)
Author.translate_aliases_to_columns(kwargs)
benchmark(run)
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_bulk_create.py 0000664 0000000 0000000 00000001243 15211567312 0025352 0 ustar 00root root 0000000 0000000 import random
import string
import pytest
from benchmarks.conftest import Author
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("num_models", [10, 20, 40])
async def test_making_and_inserting_models_in_bulk(aio_benchmark, num_models: int):
@aio_benchmark
async def make_and_insert(num_models: int):
authors = [
Author(
name="".join(random.sample(string.ascii_letters, 5)),
score=int(random.random() * 100),
)
for i in range(0, num_models)
]
assert len(authors) == num_models
await Author.objects.bulk_create(authors)
make_and_insert(num_models)
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_bulk_update.py 0000664 0000000 0000000 00000001260 15211567312 0025370 0 ustar 00root root 0000000 0000000 import random
import string
import pytest
from benchmarks.conftest import Author
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("num_models", [10, 20, 40])
async def test_updating_models_in_bulk(
aio_benchmark, num_models: int, authors_in_db: list[Author]
):
starting_first_name = authors_in_db[0].name
@aio_benchmark
async def update(authors: list[Author]):
await Author.objects.bulk_update(authors)
for author in authors_in_db:
author.name = "u_" + "".join(random.sample(string.ascii_letters, 5))
update(authors_in_db)
author = await Author.objects.get(id=authors_in_db[0].id)
assert author.name != starting_first_name
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_create.py 0000664 0000000 0000000 00000005433 15211567312 0024342 0 ustar 00root root 0000000 0000000 import random
import string
import pytest
from benchmarks.conftest import Author, Book, Publisher
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("num_models", [10, 20, 40])
async def test_creating_models_individually(aio_benchmark, num_models: int):
@aio_benchmark
async def create(num_models: int):
authors = []
for idx in range(0, num_models):
author = await Author.objects.create(
name="".join(random.sample(string.ascii_letters, 5)),
score=int(random.random() * 100),
)
authors.append(author)
return authors
authors = create(num_models)
for author in authors:
assert author.id is not None
@pytest.mark.parametrize("num_models", [10, 20, 40])
async def test_creating_individually_with_related_models(
aio_benchmark, num_models: int, author: Author, publisher: Publisher
):
@aio_benchmark
async def create_with_related_models(
author: Author, publisher: Publisher, num_models: int
):
books = []
for idx in range(0, num_models):
book = await Book.objects.create(
author=author,
publisher=publisher,
title="".join(random.sample(string.ascii_letters, 5)),
year=random.randint(0, 2000),
)
books.append(book)
return books
books = create_with_related_models(
author=author, publisher=publisher, num_models=num_models
)
for book in books:
assert book.id is not None
@pytest.mark.parametrize("num_models", [10, 20, 40])
async def test_get_or_create_when_create(aio_benchmark, num_models: int):
@aio_benchmark
async def get_or_create(num_models: int):
authors = []
for idx in range(0, num_models):
author, created = await Author.objects.get_or_create(
name="".join(random.sample(string.ascii_letters, 5)),
score=int(random.random() * 100),
)
assert created
authors.append(author)
return authors
authors = get_or_create(num_models)
for author in authors:
assert author.id is not None
@pytest.mark.parametrize("num_models", [10, 20, 40])
async def test_update_or_create_when_create(aio_benchmark, num_models: int):
@aio_benchmark
async def update_or_create(num_models: int):
authors = []
for idx in range(0, num_models):
author = await Author.objects.update_or_create(
name="".join(random.sample(string.ascii_letters, 5)),
score=int(random.random() * 100),
)
authors.append(author)
return authors
authors = update_or_create(num_models)
for author in authors:
assert author.id is not None
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_delete.py 0000664 0000000 0000000 00000001512 15211567312 0024333 0 ustar 00root root 0000000 0000000 import pytest
from benchmarks.conftest import Author
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_deleting_all(
aio_benchmark, num_models: int, authors_in_db: list[Author]
):
@aio_benchmark
async def delete_all():
await Author.objects.delete(each=True)
delete_all()
num = await Author.objects.count()
assert num == 0
@pytest.mark.parametrize("num_models", [10, 20, 40])
async def test_deleting_individually(
aio_benchmark, num_models: int, authors_in_db: list[Author]
):
@aio_benchmark
async def delete_one_by_one(authors: list[Author]):
for author in authors:
await Author.objects.filter(id=author.id).delete()
delete_one_by_one(authors_in_db)
num = await Author.objects.count()
assert num == 0
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_get.py 0000664 0000000 0000000 00000006074 15211567312 0023660 0 ustar 00root root 0000000 0000000 import random
import string
import pytest
import pytest_asyncio
from benchmarks.conftest import Author, Book, Publisher
pytestmark = pytest.mark.asyncio
@pytest_asyncio.fixture()
async def books(author: Author, publisher: Publisher, num_models: int):
books = [
Book(
author=author,
publisher=publisher,
title="".join(random.sample(string.ascii_letters, 5)),
year=random.randint(0, 2000),
)
for _ in range(0, num_models)
]
await Book.objects.bulk_create(books)
return books
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_get_all(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def get_all(authors: list[Author]):
return await Author.objects.all()
authors = get_all(authors_in_db)
for idx, author in enumerate(authors_in_db):
assert authors[idx].id == author.id
@pytest.mark.parametrize("num_models", [10, 20, 40])
async def test_get_all_with_related_models(
aio_benchmark, num_models: int, author: Author, books: list[Book]
):
@aio_benchmark
async def get_with_related(author: Author):
return await Author.objects.select_related("books").all(id=author.id)
authors = get_with_related(author)
assert len(authors[0].books) == num_models
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_get_one(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def get_one(authors: list[Author]):
return await Author.objects.get(id=authors[0].id)
author = get_one(authors_in_db)
assert author == authors_in_db[0]
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_get_or_none(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def get_or_none(authors: list[Author]):
return await Author.objects.get_or_none(id=authors[0].id)
author = get_or_none(authors_in_db)
assert author == authors_in_db[0]
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_get_or_create_when_get(
aio_benchmark, num_models: int, authors_in_db: list[Author]
):
@aio_benchmark
async def get_or_create(authors: list[Author]):
author, created = await Author.objects.get_or_create(id=authors[0].id)
assert not created
return author
author = get_or_create(authors_in_db)
assert author == authors_in_db[0]
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_first(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def first():
return await Author.objects.first()
author = first()
assert author == authors_in_db[0]
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_exists(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def check_exists(authors: list[Author]):
return await Author.objects.filter(id=authors[0].id).exists()
exists = check_exists(authors_in_db)
assert exists
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_init.py 0000664 0000000 0000000 00000002700 15211567312 0024034 0 ustar 00root root 0000000 0000000 import random
import string
import pytest
from benchmarks.conftest import Author, Book, Publisher
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_initializing_models(aio_benchmark, num_models: int):
@aio_benchmark
async def initialize_models(num_models: int):
authors = [
Author(
name="".join(random.sample(string.ascii_letters, 5)),
score=int(random.random() * 100),
)
for i in range(0, num_models)
]
assert len(authors) == num_models
_ = initialize_models(num_models)
@pytest.mark.parametrize("num_models", [10, 20, 40])
async def test_initializing_models_with_related_models(aio_benchmark, num_models: int):
@aio_benchmark
async def initialize_models_with_related_models(
author: Author, publisher: Publisher, num_models: int
):
_ = [
Book(
author=author,
publisher=publisher,
title="".join(random.sample(string.ascii_letters, 5)),
year=random.randint(0, 2000),
)
for i in range(0, num_models)
]
author = await Author(name="Author", score=10).save()
publisher = await Publisher(name="Publisher", prestige=random.randint(0, 10)).save()
_ = initialize_models_with_related_models(
author=author, publisher=publisher, num_models=num_models
)
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_iterate.py 0000664 0000000 0000000 00000001103 15211567312 0024522 0 ustar 00root root 0000000 0000000 import pytest
from benchmarks.conftest import Author
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_iterate(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def iterate_over_all(authors: list[Author]):
authors = []
async for author in Author.objects.iterate():
authors.append(author)
return authors
authors = iterate_over_all(authors_in_db)
for idx, author in enumerate(authors_in_db):
assert authors[idx].id == author.id
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_merge.py 0000664 0000000 0000000 00000010240 15211567312 0024166 0 ustar 00root root 0000000 0000000 """Benchmark for nested-join row merging in ``_process_query_result_rows``.
The workload below targets the path optimized by the in-place index
assignment in ``_merge_items_lists``: a parent that fans out across many
joined rows so the matched branch fires repeatedly during
``_recursive_add``, with both halves of late-round merges accumulating
overlapping child PKs.
Shape:
Project (1) -> Task (N, FK) -> Tag (M, m2m, shared across tasks)
A single ``Project`` produces ``N * M`` result rows on
``select_related(["tasks", "tasks__tags"]).all()``. The pairwise
``_recursive_add`` consolidates the duplicates, and in the deeper rounds
both sides hold overlapping ``Task`` PKs (because every adjacent row
carries the same task with a different tag); inside each task the tag
lists also accumulate and overlap across recursion halves.
"""
import pytest
import ormar
from benchmarks.conftest import base_ormar_config
pytestmark = pytest.mark.asyncio
class BenchTag(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="bench_merge_tags")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50)
class BenchProject(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="bench_merge_projects")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class BenchTask(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="bench_merge_tasks")
id: int = ormar.Integer(primary_key=True)
project: BenchProject = ormar.ForeignKey(
BenchProject, index=True, related_name="tasks"
)
title: str = ormar.String(max_length=100)
tags: list[BenchTag] = ormar.ManyToMany(BenchTag)
@pytest.mark.parametrize(
("num_tasks", "tags_per_task"),
[(5, 5), (10, 10), (20, 10)],
)
async def test_select_related_nested_merge(
aio_benchmark, num_tasks: int, tags_per_task: int
):
"""Project -> Tasks (FK) -> Tags (m2m) workload.
Result row count is ``num_tasks * tags_per_task`` for one project; every
row materializes a duplicate Project with one Task (with one Tag).
The merge path consolidates duplicates pairwise via ``_recursive_add``
— covers ``_merge_items_lists`` end-to-end through the full join /
materialize / merge pipeline.
"""
project = await BenchProject(name="P").save()
tags = [await BenchTag(name=f"t{i}").save() for i in range(tags_per_task)]
for i in range(num_tasks):
task = await BenchTask(project=project, title=f"T{i}").save()
for tag in tags:
await task.tags.add(tag)
@aio_benchmark
async def query():
return await BenchProject.objects.select_related(["tasks", "tasks__tags"]).all()
result = query()
assert len(result) == 1
assert len(result[0].tasks) == num_tasks
for task in result[0].tasks:
assert len(task.tags) == tags_per_task
@pytest.mark.parametrize("list_size", [10, 50, 100])
def test_merge_items_lists_pk_overlap(benchmark, list_size: int):
"""Microbenchmark for ``_merge_items_lists`` with full PK overlap.
Constructs two equally sized lists of saved tasks where every entry
in ``current_field`` matches an entry in ``other_value`` by PK. This
is the worst case the per-pair list rebuild used to be O(N) on — K
matches, each filtering an N-element ``value_to_set``. With
``other_idx`` driving in-place writes the cost drops from O(K·N) to
O(K).
The benchmark calls the merge classmethod directly so the SA query /
row materialization overhead is excluded — the signal we want is the
inner loop only. Every task is fully populated (no relations to
recurse into) so ``merge_two_instances`` is light and the
``_merge_items_lists`` body itself dominates.
"""
project = BenchProject(id=1, name="p")
current_field = [
BenchTask(id=i, project=project, title=f"T{i}") for i in range(list_size)
]
other_value = [
BenchTask(id=i, project=project, title=f"T{i}") for i in range(list_size)
]
benchmark(
BenchTask._merge_items_lists,
field_name="tasks",
current_field=current_field,
other_value=other_value,
relation_map={"tasks": ...},
)
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_save.py 0000664 0000000 0000000 00000003334 15211567312 0024033 0 ustar 00root root 0000000 0000000 import random
import string
import pytest
from benchmarks.conftest import Author, Book, Publisher
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("num_models", [10, 20, 40])
async def test_saving_models_individually(aio_benchmark, num_models: int):
@aio_benchmark
async def make_and_insert(num_models: int):
authors = [
Author(
name="".join(random.sample(string.ascii_letters, 5)),
score=int(random.random() * 100),
)
for i in range(0, num_models)
]
assert len(authors) == num_models
ids = []
for author in authors:
a = await author.save()
ids.append(a)
return ids
ids = make_and_insert(num_models)
for id in ids:
assert id is not None
@pytest.mark.parametrize("num_models", [10, 20, 40])
async def test_saving_models_individually_with_related_models(
aio_benchmark, num_models: int, author: Author, publisher: Publisher
):
@aio_benchmark
async def making_and_inserting_related_models_one_by_one(
author: Author, publisher: Publisher, num_models: int
):
books = [
Book(
author=author,
publisher=publisher,
title="".join(random.sample(string.ascii_letters, 5)),
year=random.randint(0, 2000),
)
for i in range(0, num_models)
]
ids = []
for book in books:
await book.save()
ids.append(book.id)
return ids
ids = making_and_inserting_related_models_one_by_one(
author=author, publisher=publisher, num_models=num_models
)
for id in ids:
assert id is not None
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_update.py 0000664 0000000 0000000 00000001262 15211567312 0024355 0 ustar 00root root 0000000 0000000 import random
import string
import pytest
from benchmarks.conftest import Author
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("num_models", [10, 20, 40])
async def test_updating_models_individually(
aio_benchmark, num_models: int, authors_in_db: list[Author]
):
starting_first_name = authors_in_db[0].name
@aio_benchmark
async def update(authors: list[Author]):
for author in authors:
_ = await author.update(
name="u_" + "".join(random.sample(string.ascii_letters, 5))
)
update(authors_in_db)
author = await Author.objects.get(id=authors_in_db[0].id)
assert author.name != starting_first_name
ormar-orm-ormar-a7e7fde/benchmarks/test_benchmark_values.py 0000664 0000000 0000000 00000001637 15211567312 0024400 0 ustar 00root root 0000000 0000000 import pytest
from benchmarks.conftest import Author
pytestmark = pytest.mark.asyncio
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_values(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def get_all_values(authors: list[Author]):
return await Author.objects.values()
authors_list = get_all_values(authors_in_db)
for idx, author in enumerate(authors_in_db):
assert authors_list[idx]["id"] == author.id
@pytest.mark.parametrize("num_models", [250, 500, 1000])
async def test_values_list(aio_benchmark, num_models: int, authors_in_db: list[Author]):
@aio_benchmark
async def get_all_values_list(authors: list[Author]):
return await Author.objects.values_list()
authors_list = get_all_values_list(authors_in_db)
for idx, author in enumerate(authors_in_db):
assert authors_list[idx][0] == author.id
ormar-orm-ormar-a7e7fde/docs/ 0000775 0000000 0000000 00000000000 15211567312 0016262 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs/contributing.md 0000664 0000000 0000000 00000003741 15211567312 0021320 0 ustar 00root root 0000000 0000000 All contributions to *ormar* are welcomed!
## Issues
To make it as simple as possible for us to help you, please include the following:
* OS
* python version
* ormar version
* database backend (mysql, sqlite or postgresql)
Please try to always include the above unless you're unable to install *ormar* or **know** it's not relevant
to your question or feature request.
## Pull Requests
It should be quite straight forward to get started and create a Pull Request.
!!! note
Unless your change is trivial (typo, docs tweak etc.), please create an issue to discuss the change before
creating a pull request.
To make contributing as easy and fast as possible, you'll want to run tests and linting locally.
You'll need to have **python 3.6.2**, **3.7**, or **3.8**, **poetry**, and **git** installed.
```bash
# 1. clone your fork and cd into the repo directory
git clone git@github.com:/ormar.git
cd ormar
# 2. Install ormar, dependencies and test dependencies
poetry install -E dev
# 3. Checkout a new branch and make your changes
git checkout -b my-new-feature-branch
# make your changes...
# 4. Formatting and linting
# ormar uses ruff for formatting and linting and mypy for type hints check
# run all of the following as all those calls will be run on travis after every push
ruff format ormar tests
ruff check ormar tests
flake8 ormar
mypy ormar tests
# 5. Run tests
# on localhost all tests are run against sglite backend
# rest of the backends will be checked after push
pytest -svv --cov=ormar --cov=tests --cov-fail-under=100 --cov-report=term-missing
# 6. Build documentation
mkdocs build
# if you have changed the documentation make sure it builds successfully
# you can also use `mkdocs serve` to serve the documentation at localhost:8000
# ... commit, push, and create your pull request
```
!!!tip
For more information on how and why ormar works the way it works
please see the [API documentation][API documentation]
[API documentation]: ./api/index.md ormar-orm-ormar-a7e7fde/docs/fastapi/ 0000775 0000000 0000000 00000000000 15211567312 0017711 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs/fastapi/index.md 0000664 0000000 0000000 00000014476 15211567312 0021356 0 ustar 00root root 0000000 0000000 # Fastapi
The use of ormar with fastapi is quite simple.
Apart from connecting to databases at startup everything else
you need to do is substitute pydantic models with ormar models.
Here you can find a very simple sample application code.
!!!warning
This example assumes that you already have a database created. If that is not the case please visit [database initialization][database initialization] section.
!!!tip
The following example (all sections) should be put in one file.
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.
## Quick Start
!!!note
Note that you can find the full quick start script in the [github](https://github.com/collerek/ormar) repo under examples.
### Imports and initialization
Define startup and shutdown procedures using FastAPI lifespan and use is in the
application.
```python
from typing import Optional, AsyncIterator
import sqlalchemy
from fastapi import FastAPI
import ormar
from ormar import DatabaseConnection
from contextlib import asynccontextmanager
from fastapi import FastAPI
def get_lifespan(config):
@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()
return lifespan
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=DatabaseConnection("sqlite+aiosqlite:///test.db"),
)
app = FastAPI(lifespan=get_lifespan(base_ormar_config))
```
!!!info
You can read more on connecting to databases in [fastapi][fastapi] documentation
### Models definition
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):
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)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
```
!!!tip
You can read more on defining `Models` in [models][models] section.
### Fastapi endpoints definition
Define your desired endpoints, note how `ormar` models are used both
as `response_model` and as a requests parameters.
```python
@app.get("/items/", response_model=list[Item])
async def get_items():
items = await Item.objects.select_related("category").all()
return items
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
await item.save()
return item
@app.post("/categories/", response_model=Category)
async def create_category(category: Category):
await category.save()
return 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.model_dump())
@app.delete("/items/{item_id}")
async def delete_item(item_id: int, item: Item = None):
if item:
return {"deleted_rows": await item.delete()}
item_db = await Item.objects.get(pk=item_id)
return {"deleted_rows": await item_db.delete()}
```
!!!note
Note how ormar `Model` methods like save() are available straight out of the box after fastapi initializes it for you.
!!!note
Note that you can return a `Model` (or list of `Models`) directly - fastapi will jsonize it for you
### Test the application
#### Run fastapi
If you want to run this script and play with fastapi swagger install uvicorn first
`pip install uvicorn`
And launch the fastapi.
`uvicorn :app --reload`
Now you can navigate to your browser (by default fastapi address is `127.0.0.1:8000/docs`) and play with the api.
!!!info
You can read more about running fastapi in [fastapi][fastapi] docs.
#### Test with pytest
Here you have a sample test that will prove that everything works as intended.
Be sure to create the tables first. If you are using pytest you can use a fixture.
```python
@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)
```
```python
# here is a sample test to check the working of the ormar with fastapi
from starlette.testclient import TestClient
def test_all_endpoints():
# note that TestClient is only sync, don't use asyns here
client = TestClient(app)
# note that you need to connect to database manually
# or use client as contextmanager during tests
with client as client:
response = client.post("/categories/", json={"name": "test cat"})
category = response.json()
response = client.post(
"/items/", json={"name": "test", "id": 1, "category": category}
)
item = Item(**response.json())
assert item.pk is not None
response = client.get("/items/")
items = [Item(**item) for item in response.json()]
assert items[0] == item
item.name = "New name"
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.model_dump())
assert response.json().get("deleted_rows", "__UNDEFINED__") != "__UNDEFINED__"
response = client.get("/items/")
items = response.json()
assert len(items) == 0
```
!!!tip
If you want to see more test cases and how to test ormar/fastapi see [tests][tests] directory in the github repo
!!!info
You can read more on testing fastapi in [fastapi][fastapi] docs.
[fastapi]: https://fastapi.tiangolo.com/
[models]: ../models/index.md
[database initialization]: ../models/migrations.md
[tests]: https://github.com/collerek/ormar/tree/master/tests
ormar-orm-ormar-a7e7fde/docs/fastapi/requests.md 0000664 0000000 0000000 00000014566 15211567312 0022122 0 ustar 00root root 0000000 0000000 # Request
You can use ormar Models in `fastapi` request `Body` parameters instead of pydantic models.
You can of course also mix `ormar.Model`s with `pydantic` ones if you need to.
One of the most common tasks in requests is excluding certain fields that you do not want to include in the payload you send to API.
This can be achieved in several ways in `ormar` so below you can review your options and select the one most suitable for your situation.
## Excluding fields in request
### Optional fields
Note that each field that is optional is not required, that means that Optional fields can be skipped both in response and in requests.
Field is not required if (any/many/all) of following:
* Field is marked with `nullable=True`
* Field has `default` value or function provided, i.e. `default="Test"`
* Field has a `server_default` value set
* Field is an `autoincrement=True` `primary_key` field (note that `ormar.Integer` `primary_key` is `autoincrement` by default)
Example:
```python
base_ormar_config = ormar.OrmarConfig(
metadata=metadata
database=database
)
class User(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255)
password: str = ormar.String(max_length=255)
first_name: str = ormar.String(max_length=255, nullable=True)
last_name: str = ormar.String(max_length=255)
category: str = ormar.String(max_length=255, default="User")
```
In above example fields `id` (is an `autoincrement` `Integer`), `first_name` ( has `nullable=True`) and `category` (has `default`) are optional and can be skipped in response and model will still validate.
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.
```python
# note that app is an FastApi app
@app.post("/users/", response_model=User) # here we use ormar.Model in response
async def create_user(user: User): # here we use ormar.Model in request parameter
return await user.save()
```
That means that if you do not pass i.e. `first_name` in request it will validate correctly (as field is optional), `None` will be saved in the database.
### Generate `pydantic` model from `ormar.Model`
Since task of excluding fields is so common `ormar` has a special way to generate `pydantic` models from existing `ormar.Models` without you needing to retype all the fields.
That method is `get_pydantic()` method available on all models classes.
```python
# generate a tree of models without password on User and without priority on nested Category
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.model_dump()).save()
```
!!!Note
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 `model_dump()`, `select_all()` etc.)
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.
#### Mypy and type checking
Note that assigning a function as a python type passes at runtime (as it's not checked) the static type checkers like mypy will complain.
Although result of the function call will always be the same for given model using a dynamically created type is not allowed.
Therefore you have two options:
First one is to simply add `# type: ignore` to skip the type checking
```python
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.model_dump()).save()
```
The second one is a little bit more hacky and utilizes a way in which fastapi extract function parameters.
You can overwrite the `__annotations__` entry for given param.
```python
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.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
app.post("/categories/", response_model=User)(create_user3)
```
Note that this will cause mypy to "think" that user is an ormar model but since in request it doesn't matter that much (you pass jsonized dict anyway and you need to convert before saving).
That still should work fine as generated model will be a subset of fields, so all needed fields will validate, and all not used fields will fail at runtime.
### Separate `pydantic` model
The final solution is to just create separate pydantic model manually.
That works exactly the same as with normal fastapi application, so you can have different models for response and requests etc.
Sample:
```python
import pydantic
class UserCreate(pydantic.BaseModel):
model_config = pydantic.ConfigDict(from_attributes=True)
email: str
first_name: str
last_name: str
password: str
@app.post("/users3/", response_model=User) # use ormar model here (but of course you CAN use pydantic also here)
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.model_dump()).save()
```
ormar-orm-ormar-a7e7fde/docs/fastapi/response.md 0000664 0000000 0000000 00000024026 15211567312 0022075 0 ustar 00root root 0000000 0000000 # Response
You can use ormar Models in `fastapi` response_model instead of pydantic models.
You can of course also mix `ormar.Model`s with `pydantic` ones if you need to.
One of the most common tasks in responses is excluding certain fields that you do not want to include in response data.
This can be achieved in several ways in `ormar` so below you can review your options and select the one most suitable for your situation.
## Excluding fields in response
### Optional fields
Note that each field that is optional is not required, that means that Optional fields can be skipped both in response and in requests.
Field is not required if (any/many/all) of following:
* Field is marked with `nullable=True`
* Field has `default` value or function provided, i.e. `default="Test"`
* Field has a `server_default` value set
* Field is an `autoincrement=True` `primary_key` field (note that `ormar.Integer` `primary_key` is `autoincrement` by default)
Example:
```python
base_ormar_config = ormar.OrmarConfig(
metadata=metadata
database=database
)
class User(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255)
password: str = ormar.String(max_length=255)
first_name: str = ormar.String(max_length=255, nullable=True)
last_name: str = ormar.String(max_length=255)
category: str = ormar.String(max_length=255, default="User")
```
In above example fields `id` (is an `autoincrement` `Integer`), `first_name` ( has `nullable=True`) and `category` (has `default`) are optional and can be skipped in response and model will still validate.
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:
```python
# note that app is an FastApi app
@app.post("/users/", response_model=User) # here we use ormar.Model in response
async def create_user(user: User): # here we use ormar.Model in request parameter
return await user.save()
```
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`.
If you want to fully exclude the field from the result read on.
### FastApi `response_model_exclude`
Fastapi has `response_model_exclude` that accepts a set (or a list) of field names.
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.
```python
@app.post("/users/", response_model=User, response_model_exclude={"password"})
async def create_user(user: User):
return await user.save()
```
Above endpoint can be queried like this:
```python
from starlette.testclient import TestClient
client = TestClient(app)
with client as client:
# note there is no pk
user = {
"email": "test@domain.com",
"password": "^*^%A*DA*IAAA",
"first_name": "John",
"last_name": "Doe",
}
response = client.post("/users/", json=user)
# note that the excluded field is fully gone from response
assert "password" not in response.json()
# read the response and initialize model out of it
created_user = User(**response.json())
# note pk is populated by autoincrement
assert created_user.pk is not None
# note that password is missing in initialized model too
assert created_user.password is None
```
!!!Note
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.
#### Nested models excludes
Despite the fact that `fastapi` allows passing only set of field names, so simple excludes, when using `response_model_exclude`, ormar is smarter.
In `ormar` you can exclude nested models using two types of notations.
One is a dictionary with nested fields that represents the model tree structure, and the second one is double underscore separated path of field names.
Assume for a second that our user's category is a separate model:
```python
base_ormar_config = ormar.OrmarConfig(
metadata=metadata
database=database
)
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=255)
priority: int = ormar.Integer(nullable=True)
class User(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255)
password: str = ormar.String(max_length=255)
first_name: str = ormar.String(max_length=255, nullable=True)
last_name: str = ormar.String(max_length=255)
category: Optional[Category] = ormar.ForeignKey(Category, related_name="categories")
```
If you want to exclude `priority` from category in your response, you can still use fastapi parameter.
```python
@app.post("/users/", response_model=User, response_model_exclude={"category__priority"})
async def create_user(user: User):
return await user.save()
```
Note that you can go in deeper models with double underscore, and if you want to exclude multiple fields from nested model you need to prefix them with full path.
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
!!!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.
### 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.
```python
@app.post("/users2/", response_model=User)
async def create_user2(user: User):
user = await user.save()
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`.
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).
So if you skip `response_model` altogether you can do something like this:
```python
@app.post("/users4/") # note no response_model
async def create_user4(user: User):
user = await user.save()
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.
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`
Since task of excluding fields is so common `ormar` has a special way to generate `pydantic` models from existing `ormar.Models` without you needing to retype all the fields.
That method is `get_pydantic()` method available on all models classes.
```python
# generate a tree of models without password on User and without priority on nested Category
ResponseUser = User.get_pydantic(exclude={"password": ..., "category": {"priority"}})
@app.post("/users3/", response_model=ResponseUser) # use the generated model here
async def create_user3(user: User):
return await user.save()
```
!!!Note
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 `model_dump()`, `select_all()` etc.)
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.
### Separate `pydantic` model
The final solution is to just create separate pydantic model manually.
That works exactly the same as with normal fastapi application so you can have different models for response and requests etc.
Sample:
```python
import pydantic
class UserBase(pydantic.BaseModel):
model_config = pydantic.ConfigDict(from_attributes=True)
email: str
first_name: str
last_name: str
@app.post("/users3/", response_model=UserBase) # use pydantic model here
async def create_user3(user: User): #use ormar model here (but of course you CAN use pydantic also here)
return await user.save()
```
ormar-orm-ormar-a7e7fde/docs/fields/ 0000775 0000000 0000000 00000000000 15211567312 0017530 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs/fields/common-parameters.md 0000664 0000000 0000000 00000021600 15211567312 0023502 0 ustar 00root root 0000000 0000000 # Common Parameters
All `Field` types have a set of common parameters.
## primary_key
`primary_key`: `bool` = `False` -> by default False.
Sets the primary key column on a table, foreign keys always refer to the pk of the `Model`.
Used in sql only.
## autoincrement
`autoincrement`: `bool` = `primary_key and type == int` -> defaults to True if column is a primary key and of type Integer, otherwise False.
Can be only used with int/bigint fields.
If a field has autoincrement it becomes optional.
Used both in sql and pydantic (changes pk field to optional for autoincrement).
## nullable
`nullable`: `bool` = `False` -> defaults to False for all fields except relation fields.
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
Specifies if field is optional or required, used both with sql and pydantic.
By default, used for both `pydantic` and `sqlalchemy` as those are the most common settings:
* `nullable=False` - means database column is not null and field is required in pydantic
* `nullable=True` - means database column is null and field is optional in pydantic
If you want to set different setting for pydantic and the database see `sql_nullable` below.
!!!note
By default all `ForeignKeys` are also nullable, meaning the related `Model` is not required.
If you change the `ForeignKey` column to `nullable=False`, it becomes required.
## sql_nullable
`sql_nullable`: `bool` = `nullable` -> defaults to the value of nullable (described above).
Specifies if field is not null or allows nulls in the database only.
Use this setting in combination with `nullable` only if you want to set different options on pydantic model and in the database.
A sample usage might be i.e. making field not null in the database, but allow this field to be nullable in pydantic (i.e. with `server_default` value).
That will prevent the updates of the field to null (as with `server_default` set you cannot insert null values already as the default value would be used)
## default
`default`: `Any` = `None` -> defaults to None.
A default value used if no other value is passed.
In sql invoked on an insert, used during pydantic model definition.
If the field has a default value it becomes optional.
You can pass a static value or a Callable (function etc.)
Used both in sql and pydantic.
Sample usage:
```python
# note the distinction between passing a value and Callable pointer
# value
name: str = ormar.String(max_length=200, default="Name")
# note that when you call a function it's not a pointer to Callable
# a definition like this will call the function at startup and assign
# the result of the function to the default, so it will be constant value for all instances
created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now())
# if you want to pass Callable reference (note that it cannot have arguments)
# note lack of the parenthesis -> ormar will call this function for you on each model
created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
# Callable can be a function, builtin, class etc.
```
## server default
`server_default`: `Any` = `None` -> defaults to None.
A default value used if no other value is passed.
In sql invoked on the server side so you can pass i.e. sql function (like now() or query/value wrapped in sqlalchemy text() clause).
If the field has a server_default value it becomes optional.
You can pass a static value or a Callable (function etc.)
Used in sql only.
Sample usage:
```Python hl_lines="20-22"
--8<-- "../docs_src/fields/docs004.py"
```
!!!warning
`server_default` accepts `str`, `sqlalchemy.sql.elements.ClauseElement` or `sqlalchemy.sql.elements.TextClause`
so if you want to set i.e. Integer value you need to wrap it in `sqlalchemy.text()` function like above
!!!tip
You can pass also valid sql (dialect specific) wrapped in `sqlalchemy.text()`
For example `func.now()` above could be exchanged for `text('(CURRENT_TIMESTAMP)')` for sqlite backend
!!!info
`server_default` is passed straight to sqlalchemy table definition so you can read more in [server default][server default] sqlalchemy documentation
## on_update
`on_update`: `Any` = `None` -> defaults to None.
A value (or Callable) that is written to the column on every update when the
user did not provide that field explicitly.
It applies to all write paths that update existing rows:
* `Model.update()` (and therefore `Model.upsert()`)
* `QuerySet.update()`
* `QuerySet.bulk_update()`
A field is considered explicitly provided (and its `on_update` is **skipped**)
when any of the following is true:
* it was passed as a keyword argument to `update()` / `upsert()`
* it was mutated on the instance via attribute assignment before `update()` /
`bulk_update()` is called
* it was passed through `QuerySet.update(field=value)`
You can pass a static value or a Callable; a Callable is invoked every time
the update fires (e.g. `datetime.now` produces the current timestamp on every
update).
Used in sql only.
Sample usage:
```python
from datetime import datetime
class Task(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="tasks")
id: int = ormar.Integer(primary_key=True)
# callable - invoked on every update
updated_at: datetime = ormar.DateTime(
default=datetime.now, on_update=datetime.now
)
# static value
is_dirty: bool = ormar.Boolean(default=False, on_update=True)
# static value - field keeps the value the caller passed if any
revision: int = ormar.Integer(default=0, on_update=1)
task = await Task.objects.create()
assert task.is_dirty is False
await task.update()
assert task.is_dirty is True # on_update applied
assert task.revision == 1 # on_update applied
assert task.updated_at > created_time # on_update callable re-evaluated
# explicit values override on_update
await task.update(revision=5)
assert task.revision == 5 # user value wins, on_update skipped
```
!!!note
`on_update` is orthogonal to the `onupdate` parameter on `ForeignKey`,
which maps to the SQL `ON UPDATE` referential action and is unrelated.
## name
`name`: `str` = `None` -> defaults to None
Allows you to specify a column name alias to be used.
Useful for existing database structures that use a reserved keyword, or if you would like to use database name that is different from `ormar` field name.
Take for example the snippet below.
`from`, being a reserved word in python, will prevent you from creating a model with that column name.
Changing the model name to `from_` and adding the parameter `name='from'` will cause ormar to use `from` for the database column name.
```python
#... rest of Model cut for brevity
from_: str = ormar.String(max_length=15, name='from')
```
Similarly, you can change the foreign key column names in database, while keeping the desired relation name in ormar:
```python
# ... rest of Model cut for brevity
album: Optional[Album] = ormar.ForeignKey(Album, name="album_id")
```
## index
`index`: `bool` = `False` -> by default False,
Sets the index on a table's column.
Used in sql only.
## unique
`unique`: `bool` = `False`
Sets the unique constraint on a table's column.
Used in sql only.
## overwrite_pydantic_type
By default, ormar uses predefined pydantic field types that it applies on model creation (hence the type hints are optional).
If you want to, you can apply your own type, that will be **completely** replacing the build in one.
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.
!!!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!
```python
base_ormar_config = ormar.OrmarConfig(
metadata=metadata
database=database
)
# sample overwrites
class OverwriteTest(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="overwrites")
id: int = ormar.Integer(primary_key=True)
my_int: str = ormar.Integer(overwrite_pydantic_type=PositiveInt)
constraint_dict: Json = ormar.JSON(
overwrite_pydantic_type=Optional[Json[dict[str, int]]])
```
[relations]: ../relations/index.md
[queries]: ../queries/index.md
[pydantic]: https://pydantic-docs.helpmanual.io/usage/types/#constrained-types
[server default]: https://docs.sqlalchemy.org/en/13/core/defaults.html#server-invoked-ddl-explicit-default-expressions
ormar-orm-ormar-a7e7fde/docs/fields/encryption.md 0000664 0000000 0000000 00000013131 15211567312 0022243 0 ustar 00root root 0000000 0000000 # Encryption
`ormar` provides you with a way to encrypt a field in the database only.
Provided encryption backends allow for both one-way encryption (`HASH` backend) as
well as both-way encryption/decryption (`FERNET` backend).
!!!warning
Note that in order for encryption to work you need to install optional `cryptography` package.
You can do it manually `pip install cryptography` or with ormar by `pip install ormar[crypto]`
!!!warning
Note that adding `encrypt_backend` changes the database column type to `TEXT`,
which needs to be reflected in db either by migration (`alembic`) or manual change
## Defining a field encryption
To encrypt a field you need to pass at minimum `encrypt_secret` and `encrypt_backend` parameters.
```python hl_lines="10-12"
base_ormar_config = ormar.OrmarConfig(
metadata=metadata
database=database
)
class Filter(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100,
encrypt_secret="secret123",
encrypt_backend=ormar.EncryptBackends.FERNET)
```
!!!warning
You can encrypt all `Field` types apart from `primary_key` column and relation
columns (`ForeignKey` and `ManyToMany`). Check backends details for more information.
## Available backends
### HASH
HASH is a one-way hash (like for password), never decrypted on retrieval
To set it up pass appropriate backend value.
```python
... # rest of model definition
password: str = ormar.String(max_length=128,
encrypt_secret="secret123",
encrypt_backend=ormar.EncryptBackends.HASH)
```
Note that since this backend never decrypt the stored value it's only applicable for
`String` fields. Used hash is a `sha512` hash, so the field length has to be >=128.
!!!warning
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
Note that provided `encrypt_secret` is first hashed itself and used as salt, so in order to
compare to stored string you need to recreate this steps. The `order_by` will not work as encrypted strings are compared so you cannot reliably order by.
```python
class Hash(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="hashes")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=128,
encrypt_secret="udxc32",
encrypt_backend=ormar.EncryptBackends.HASH)
await Hash(name='test1').save()
# note the steps to recreate the stored value
# you can use also cryptography package instead of hashlib
secret = hashlib.sha256("udxc32".encode()).digest()
secret = base64.urlsafe_b64encode(secret)
hashed_test1 = hashlib.sha512(secret + 'test1'.encode()).hexdigest()
# full value comparison works
hash1 = await Hash.objects.get(name='test1')
assert hash1.name == hashed_test1
# but partial comparison does not (hashed strings are compared)
with pytest.raises(NoMatch):
await Filter.objects.get(name__icontains='test')
```
### FERNET
FERNET is a two-way encrypt/decrypt backend
To set it up pass appropriate backend value.
```python
... # rest of model definition
year: int = ormar.Integer(encrypt_secret="secret123",
encrypt_backend=ormar.EncryptBackends.FERNET)
```
Value is encrypted on way to database end decrypted on way out. Can be used on all types,
as the returned value is parsed to corresponding python type.
!!!warning
Note that in `FERNET` backend you loose `filter`ing possibility altogether as part of the encrypted value is a timestamp.
The same goes for `order_by` as encrypted strings are compared so you cannot reliably order by.
```python
class Filter(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100,
encrypt_secret="asd123",
encrypt_backend=ormar.EncryptBackends.FERNET)
await Filter(name='test1').save()
await Filter(name='test1').save()
# values are properly encrypted and later decrypted
filters = await Filter.objects.all()
assert filters[0].name == filters[1].name == 'test1'
# but you cannot filter at all since part of the fernet hash is a timestamp
# which means that even if you encrypt the same string 2 times it will be different
with pytest.raises(NoMatch):
await Filter.objects.get(name='test1')
```
## Custom Backends
If you wish to support other type of encryption (i.e. AES) you can provide your own `EncryptionBackend`.
To setup a backend all you need to do is subclass `ormar.fields.EncryptBackend` class and provide required backend.
Sample dummy backend (that does nothing) can look like following:
```python
class DummyBackend(ormar.fields.EncryptBackend):
def _initialize_backend(self, secret_key: bytes) -> None:
pass
def encrypt(self, value: Any) -> str:
return value
def decrypt(self, value: Any) -> str:
return value
```
To use this backend set `encrypt_backend` to `CUSTOM` and provide your backend as
argument by `encrypt_custom_backend`.
```python
class Filter(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100,
encrypt_secret="secret123",
encrypt_backend=ormar.EncryptBackends.CUSTOM,
encrypt_custom_backend=DummyBackend
)
```
ormar-orm-ormar-a7e7fde/docs/fields/field-types.md 0000664 0000000 0000000 00000015167 15211567312 0022311 0 ustar 00root root 0000000 0000000 # Fields
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 `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.
## Fields Types
### String
`String(max_length: int,
min_length: int = None,
regex: str = None,)` has a required `max_length` parameter.
* Sqlalchemy column: `sqlalchemy.String`
* Type (used for pydantic): `str`
!!!tip
For explanation of other parameters check [pydantic](https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation) documentation.
### Text
`Text()` has no required parameters.
* Sqlalchemy column: `sqlalchemy.Text`
* Type (used for pydantic): `str`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Boolean
`Boolean()` has no required parameters.
* Sqlalchemy column: `sqlalchemy.Boolean`
* Type (used for pydantic): `bool`
### Integer
`Integer(minimum: int = None,
maximum: int = None,
multiple_of: int = None)` has no required parameters.
* Sqlalchemy column: `sqlalchemy.Integer`
* Type (used for pydantic): `int`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### BigInteger
`BigInteger(minimum: int = None,
maximum: int = None,
multiple_of: int = None)` has no required parameters.
* Sqlalchemy column: `sqlalchemy.BigInteger`
* Type (used for pydantic): `int`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### SmallInteger
`SmallInteger(minimum: int = None,
maximum: int = None,
multiple_of: int = None)` has no required parameters.
* Sqlalchemy column: `sqlalchemy.SmallInteger`
* Type (used for pydantic): `int`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Float
`Float(minimum: float = None,
maximum: float = None,
multiple_of: int = None)` has no required parameters.
* Sqlalchemy column: `sqlalchemy.Float`
* Type (used for pydantic): `float`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Decimal
`Decimal(minimum: float = None,
maximum: float = None,
multiple_of: int = None,
precision: int = None,
scale: int = None,
max_digits: int = None,
decimal_places: int = None)` has no required parameters
You can use either `length` and `precision` parameters or `max_digits` and `decimal_places`.
* Sqlalchemy column: `sqlalchemy.DECIMAL`
* Type (used for pydantic): `decimal.Decimal`
!!!tip
For explanation of other parameters check [pydantic][pydantic] documentation.
### Date
`Date()` has no required parameters.
* Sqlalchemy column: `sqlalchemy.Date`
* Type (used for pydantic): `datetime.date`
### Time
`Time(timezone: bool = False)` has no required parameters.
You can pass `timezone=True` for timezone aware database column.
* Sqlalchemy column: `sqlalchemy.Time`
* Type (used for pydantic): `datetime.time`
### DateTime
`DateTime(timezone: bool = False)` has no required parameters.
You can pass `timezone=True` for timezone aware database column.
* Sqlalchemy column: `sqlalchemy.DateTime`
* Type (used for pydantic): `datetime.datetime`
### JSON
`JSON()` has no required parameters.
* Sqlalchemy column: `sqlalchemy.JSON`
* Type (used for pydantic): `pydantic.Json`
### LargeBinary
`LargeBinary(max_length)` has a required `max_length` parameter.
* Sqlalchemy column: `sqlalchemy.LargeBinary`
* Type (used for pydantic): `bytes`
LargeBinary length is used in some backend (i.e. mysql) to determine the size of the field,
in other backends it's simply ignored yet in ormar it's always required. It should be max
size of the file/bytes in bytes.
`LargeBinary` has also optional `represent_as_base64_str: bool = False` flag.
When set to `True` `ormar` will auto-convert bytes value to base64 decoded string,
you can also set value by passing a base64 encoded string.
That way you can i.e. set the value by API, even if value is not `utf-8` compatible and would otherwise fail during json conversion.
```python
import base64
... # other imports skipped for brevity
base_ormar_config = ormar.OrmarConfig(
metadata=metadata
database=database
)
class LargeBinaryStr(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="my_str_blobs")
id: int = ormar.Integer(primary_key=True)
test_binary: str = ormar.LargeBinary(
max_length=100000, represent_as_base64_str=True
)
# set non utf-8 compliant value - note this can be passed by api (i.e. fastapi) in json
item = LargeBinaryStr(test_binary=base64.b64encode(b"\xc3\x28").decode())
assert item.test_binary == base64.b64encode(b"\xc3\x28").decode()
# technical note that underlying value is still bytes and will be saved as so
assert item.__dict__["test_binary"] == b"\xc3\x28"
```
### UUID
`UUID(uuid_format: str = 'hex')` has no required parameters.
* Sqlalchemy column: `ormar.UUID` based on `sqlalchemy.CHAR(36)` or `sqlalchemy.CHAR(32)` field (for string or hex format respectively)
* Type (used for pydantic): `uuid.UUID`
`uuid_format` parameters allow 'hex'(default) or 'string' values.
Depending on the format either 32 or 36 char is used in the database.
Sample:
* 'hex' format value = `c616ab438cce49dbbf4380d109251dce` (CHAR(32))
* 'string' value = `c616ab43-8cce-49db-bf43-80d109251dce` (CHAR(36))
When loaded it's always python UUID so you can compare it and compare two formats values between each other.
### Enum
There are two ways to use enums in ormar -> one is a dedicated `Enum` field that uses `sqlalchemy.Enum` column type, while the other is setting `choices` on any field in ormar.
The Enum field uses the database dialect specific Enum column type if it's available, but fallback to varchar if this field type is not available.
The `choices` option always respect the database field type selected.
So which one to use depends on the backend you use and on the column/ data type you want in your Enum field.
#### Enum - Field
`Enum(enum_class=type[Enum])` has a required `enum_class` parameter.
* Sqlalchemy column: `sqlalchemy.Enum`
* Type (used for pydantic): `type[Enum]`
[relations]: ../relations/index.md
[queries]: ../queries.md
[pydantic]: https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation
[server default]: https://docs.sqlalchemy.org/en/13/core/defaults.html#server-invoked-ddl-explicit-default-expressions
ormar-orm-ormar-a7e7fde/docs/fields/pydantic-fields.md 0000664 0000000 0000000 00000011733 15211567312 0023136 0 ustar 00root root 0000000 0000000 # Pydantic only fields
Ormar allows you to declare normal `pydantic` fields in its model, so you have access to
all basic and custom pydantic fields like `str`, `int`, `HttpUrl`, `PaymentCardNumber` etc.
You can even declare fields leading to nested pydantic only Models, not only single fields.
Since those fields are not stored in database (that's the whole point of those fields),
you have to provide a meaningful value for them, either by setting a default one or
providing one during model initialization.
If `ormar` cannot resolve the value for pydantic field it will fail during loading data from the database,
with missing required value for declared pydantic field.
Options to provide a value are described below.
Of course you can combine few or all of them in one model.
## Optional field
If you set a field as `Optional`, it defaults to `None` if not provided and that's
exactly what's going to happen during loading from database.
```python
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=DatabaseConnection(DATABASE_URL),
)
class ModelTest(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=200)
number: Optional[PaymentCardNumber]
test = ModelTest(name="Test")
assert test.name == "Test"
assert test.number is None
test.number = "123456789015"
await test.save()
test_check = await ModelTest.objects.get()
assert test_check.name == "Test"
# after load it's back to None
assert test_check.number is None
```
## Field with default value
By setting a default value, this value will be set on initialization and database load.
Note that setting a default to `None` is the same as setting the field to `Optional`.
```python
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=DatabaseConnection(DATABASE_URL),
)
class ModelTest(ormar.Model):
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"
test = ModelTest(name="Test")
assert test.name == "Test"
assert test.url == "https://www.example.com"
test.url = "https://www.sdta.ada.pt"
assert test.url == "https://www.sdta.ada.pt"
await test.save()
test_check = await ModelTest.objects.get()
assert test_check.name == "Test"
# after load it's back to default
assert test_check.url == "https://www.example.com"
```
## Default factory function
By setting a `default_factory` function, this result of the function call will be set
on initialization and each database load.
```python
from pydantic import Field, PaymentCardNumber
# ...
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=DatabaseConnection(DATABASE_URL),
)
CARD_NUMBERS = [
"123456789007",
"123456789015",
"123456789023",
"123456789031",
"123456789049",
]
def get_number():
return random.choice(CARD_NUMBERS)
class ModelTest2(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=200)
# note that you do not call the function, just pass reference
number: PaymentCardNumber = Field(default_factory=get_number)
# note that you still CAN provide a value
test = ModelTest2(name="Test2", number="4000000000000002")
assert test.name == "Test2"
assert test.number == "4000000000000002"
await test.save()
test_check = await ModelTest2.objects.get()
assert test_check.name == "Test2"
# after load value is set to be one of the CARD_NUMBERS
assert test_check.number in CARD_NUMBERS
assert test_check.number != test.number
```
## Custom setup in `__init__`
You can provide a value for the field in your `__init__()` method before calling a `super()` init method.
```python
from pydantic import BaseModel
# ...
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=DatabaseConnection(DATABASE_URL),
)
class PydanticTest(BaseModel):
aa: str
bb: int
class ModelTest3(ormar.Model):
ormar_config = base_ormar_config.copy()
# provide your custom init function
def __init__(self, **kwargs):
# add value for required field without default value
kwargs["pydantic_test"] = PydanticTest(aa="random", bb=42)
# remember to call ormar.Model init!
super().__init__(**kwargs)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=200)
pydantic_test: PydanticTest
test = ModelTest3(name="Test3")
assert test.name == "Test3"
assert test.pydantic_test.bb == 42
test.pydantic.aa = "new value"
assert test.pydantic.aa == "new value"
await test.save()
test_check = await ModelTest3.objects.get()
assert test_check.name == "Test3"
# after load it's back to value provided in init
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.
ormar-orm-ormar-a7e7fde/docs/gen_ref_pages.py 0000664 0000000 0000000 00000001643 15211567312 0021424 0 ustar 00root root 0000000 0000000 """Generate the code reference pages and navigation."""
from pathlib import Path
import mkdocs_gen_files
nav = mkdocs_gen_files.Nav()
for path in sorted(Path("ormar").rglob("*.py")):
module_path = path.relative_to(".").with_suffix("")
doc_path = path.relative_to("ormar").with_suffix(".md")
full_doc_path = Path("api", doc_path)
parts = tuple(module_path.parts)
if parts[-1] == "__init__":
parts = parts[:-1]
doc_path = doc_path.with_name("index.md")
full_doc_path = full_doc_path.with_name("index.md")
elif parts[-1] == "__main__":
continue
nav[parts] = str(doc_path)
with mkdocs_gen_files.open(full_doc_path, "w") as fd:
ident = ".".join(parts)
fd.write(f"::: {ident}")
mkdocs_gen_files.set_edit_path(full_doc_path, path)
with mkdocs_gen_files.open("api/SUMMARY.md", "w") as nav_file:
nav_file.writelines(nav.build_literate_nav())
ormar-orm-ormar-a7e7fde/docs/index.md 0000664 0000000 0000000 00000064400 15211567312 0017717 0 ustar 00root root 0000000 0000000 # ormar
### Overview
The `ormar` package is an async ORM for Python, with support for **Postgres,
MySQL**, and **SQLite**.
The main benefits of using `ormar` are:
* getting an **async ORM that can be used with async frameworks** (fastapi, starlette etc.)
* getting just **one model to maintain** - you don't have to maintain pydantic and other orm models (sqlalchemy, peewee, gino etc.)
The goal was to create a simple ORM that can be **used directly (as request and response models) with [`fastapi`][fastapi]** that bases it's data validation on pydantic.
Ormar - apart from the obvious "ORM" in name - gets its name from _ormar_ in Swedish which means _snakes_, and _ormar(e)_ in Croatian which means _cabinet_.
And what's a better name for python ORM than snakes cabinet :)
**If you like ormar remember to star the repository in [github](https://github.com/collerek/ormar)!**
The bigger community we build, the easier it will be to catch bugs and attract contributors ;)
### Documentation
Check out the [documentation][documentation] for details.
**Note that for brevity most of the documentation snippets omit the creation of the database
and scheduling the execution of functions for asynchronous run.**
If you want more real life examples than in the documentation you can see the [tests][tests] folder,
since they actually have to create and connect to a database in most of the tests.
Yet remember that those are - well - tests and not all solutions are suitable to be used in real life applications.
### Part of the `fastapi` ecosystem
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.
It's relatively easy to implement columns for specific dialects as an extensions of ormar.
If you maintain or use a different library and would like it to support `ormar` let us know how we can help.
### Dependencies
Ormar is built with:
* [`sqlalchemy core`][sqlalchemy-core] for query building.
* [SQLAlchemy async][sqlalchemy-async] for database connectivity.
* [`pydantic`][pydantic] for data validation.
### License
`ormar` is built as open-source software and will remain completely free (MIT license).
As I write open-source code to solve everyday problems in my work or to promote and build strong python
community you can say thank you and buy me a coffee or sponsor me with a monthly amount to help ensure my work remains free and maintained.
### Migrating from `sqlalchemy` and existing databases
If you currently use `sqlalchemy` and would like to switch to `ormar` check out the auto-translation
tool that can help you with translating existing sqlalchemy orm models so you do not have to do it manually.
**Beta** versions available at github: [`sqlalchemy-to-ormar`](https://github.com/collerek/sqlalchemy-to-ormar)
or simply `pip install sqlalchemy-to-ormar`
`sqlalchemy-to-ormar` can be used in pair with `sqlacodegen` to auto-map/ generate `ormar` models from existing database, even if you don't use `sqlalchemy` for your project.
### Migrations & Database creation
Because ormar is built on SQLAlchemy core, you can use [`alembic`][alembic] to provide
database migrations (and you really should for production code).
For tests and basic applications the `sqlalchemy` is more than enough:
```python
# note this is just a partial snippet full working example below
# 1. Imports
import sqlalchemy
import ormar
from ormar import DatabaseConnection
# 2. Initialization
DATABASE_URL = "sqlite+aiosqlite:///db.sqlite"
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=DatabaseConnection(DATABASE_URL),
)
# Define models here
# 3. Database creation and tables creation
engine = sqlalchemy.create_engine(DATABASE_URL.replace('+aiosqlite', ''))
base_ormar_config.metadata.create_all(engine)
```
For a sample configuration of alembic and more information regarding migrations and
database creation visit [migrations][migrations] documentation section.
### Package versions
**ormar is still under development:**
We recommend pinning any dependencies (with i.e. `ormar~=0.9.1`)
`ormar` also follows the release numeration that breaking changes bump the major number,
while other changes and fixes bump minor number, so with the latter you should be safe to
update, yet always read the [releases][releases] docs before.
`example: (0.5.2 -> 0.6.0 - breaking, 0.5.2 -> 0.5.3 - non breaking)`.
### Asynchronous Python
Note that `ormar` is an asynchronous ORM, which means that you have to `await` the calls to
the methods, that are scheduled for execution in an event loop. Python has a builtin module
[`asyncio`][asyncio] that allows you to do just that.
Note that most "normal" python interpreters do not allow execution of `await`
outside of a function (because you actually schedule this function for delayed execution
and don't get the result immediately).
In a modern web framework (like `fastapi`), the framework will handle this for you, but if
you plan to do this on your own you need to perform this manually like described in the
quick start below.
### Quick Start
Note that you can find the same script in examples folder on github.
```python
from typing import Optional
import ormar
import pydantic
import sqlalchemy
DATABASE_URL = "sqlite+aiosqlite:///db.sqlite"
# 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
base_ormar_config = ormar.OrmarConfig(
database=ormar.DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
# Note that all type hints are optional
# below is a perfectly valid model declaration
# class Author(ormar.Model):
# 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):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
# note - normally import should be at the beginning of the file
import asyncio
async def setup_database():
"""Create all tables in the database."""
if not base_ormar_config.database.is_connected:
await base_ormar_config.database.connect()
async with base_ormar_config.database.engine.begin() as conn:
await conn.run_sync(base_ormar_config.metadata.drop_all)
await conn.run_sync(base_ormar_config.metadata.create_all)
if base_ormar_config.database.is_connected:
await base_ormar_config.database.disconnect()
# create the database
# note that in production you should use migrations
# note that this is not required if you connect to existing database
# just to be sure we clear the db before
asyncio.run(setup_database())
# all functions below are divided into functionality categories
# note how all functions are defined with async - hence can use await AND needs to
# be awaited on their own
async def create():
# Create some records to work with through QuerySet.create method.
# Note that queryset is exposed on each Model's class as objects
tolkien = await Author.objects.create(name="J.R.R. Tolkien")
await Book.objects.create(author=tolkien, title="The Hobbit", year=1937)
await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955)
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
# alternative creation of object divided into 2 steps
sapkowski = Author(name="Andrzej Sapkowski")
# do some stuff
await sapkowski.save()
# or save() after initialization
await Book(author=sapkowski, title="The Witcher", year=1990).save()
await Book(author=sapkowski, title="The Tower of Fools", year=2002).save()
# to read more about inserting data into the database
# visit: https://collerek.github.io/ormar/queries/create/
async def read():
# Fetch an instance, without loading a foreign key relationship on it.
# Django style
book = await Book.objects.get(title="The Hobbit")
# or python style
book = await Book.objects.get(Book.title == "The Hobbit")
book2 = await Book.objects.first()
# first() fetch the instance with lower primary key value
assert book == book2
# you can access all fields on loaded model
assert book.title == "The Hobbit"
assert book.year == 1937
# when no condition is passed to get()
# it behaves as last() based on primary key column
book3 = await Book.objects.get()
assert book3.title == "The Tower of Fools"
# When you have a relation, ormar always defines a related model for you
# even when all you loaded is a foreign key value like in this example
assert isinstance(book.author, Author)
# primary key is populated from foreign key stored in books table
assert book.author.pk == 1
# since the related model was not loaded all other fields are None
assert book.author.name is None
# Load the relationship from the database when you already have the related model
# alternatively see joins section below
await book.author.load()
assert book.author.name == "J.R.R. Tolkien"
# get all rows for given model
authors = await Author.objects.all()
assert len(authors) == 2
# to read more about reading data from the database
# visit: https://collerek.github.io/ormar/queries/read/
async def update():
# read existing row from db
tolkien = await Author.objects.get(name="J.R.R. Tolkien")
assert tolkien.name == "J.R.R. Tolkien"
tolkien_id = tolkien.id
# change the selected property
tolkien.name = "John Ronald Reuel Tolkien"
# call update on a model instance
await tolkien.update()
# confirm that object was updated
tolkien = await Author.objects.get(name="John Ronald Reuel Tolkien")
assert tolkien.name == "John Ronald Reuel Tolkien"
assert tolkien.id == tolkien_id
# alternatively update data without loading
await Author.objects.filter(name__contains="Tolkien").update(name="J.R.R. Tolkien")
# to read more about updating data in the database
# visit: https://collerek.github.io/ormar/queries/update/
async def delete():
silmarillion = await Book.objects.get(year=1977)
# call delete() on instance
await silmarillion.delete()
# alternatively delete without loading
await Book.objects.delete(title="The Tower of Fools")
# note that when there is no record ormar raises NoMatch exception
try:
await Book.objects.get(year=1977)
except ormar.NoMatch:
print("No book from 1977!")
# to read more about deleting data from the database
# visit: https://collerek.github.io/ormar/queries/delete/
# note that despite the fact that record no longer exists in database
# the object above is still accessible and you can use it (and i.e. save()) again.
tolkien = silmarillion.author
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
async def joins():
# Tho join two models use select_related
# Django style
book = await Book.objects.select_related("author").get(title="The Hobbit")
# Python style
book = await Book.objects.select_related(Book.author).get(
Book.title == "The Hobbit"
)
# now the author is already prefetched
assert book.author.name == "J.R.R. Tolkien"
# By default you also get a second side of the relation
# constructed as lowercase source model name +'s' (books in this case)
# you can also provide custom name with parameter related_name
# Django style
author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien")
# Python style
author = await Author.objects.select_related(Author.books).all(
Author.name == "J.R.R. Tolkien"
)
assert len(author[0].books) == 3
# for reverse and many to many relations you can also prefetch_related
# that executes a separate query for each of related models
# Django style
author = await Author.objects.prefetch_related("books").get(name="J.R.R. Tolkien")
# Python style
author = await Author.objects.prefetch_related(Author.books).get(
Author.name == "J.R.R. Tolkien"
)
assert len(author.books) == 3
# to read more about relations
# visit: https://collerek.github.io/ormar/relations/
# to read more about joins and subqueries
# visit: https://collerek.github.io/ormar/queries/joins-and-subqueries/
async def filter_and_sort():
# to filter the query you can use filter() or pass key-value pars to
# get(), all() etc.
# to use special methods or access related model fields use double
# underscore like to filter by the name of the author use author__name
# Django style
books = await Book.objects.all(author__name="J.R.R. Tolkien")
# python style
books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien")
assert len(books) == 3
# filter can accept special methods also separated with double underscore
# to issue sql query ` where authors.name like "%tolkien%"` that is not
# case sensitive (hence small t in Tolkien)
# Django style
books = await Book.objects.filter(author__name__icontains="tolkien").all()
# python style
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).all()
assert len(books) == 3
# to sort use order_by() function of queryset
# to sort decreasing use hyphen before the field name
# same as with filter you can use double underscores to access related fields
# Django style
books = (
await Book.objects.filter(author__name__icontains="tolkien")
.order_by("-year")
.all()
)
# python style
books = (
await Book.objects.filter(Book.author.name.icontains("tolkien"))
.order_by(Book.year.desc())
.all()
)
assert len(books) == 3
assert books[0].title == "The Silmarillion"
assert books[2].title == "The Hobbit"
# to read more about filtering and ordering
# visit: https://collerek.github.io/ormar/queries/filter-and-sort/
async def subset_of_columns():
# to exclude some columns from loading when querying the database
# you can use fields() method
hobbit = await Book.objects.fields(["title"]).get(title="The Hobbit")
# note that fields not included in fields are empty (set to None)
assert hobbit.year is None
assert hobbit.author is None
# selected field is there
assert hobbit.title == "The Hobbit"
# alternatively you can provide columns you want to exclude
hobbit = await Book.objects.exclude_fields(["year"]).get(title="The Hobbit")
# year is still not set
assert hobbit.year is None
# but author is back
assert hobbit.author is not None
# also you cannot exclude primary key column - it's always there
# even if you EXPLICITLY exclude it it will be there
# note that each model have a shortcut for primary_key column which is pk
# and you can filter/access/set the values by this alias like below
assert hobbit.pk is not None
# note that you cannot exclude fields that are not nullable
# (required) in model definition
try:
await Book.objects.exclude_fields(["title"]).get(title="The Hobbit")
except pydantic.ValidationError:
print("Cannot exclude non nullable field title")
# to read more about selecting subset of columns
# visit: https://collerek.github.io/ormar/queries/select-columns/
async def pagination():
# to limit number of returned rows use limit()
books = await Book.objects.limit(1).all()
assert len(books) == 1
assert books[0].title == "The Hobbit"
# to offset number of returned rows use offset()
books = await Book.objects.limit(1).offset(1).all()
assert len(books) == 1
assert books[0].title == "The Lord of the Rings"
# alternatively use paginate that combines both
books = await Book.objects.paginate(page=2, page_size=2).all()
assert len(books) == 2
# note that we removed one book of Sapkowski in delete()
# and recreated The Silmarillion - by default when no order_by is set
# ordering sorts by primary_key column
assert books[0].title == "The Witcher"
assert books[1].title == "The Silmarillion"
# to read more about pagination and number of rows
# visit: https://collerek.github.io/ormar/queries/pagination-and-rows-number/
async def aggregations():
# count:
assert 2 == await Author.objects.count()
# exists
assert await Book.objects.filter(title="The Hobbit").exists()
# maximum
assert 1990 == await Book.objects.max(columns=["year"])
# minimum
assert 1937 == await Book.objects.min(columns=["year"])
# average
assert 1964.75 == await Book.objects.avg(columns=["year"])
# sum
assert 7859 == await Book.objects.sum(columns=["year"])
# to read more about aggregated functions
# visit: https://collerek.github.io/ormar/queries/aggregations/
async def raw_data():
# extract raw data in a form of dicts or tuples
# note that this skips the validation(!) as models are
# not created from parsed data
# get list of objects as dicts
assert await Book.objects.values() == [
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937},
{"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955},
{"id": 4, "author": 2, "title": "The Witcher", "year": 1990},
{"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977},
]
# get list of objects as tuples
assert await Book.objects.values_list() == [
(1, 1, "The Hobbit", 1937),
(2, 1, "The Lord of the Rings", 1955),
(4, 2, "The Witcher", 1990),
(5, 1, "The Silmarillion", 1977),
]
# filter data - note how you always get a list
assert await Book.objects.filter(title="The Hobbit").values() == [
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}
]
# select only wanted fields
assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [
{"id": 1, "title": "The Hobbit"}
]
# if you select only one column you could flatten it with values_list
assert await Book.objects.values_list("title", flatten=True) == [
"The Hobbit",
"The Lord of the Rings",
"The Witcher",
"The Silmarillion",
]
# to read more about extracting raw values
# visit: https://collerek.github.io/ormar/queries/aggregations/
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 base_ormar_config.database:
await function()
# note that if you use framework like `fastapi` you shouldn't connect
# in your endpoints but have a global connection pool
# check https://collerek.github.io/ormar/fastapi/ and section with db connection
async def cleanup_database():
"""Drop all tables from the database."""
if not base_ormar_config.database.is_connected:
await base_ormar_config.database.connect()
async with base_ormar_config.database.engine.begin() as conn:
await conn.run_sync(base_ormar_config.metadata.drop_all)
if base_ormar_config.database.is_connected:
await base_ormar_config.database.disconnect()
# gather and execute all functions
# note that normally you use gather() function to run several functions
# concurrently but we actually modify the data and we rely on the order of functions
for func in [
create,
read,
update,
delete,
joins,
filter_and_sort,
subset_of_columns,
pagination,
aggregations,
raw_data,
]:
print(f"Executing: {func.__name__}")
asyncio.run(with_connect(func))
# drop the database tables
asyncio.run(cleanup_database())
```
## Ormar Specification
### QuerySet methods
* `create(**kwargs): -> Model`
* `get(*args, **kwargs): -> Model`
* `get_or_none(*args, **kwargs): -> Optional[Model]`
* `get_or_create(_defaults: Optional[dict[str, Any]] = None, *args, **kwargs) -> tuple[Model, bool]`
* `first(*args, **kwargs): -> Model`
* `update(each: bool = False, **kwargs) -> int`
* `update_or_create(**kwargs) -> Model`
* `bulk_create(objects: list[Model]) -> None`
* `bulk_update(objects: list[Model], columns: list[str] = None) -> None`
* `delete(*args, each: bool = False, **kwargs) -> int`
* `all(*args, **kwargs) -> list[Optional[Model]]`
* `iterate(*args, **kwargs) -> AsyncGenerator[Model]`
* `filter(*args, **kwargs) -> QuerySet`
* `exclude(*args, **kwargs) -> QuerySet`
* `select_related(related: Union[list, str]) -> QuerySet`
* `prefetch_related(related: Union[list, str]) -> QuerySet`
* `limit(limit_count: int) -> QuerySet`
* `offset(offset: int) -> QuerySet`
* `count(distinct: bool = True) -> int`
* `exists() -> bool`
* `max(columns: list[str]) -> Any`
* `min(columns: list[str]) -> Any`
* `avg(columns: list[str]) -> Any`
* `sum(columns: list[str]) -> Any`
* `fields(columns: Union[list, str, set, dict]) -> QuerySet`
* `exclude_fields(columns: Union[list, str, set, dict]) -> QuerySet`
* `order_by(columns:Union[list, str]) -> QuerySet`
* `values(fields: Union[list, str, set, dict])`
* `values_list(fields: Union[list, str, set, dict])`
#### Relation types
* One to many - with `ForeignKey(to: Model)`
* Many to many - with `ManyToMany(to: Model, Optional[through]: Model)`
#### Model fields types
Available Model Fields (with required args - optional ones in docs):
* `String(max_length)`
* `Text()`
* `Boolean()`
* `Integer()`
* `Float()`
* `Date()`
* `Time()`
* `DateTime()`
* `JSON()`
* `BigInteger()`
* `SmallInteger()`
* `Decimal(scale, precision)`
* `UUID()`
* `LargeBinary(max_length)`
* `Enum(enum_class)`
* `Enum` like Field - by passing `choices` to any other Field type
* `EncryptedString` - by passing `encrypt_secret` and `encrypt_backend`
* `ForeignKey(to)`
* `ManyToMany(to)`
### Available fields options
The following keyword arguments are supported on all field types.
* `primary_key: bool`
* `nullable: bool`
* `default: Any`
* `server_default: Any`
* `on_update: Any`
* `index: bool`
* `unique: bool`
* `name: str`
All fields are required unless one of the following is set:
* `nullable` - Creates a nullable column. Sets the default to `False`. Read the fields common parameters for details.
* `sql_nullable` - Used to set different setting for pydantic and the database. Sets the default to `nullable` value. Read the fields common parameters for details.
* `default` - Set a default value for the field. **Not available for relation fields**
* `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.
### Available signals
Signals allow to trigger your function for a given event on a given Model.
* `pre_save`
* `post_save`
* `pre_update`
* `post_update`
* `pre_delete`
* `post_delete`
* `pre_relation_add`
* `post_relation_add`
* `pre_relation_remove`
* `post_relation_remove`
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
[sqlalchemy-async]: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
[pydantic]: https://pydantic-docs.helpmanual.io/
[encode/orm]: https://github.com/encode/orm/
[alembic]: https://alembic.sqlalchemy.org/en/latest/
[fastapi]: https://fastapi.tiangolo.com/
[documentation]: https://collerek.github.io/ormar/
[migrations]: https://collerek.github.io/ormar/models/migrations/
[asyncio]: https://docs.python.org/3/library/asyncio.html
[releases]: https://collerek.github.io/ormar/releases/
[tests]: https://github.com/collerek/ormar/tree/master/tests
ormar-orm-ormar-a7e7fde/docs/install.md 0000664 0000000 0000000 00000002064 15211567312 0020254 0 ustar 00root root 0000000 0000000 ## Installation
Installation is as simple as:
```py
pip install ormar
```
### Dependencies
Ormar uses SQLAlchemy async with `DatabaseConnection` for connectivity, `pydantic` for validation and `sqlalchemy-core` for queries.
Both should install along the installation of ormar if not present at your system before.
* 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:
### Database backend
#### Postgresql
```py
pip install ormar[postgresql]
```
Will install also `asyncpg` and `psycopg2`.
#### Mysql
```py
pip install ormar[mysql]
```
Will install also `aiomysql` and `pymysql`.
#### Sqlite
```py
pip install ormar[sqlite]
```
Will install also `aiosqlite`.
### Crypto
```py
pip install ormar[crypto]
```
Will install also `cryptography` that is required to work with encrypted columns.
### Manual installation of dependencies
Of course, you can also install these requirements manually with `pip install asyncpg` etc.
ormar-orm-ormar-a7e7fde/docs/migration.md 0000664 0000000 0000000 00000033376 15211567312 0020611 0 ustar 00root root 0000000 0000000 # Migration to 0.22.0 - DatabaseConnection
Version 0.22.0 migrates from the `databases` library to native async SQLAlchemy using ormar's `DatabaseConnection` wrapper. This provides better integration with SQLAlchemy's async ecosystem and improved transaction handling and avoid dependency on achived `databases` library.
## Breaking Changes
Migration to version >= 0.22.0 requires the following changes:
### 1. Import Change
Replace `databases` import with `DatabaseConnection` from ormar:
```python
# ormar < 0.22
import databases
database = databases.Database("sqlite:///db.sqlite")
# ormar >= 0.22
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
```
### 2. Database URL Format - Async Drivers Required
Database URLs must now use async drivers:
```python
# SQLite - add +aiosqlite
# OLD: "sqlite:///db.sqlite"
# NEW: "sqlite+aiosqlite:///db.sqlite"
# PostgreSQL - use +asyncpg
# OLD: "postgresql://user:pass@localhost/db"
# NEW: "postgresql+asyncpg://user:pass@localhost/db"
# MySQL - use +aiomysql
# OLD: "mysql://user:pass@localhost/db"
# NEW: "mysql+aiomysql://user:pass@localhost/db"
```
### 3. Engine Parameter No Longer Needed
The `engine` parameter in `OrmarConfig` is no longer required - it's created internally by `DatabaseConnection`:
```python
# ormar < 0.22
import databases
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
engine = sqlalchemy.create_engine("sqlite:///db.sqlite")
base_ormar_config = ormar.OrmarConfig(
database=database,
metadata=sqlalchemy.MetaData(),
engine=engine, # <- Remove this
)
# ormar >= 0.22
from ormar import DatabaseConnection
import sqlalchemy
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
base_ormar_config = ormar.OrmarConfig(
database=database,
metadata=sqlalchemy.MetaData(),
# engine is created internally
)
```
### 4. Table Creation
When creating tables, use a sync engine (async engines don't support metadata.create_all()):
```python
import sqlalchemy
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///db.sqlite"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
# Create a sync engine for table creation
sync_engine = sqlalchemy.create_engine(
DATABASE_URL.replace('+aiosqlite', '')
)
metadata.create_all(sync_engine)
```
### 5. Transaction API (Mostly Compatible)
The transaction API remains mostly the same, but now uses context variables and SQLAlchemy savepoints:
```python
# Both versions work the same way
async with database.transaction():
await Model.objects.create(...)
# Nested transactions use savepoints automatically
async with database.transaction():
await Model1.objects.create(...)
async with database.transaction(): # Uses savepoint
await Model2.objects.create(...)
# force_rollback for testing
async with database.transaction(force_rollback=True):
# Your test code - will rollback even on success
pass
# like in databases `force_rollback` can also be used with DatabaseConnection directly to use one global transaction.
```
### Complete Migration Example
```python
# Before (ormar < 0.22)
import databases
import sqlalchemy
import ormar
database = databases.Database("sqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
engine = sqlalchemy.create_engine("sqlite:///db.sqlite")
base_ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
engine=engine,
)
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
# Create tables
metadata.create_all(engine)
# After (ormar >= 0.22)
import sqlalchemy
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///db.sqlite"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
# Create tables with sync engine
sync_engine = sqlalchemy.create_engine(
DATABASE_URL.replace('+aiosqlite', '')
)
metadata.create_all(sync_engine)
```
### FastAPI Integration
The lifespan pattern remains compatible:
```python
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(_: FastAPI):
if not database.is_connected:
await database.connect()
yield
if database.is_connected:
await database.disconnect()
app = FastAPI(lifespan=lifespan)
```
### What Stays the Same
- Model definitions (no changes needed)
- Query API (all methods work the same)
- Field definitions (no changes needed)
- Relation handling (no changes needed)
- Signal decorators (no changes needed)
- Connection lifecycle management API (`connect()`, `disconnect()`, `is_connected`)
---
# 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
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. ormar-orm-ormar-a7e7fde/docs/models/ 0000775 0000000 0000000 00000000000 15211567312 0017545 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs/models/index.md 0000664 0000000 0000000 00000045402 15211567312 0021203 0 ustar 00root root 0000000 0000000 # Models
## Defining models
By defining an ormar Model you get corresponding **Pydantic model** as well as **Sqlalchemy table** for free.
They are being managed in the background and you do not have to create them on your own.
### Model Class
To build an ormar model you simply need to inherit a `ormar.Model` class.
```Python hl_lines="10"
--8<-- "../docs_src/models/docs001.py"
```
### Defining Fields
Next assign one or more of the [Fields][fields] as a class level variables.
#### Basic Field Types
Each table **has to** have a primary key column, which you specify by setting `primary_key=True` on selected field.
Only one primary key column is allowed.
```Python hl_lines="16-18"
--8<-- "../docs_src/models/docs001.py"
```
!!! warning
Not assigning `primary_key` column or assigning more than one column per `Model` will raise `ModelDefinitionError`
exception.
By default if you assign primary key to `Integer` field, the `autoincrement` option is set to true.
You can disable by passing `autoincrement=False`.
```Python
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 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 `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 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.
If you pass a value in `fastapi` `request` and return the same instance that `fastapi` constructs for you in `request_model`
you should get back exactly same value in `response`.).
!!!warning
pydantic fields have to be always **Optional** and it cannot be changed (otherwise db load validation would fail)
```Python hl_lines="20"
--8<-- "../docs_src/models/docs014.py"
```
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 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
# <==part of related code removed for clarity==>
base_ormar_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
class User(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="users2")
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255, nullable=False)
password: str = ormar.String(max_length=255)
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 = pydantic.Field(
default=datetime.datetime.now
)
# <==part of related code removed for clarity==>
app = FastAPI()
@app.post("/users/")
async def create_user(user: User):
return await user.save()
# <==part of related code removed for clarity==>
def test_excluding_fields_in_endpoints():
client = TestClient(app)
with client as client:
timestamp = datetime.datetime.now()
user = {
"email": "test@domain.com",
"password": "^*^%A*DA*IAAA",
"first_name": "John",
"last_name": "Doe",
"timestamp": str(timestamp),
}
response = client.post("/users/", json=user)
assert list(response.json().keys()) == [
"id",
"email",
"first_name",
"last_name",
"category",
"timestamp",
]
# returned is the same timestamp
assert response.json().get("timestamp") == str(timestamp).replace(" ", "T")
# <==part of related code removed for clarity==>
```
#### Fields names vs Column names
By default names of the fields will be used for both the underlying `pydantic` model and `sqlalchemy` table.
If for whatever reason you prefer to change the name in the database but keep the name in the model you can do this
with specifying `name` parameter during Field declaration
Here you have a sample model with changed names
```Python hl_lines="19-22"
--8<-- "../docs_src/models/docs008.py"
```
Note that you can also change the ForeignKey column name
```Python hl_lines="36"
--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="44"
--8<-- "../docs_src/models/docs010.py"
```
### 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 `ormar_config` of your class as `queryset_class` parameter.
```python
import ormar
from ormar.queryset.queryset import QuerySet
from fastapi import HTTPException
class MyQuerySetClass(QuerySet):
async def first_or_404(self, *args, **kwargs):
entity = await self.get_or_none(*args, **kwargs)
if entity is None:
# in fastapi or starlette
raise HTTPException(404)
class Book(ormar.Model):
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)
# when book not found, raise `404` in your view.
book = await Book.objects.first_or_404(name="123")
```
### Type Hints
Note that for better IDE support and mypy checks you can provide type hints.
```Python hl_lines="16-18"
--8<-- "../docs_src/models/docs001.py"
```
Note that type hints are **optional** so perfectly valid `ormar` code can look like this:
```Python hl_lines="16-18"
--8<-- "../docs_src/models/docs012.py"
```
!!!warning
Even if you use type hints **`ormar` does not use them to construct `pydantic` fields!**
Type hints are there only to support static checkers and linting,
`ormar` construct annotations used by `pydantic` from own fields.
### Dependencies
Since ormar depends on [SQLAlchemy async][sqlalchemy-async] and [`sqlalchemy-core`][sqlalchemy-core] for database connection
and table creation you need to assign each `Model` with two special parameters.
#### Database Connection
One is `DatabaseConnection` instance created with your database url in [sqlalchemy connection string][sqlalchemy connection string] format (with async driver).
Created instance needs to be passed to every `Model` with `ormar_config` object `database` parameter.
```Python hl_lines="4 6 12"
--8<-- "../docs_src/models/docs001.py"
```
!!! tip
You need to create the `DatabaseConnection` instance **only once** and use it for all models.
You can create several ones if you want to use multiple databases.
#### Sqlalchemy
Second dependency is sqlalchemy `MetaData` instance.
Created instance needs to be passed to every `Model` with `ormar_config` object `metadata` parameter.
```Python hl_lines="1 7 13"
--8<-- "../docs_src/models/docs001.py"
```
!!! tip
You need to create the `MetaData` instance **only once** and use it for all models.
You can create several ones if you want to use multiple databases.
#### Best practice
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.
```Python hl_lines="10-13 20 29"
--8<-- "../docs_src/models/docs013.py"
```
### Table Names
By default table name is created from Model class name as lowercase name plus 's'.
You can overwrite this parameter by providing `ormar_config` object's `tablename` argument.
```Python hl_lines="14-16"
--8<-- "../docs_src/models/docs002.py"
```
### Schemas
You can place a model in a non-default database schema by passing `schema=` to
`OrmarConfig` (or `OrmarConfig.copy()`). This lets you have multiple tables that
share a name as long as they live in different schemas, and lets you logically
group tables by domain.
```python
base = ormar.OrmarConfig(database=..., metadata=sqlalchemy.MetaData())
class HrEmployee(ormar.Model):
ormar_config = base.copy(tablename="employees", schema="hr")
id: int = ormar.Integer(primary_key=True)
class ItEmployee(ormar.Model):
ormar_config = base.copy(tablename="employees", schema="it")
id: int = ormar.Integer(primary_key=True)
```
`copy()` inherits the parent config's schema by default; pass `schema=None`
explicitly to opt out, or pass a different value to override.
#### Dialect support
| Dialect | Status | Notes |
|------------|-------------------------|-------|
| PostgreSQL | Supported | True schemas. Cross-schema foreign keys work. You must `CREATE SCHEMA` before `metadata.create_all()`. |
| MySQL | Supported with caveats | `schema` maps to a database name (qualified `db.tbl`). Cross-database FKs require both tables to be InnoDB. `DEFERRABLE`/`MATCH` are not allowed. You must `CREATE DATABASE` before `metadata.create_all()`. |
| SQLite | Not supported | SQLite forbids foreign keys whose parent and child live in different attached databases. Ormar raises `ModelDefinitionError` if it sees a cross-schema FK targeting SQLite. |
!!! warning
`metadata.create_all()` does **not** create the schemas themselves. You must
issue `CREATE SCHEMA ` (PostgreSQL) or `CREATE DATABASE ` (MySQL)
before tables can be created in those schemas.
### Table Comment
You can attach a SQL `COMMENT ON TABLE` value to a model by passing `comment=` to
`OrmarConfig` (or `OrmarConfig.copy()`). The value is forwarded to the underlying
SQLAlchemy `Table` and emitted by `metadata.create_all()` on dialects that
support table comments (PostgreSQL and MySQL; SQLite ignores it).
```python
class Book(ormar.Model):
ormar_config = base_ormar_config.copy(
tablename="books",
comment="Library catalogue entries.",
)
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
```
`copy()` inherits the parent config's comment by default; pass `comment=None`
explicitly to opt out, or pass a different value to override.
### Constraints
On a model level you can also set model-wise constraints on sql columns.
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.
!!!tip
To read more about columns constraints like `primary_key`, `unique`, `ForeignKey` etc. visit [fields][fields].
#### UniqueColumns
You can set this parameter by providing `ormar_config` object `constraints` argument.
```Python hl_lines="14-17"
--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.
#### IndexColumns
You can set this parameter by providing `ormar_config` object `constraints` argument.
```Python hl_lines="14-17"
--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.
#### CheckColumns
You can set this parameter by providing `ormar_config` object `constraints` argument.
```Python hl_lines="16-21"
--8<-- "../docs_src/models/docs018.py"
```
!!!note
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 `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 `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.
Note that if you do not provide your own configuration, ormar will do it for you.
The default config provided is as follows:
```python
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="17"
--8<-- "../docs_src/models/docs016.py"
```
### Extra fields in models
By default `ormar` forbids you to pass extra fields to Model.
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 `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, OrmarConfig
class Child(ormar.Model):
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 `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 `ormar_config` object.
Sample default ordering (not specified - so by primary key):
```python
base_ormar_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
# default sort by column id ascending
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(
tablename="authors",
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
```
Modified
```python hl_lines="9"
base_ormar_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
# now default sort by name descending
class Author(ormar.Model):
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)
```
## Model Initialization
There are two ways to create and persist the `Model` instance in the database.
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="30-31"
--8<-- "../docs_src/models/docs007.py"
```
If you want to initiate your `Model` and at the same time save in in the database use a QuerySet's method `create()`.
For creating multiple objects at once a `bulk_create()` QuerySet's method is available.
Each model has a `QuerySet` initialised as `objects` parameter
```Python hl_lines="33"
--8<-- "../docs_src/models/docs007.py"
```
!!!info
To read more about `QuerySets` (including bulk operations) and available methods visit [queries][queries]
## `Model` save status
Each model instance is a separate python object and they do not know anything about each other.
```python
track1 = await Track.objects.get(name='The Bird')
track2 = await Track.objects.get(name='The Bird')
assert track1 == track2 # True
track1.name = 'The Bird2'
await track1.save()
assert track1.name == track2.name # False
# track2 does not update and knows nothing about track1
```
The objects itself have a saved status, which is set as following:
* Model is saved after `save/update/load/upsert` method on model
* Model is saved after `create/get/first/all/get_or_create/update_or_create` method
* Model is saved when passed to `bulk_update` and `bulk_create`
* Model is saved after `adding/removing` `ManyToMany` related objects (through model instance auto saved/deleted)
* Model is **not** saved after change of any own field (including `pk` as `Model.pk` alias)
* Model is **not** saved after adding/removing `ForeignKey` related object (fk column not saved)
* Model is **not** saved after instantiation with `__init__` (w/o `QuerySet.create` or before calling `save`)
You can check if model is saved with `ModelInstance.saved` property
[fields]: ../fields/field-types.md
[relations]: ../relations/index.md
[queries]: ../queries/index.md
[pydantic]: https://pydantic-docs.helpmanual.io/
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
[sqlalchemy-metadata]: https://docs.sqlalchemy.org/en/13/core/metadata.html
[sqlalchemy-async]: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
[sqlalchemy connection string]: https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
[sqlalchemy table creation]: https://docs.sqlalchemy.org/en/13/core/metadata.html#creating-and-dropping-database-tables
[alembic]: https://alembic.sqlalchemy.org/en/latest/tutorial.html
[save status]: ../models/index/#model-save-status
[Internals]: ../models/internals.md
ormar-orm-ormar-a7e7fde/docs/models/inheritance.md 0000664 0000000 0000000 00000045641 15211567312 0022372 0 ustar 00root root 0000000 0000000 # Inheritance
Out of various types of ORM models inheritance `ormar` currently supports three of them:
* **Mixins**
* **Concrete table inheritance** (with parents set to `abstract=True`)
* **Proxy models** (with children set to `proxy=True`)
## Types of inheritance
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),
only actual models create tables, but those fields from mixins are added
* **Concrete table inheritance [SUPPORTED]** - means that parent is marked as abstract
and each child has its own table with columns from a parent and own child columns, kind
of similar to Mixins but parent also is a Model
* **Proxy models [SUPPORTED]** - means that only parent has an actual table,
children just add methods, modify settings etc. and share the parent's table
* **Single table inheritance [NOT SUPPORTED]** - means that only one table is created
with fields that are combination/sum of the parent and all children models but child
models use only subset of column in db (all parent and own ones, skipping the other
children ones)
* **Multi/ Joined table inheritance [NOT SUPPORTED]** - means that part of the columns
is saved on parent model and part is saved on child model that are connected to each
other by kind of one to one relation and under the hood you operate on two models at
once
## Mixins
To use Mixins just define a class that is not inheriting from an `ormar.Model` but is
defining `ormar.Fields` as class variables.
```python
base_ormar_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
# a mixin defines the fields but is a normal python class
class AuditMixin:
created_by: str = ormar.String(max_length=100)
updated_by: str = ormar.String(max_length=100, default="Sam")
class DateFieldsMixins:
created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
updated_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
# a models can inherit from one or more mixins
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()
```
!!!tip
Note that Mixins are **not** models, so you still need to inherit
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`.
There will be only one table created for model `Category` (`categories`), with `Category` class fields
combined with all `Mixins` fields.
Note that `Mixin` in class name is optional but is a good python practice.
## Concrete table inheritance
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 `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 `ormar_config` definition.
But if you provide it - it will be inherited, that way you do not have to
provide `metadata` and `database` in the final/concrete class
Note that you can always overwrite it in child/concrete class if you need to.
More over at least one of the classes in inheritance chain have to provide both `database` and `metadata` -
otherwise an error will be raised.
```python
# note that base classes have abstract=True
# since this model will never be initialized you can skip metadata and database
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")
# but if you provide it it will be inherited - DRY (Don't Repeat Yourself) in action
class DateFieldsModel(ormar.Model):
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)
# that way you do not have to provide metadata and database in concrete class
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()
```
The list of inherited options/settings is as follows: `metadata`, `database`
and `constraints`.
Of course apart from that all fields from base classes are combined and created in the
concrete table of the final Model.
!!!tip
Note how you don't have to provide `abstarct=False` in the final class - it's the default setting
that is not inherited.
## Redefining fields in subclasses
Note that you can redefine previously created fields like in normal python class
inheritance.
Whenever you define a field with same name and new definition it will completely replace
the previously defined one.
```python hl_lines="28"
# base class
class DateFieldsModel(ormar.Model):
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")]
)
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 RedefinedField(DateFieldsModel):
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
created_date: str = ormar.String(max_length=200, name="creation_date")
# you can verify that the final field is correctly declared and created
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.ormar_config.table.columns)
assert isinstance(
RedefinedField.ormar_config.table.columns["creation_date"].type,
sqlalchemy.sql.sqltypes.String,
)
```
!!!warning
If you declare `UniqueColumns` constraint with column names, the final model **has to have**
a column with the same name declared. Otherwise, the `ModelDefinitionError` will be raised.
So in example above if you do not provide `name` for `created_date` in `RedefinedField` model
ormar will complain.
`created_date: str = ormar.String(max_length=200) # exception`
`created_date: str = ormar.String(max_length=200, name="creation_date2") # exception`
## Proxy models
A proxy model is a child class that **shares its parent's table** and only adds
methods, computed fields, or a custom `queryset_class`. Use it when you want
multiple Python views over the same underlying table, for example to attach
domain methods to a generic record type.
To declare a proxy model, set `proxy=True` on the child's `OrmarConfig`:
```python
base_ormar_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
class Human(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="humans")
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=50)
last_name: str = ormar.String(max_length=50)
class User(Human):
ormar_config = base_ormar_config.copy(proxy=True)
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
```
Both `Human.objects.all()` and `User.objects.all()` read from the same `humans`
table; the only difference is that `User` rows are returned as `User` instances
and gain the `full_name()` method.
### Constraints
* The base class must be a non-abstract `ormar.Model`. Proxying an
`abstract=True` parent raises `ModelDefinitionError` — use concrete
inheritance for that.
* A proxy model **cannot** declare new ormar fields. Doing so raises
`ModelDefinitionError` because the schema is fixed by the parent.
* `proxy=True` and `abstract=True` are mutually exclusive on the same class.
* Proxy chains (e.g. `Admin(User)` with `proxy=True` where `User` itself is a
proxy of `Human`) are allowed and resolve back to the root concrete table.
* All concrete ormar bases of a proxy must share the same table.
### Behavior notes
* Each proxy class has its own `SignalEmitter`, so a `pre_save` registered on
`User` does not fire when a `Human` instance is saved (and vice versa).
Set `emit_parent_signals=True` on the proxy's `OrmarConfig` to also dispatch
the parent's `pre_save` / `post_save` / `pre_update` / `post_update` /
`pre_delete` / `post_delete` handlers (with `sender=parent_cls`) on every
save / update / delete via the proxy. The flag is opt-in to preserve the
contract of existing handlers that assume the instance is the parent type.
* Reverse relations defined on related models still point at the original
parent class — proxying does not create new reverse accessors.
* The proxy's `queryset_class` and `extra` settings can be overridden via
`OrmarConfig.copy(proxy=True, queryset_class=...)` independently of the
parent.
## Relations in inheritance
You can declare relations in every step of inheritance, so both in parent and child
classes.
When you define a relation on a child model level it's either overwriting the relation
defined in parent model (if the same field name is used), or is accessible only to this
child if you define a new relation.
When inheriting relations, you always need to be aware of `related_name` parameter, that
has to be unique across a related model, when you define multiple child classes that inherit the
same relation.
If you do not provide `related_name` parameter ormar calculates it for you. This works
with inheritance as all child models have to have different class names, which are used
to calculate the default `related_name` (class.name.lower()+'s').
But, if you provide a `related_name` this name cannot be reused in all child models as
they would overwrite each other on the related model side.
Therefore, you have two options:
* redefine relation field in child models and manually provide different `related_name`
parameters
* let this for `ormar` to handle -> auto adjusted related_name are: original
related_name + "_" + child model **table** name
That might sound complicated but let's look at the following example:
### ForeignKey relations
```python
# normal model used in relation
class Person(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
# parent model - needs to be abstract
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)
# note that we refer to the Person model again so we **have to** provide related_name
co_owner: Person = ormar.ForeignKey(Person, related_name="coowned")
created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
class Truck(Car):
ormar_config = base_ormar_config.copy()
max_capacity: int = ormar.Integer()
class Bus(Car):
ormar_config = base_ormar_config.copy(tablename="buses")
max_persons: int = ormar.Integer()
```
Now when you will inspect the fields on Person model you will get:
```python
Person.ormar_config.model_fields
"""
{'id': ,
'name': ,
'trucks': ,
'coowned_trucks': ,
'buss': ,
'coowned_buses': }
"""
```
Note how you have `trucks` and `buss` fields that leads to Truck and Bus class that
this Person owns. There were no `related_name` parameter so default names were used.
At the same time the co-owned cars need to be referenced by `coowned_trucks`
and `coowned_buses`. Ormar appended `_trucks` and `_buses` suffixes taken from child
model table names.
Seems fine, but the default name for owned trucks is ok (`trucks`) but the `buss` is
ugly, so how can we change it?
The solution is pretty simple - just redefine the field in Bus class and provide
different `related_name` parameter.
```python
# rest of the above example remains the same
class Bus(Car):
ormar_config = base_ormar_config.copy(tablename="buses")
# new field that changes the related_name
owner: Person = ormar.ForeignKey(Person, related_name="buses")
max_persons: int = ormar.Integer()
```
Now the columns looks much better.
```python
Person.ormar_config.model_fields
"""
{'id': ,
'name': ,
'trucks': ,
'coowned_trucks': ,
'buses': ,
'coowned_buses': }
"""
```
!!!note
You could also provide `related_name` for the `owner` field, that way the proper suffixes
would be added.
`owner: Person = ormar.ForeignKey(Person, related_name="owned")`
and model fields for Person owned cars would become `owned_trucks` and `owned_buses`.
### ManyToMany relations
Similarly, you can inherit from Models that have ManyToMany relations declared but
there is one, but substantial difference - the Through model.
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.
Of course, you can overwrite the relation in each Child model, but that requires additional
code and undermines the point of the whole inheritance. `Ormar` will handle this for you if
you agree with default naming convention, which you can always manually overwrite in
children if needed.
Again, let's look at the example to easier grasp the concepts.
We will modify the previous example described above to use m2m relation for co_owners.
```python
# person remain the same as above
class Person(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="cars_x_persons")
# note how co_owners is now ManyToMany relation
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)
# note the related_name - needs to be unique across Person
# model, regardless of how many different models leads to Person
owner: Person = ormar.ForeignKey(Person, related_name="owned")
co_owners: list[Person] = ormar.ManyToMany(
Person, through=PersonsCar, related_name="coowned"
)
created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
# child models define only additional Fields
class Truck2(Car2):
ormar_config = base_ormar_config.copy(tablename="trucks2")
max_capacity: int = ormar.Integer()
class Bus2(Car2):
ormar_config = base_ormar_config.copy(tablename="buses2")
max_persons: int = ormar.Integer()
```
`Ormar` automatically modifies related_name of the fields to include the **table** name
of the children models. The default name is original related_name + '_' + child table name.
That way for class Truck2 the relation defined in
`owner: Person = ormar.ForeignKey(Person, related_name="owned")` becomes `owned_trucks2`
You can verify the names by inspecting the list of fields present on `Person` model.
```python
Person.ormar_config.model_fields
{
# note how all relation fields need to be unique on Person
# regardless if autogenerated or manually overwritten
'id': ,
'name': ,
# note that we expanded on previous example so all 'old' fields are here
'trucks': ,
'coowned_trucks': ,
'buses': ,
'coowned_buses': ,
# newly defined related fields
'owned_trucks2': ,
'coowned_trucks2': ,
'owned_buses2': ,
'coowned_buses2':
}
```
But that's not all. It's kind of internal to `ormar` but affects the data structure in the database,
so let's examine the through models for both `Bus2` and `Truck2` models.
```python
Bus2.ormar_config.model_fields['co_owners'].through
Bus2.ormar_config.model_fields['co_owners'].through.ormar_config.tablename
'cars_x_persons_buses2'
Truck2.ormar_config.model_fields['co_owners'].through
Truck2.ormar_config.model_fields['co_owners'].through.ormar_config.tablename
'cars_x_persons_trucks2'
```
As you can see above `ormar` cloned the Through model for each of the Child classes and added
Child **class** name at the end, while changing the table names of the cloned fields
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.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'])
```
So be aware that if you introduce inheritance along the way and convert a model into
abstract parent model you may lose your data on through table if not careful.
!!!note
Note that original table name and model name of the Through model is never used.
Only the cloned models tables are created and used.
!!!warning
Note that each subclass of the Model that has `ManyToMany` relation defined generates
a new `Through` model, meaning also **new database table**.
That means that each time you define a Child model you need to either manually create
the table in the database, or run a migration (with alembic).
ormar-orm-ormar-a7e7fde/docs/models/internals.md 0000664 0000000 0000000 00000004373 15211567312 0022075 0 ustar 00root root 0000000 0000000 # Internals
Apart from special parameters defined in the `Model` during definition (tablename, metadata etc.) the `Model` provides you with useful internals.
## Pydantic Model
All `Model` classes inherit from `pydantic.BaseModel` so you can access all normal attributes of pydantic models.
For example to list pydantic model fields you can:
```Python hl_lines="21"
--8<-- "../docs_src/models/docs003.py"
```
!!!tip
Note how the primary key `id` field is optional as `Integer` primary key by default has `autoincrement` set to `True`.
!!!info
For more options visit official [pydantic][pydantic] documentation.
## Sqlalchemy Table
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="25"
--8<-- "../docs_src/models/docs004.py"
```
!!!tip
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.ormar_config.model_fields` parameter
For example to list table model fields you can:
```Python hl_lines="23"
--8<-- "../docs_src/models/docs005.py"
```
!!!info
Note that fields stored on a model are `classes` not `instances`.
So if you print just model fields you will get:
`{'id': , `
`'name': , `
`'completed': }`
[fields]: ./fields.md
[relations]: ./relations/index.md
[queries]: ./queries.md
[pydantic]: https://pydantic-docs.helpmanual.io/
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
[sqlalchemy-metadata]: https://docs.sqlalchemy.org/en/13/core/metadata.html
[sqlalchemy-async]: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
[sqlalchemy connection string]: https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
[sqlalchemy table creation]: https://docs.sqlalchemy.org/en/13/core/metadata.html#creating-and-dropping-database-tables
[alembic]: https://alembic.sqlalchemy.org/en/latest/tutorial.html
[save status]: ../models/#model-save-status
[Internals]: #internals
ormar-orm-ormar-a7e7fde/docs/models/methods.md 0000664 0000000 0000000 00000057620 15211567312 0021544 0 ustar 00root root 0000000 0000000 # Model methods
!!!tip
Main interaction with the databases is exposed through a `QuerySet` object exposed on
each model as `Model.objects` similar to the django orm.
To read more about **quering, joining tables, excluding fields etc. visit [queries][queries] section.**
Each model instance have a set of methods to `save`, `update` or `load` itself.
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 `model_dump()` and `model_dump_json()` methods that can also accept `exclude`, `include` and other parameters.
To read more check [pydantic][pydantic] documentation
## model_construct()
`model_construct` is a raw equivalent of `__init__` method used for construction of new instances.
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.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:
* Providing a `default` value for not set fields
* Initialize nested ormar models if you pass a dictionary or a primary key value
## model_dump()
`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.
`model_dump` as the name suggests export data from model tree to dictionary.
Explanation of model_dump parameters:
### include (`ormar` modified)
`include: Union[set, dict] = None`
Set or dictionary of field names to include in returned dictionary.
Note that `pydantic` has an uncommon pattern of including/ excluding fields in lists (so also nested models) by an index.
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 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!
### exclude (`ormar` modified)
`exclude: Union[set, dict] = None`
Set or dictionary of field names to exclude in returned dictionary.
Note that `pydantic` has an uncommon pattern of including/ excluding fields in lists (so also nested models) by an index.
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"}`)
`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!
### exclude_unset
`exclude_unset: bool = False`
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!
```python
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, default="Test")
visibility: bool = ormar.Boolean(default=True)
class Item(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
price: float = ormar.Float(default=9.99)
categories: list[Category] = ormar.ManyToMany(Category)
category = Category(name="Test 2")
assert category.model_dump() == {'id': None, 'items': [], 'name': 'Test 2',
'visibility': True}
assert category.model_dump(exclude_unset=True) == {'items': [], 'name': 'Test 2'}
await category.save()
category2 = await Category.objects.get()
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.model_dump(exclude_unset=True) == {'id': 1, 'items': [],
'name': 'Test 2', 'visibility': True}
```
### exclude_defaults
`exclude_defaults: bool = False`
Flag indicates are equal to their default values (whether set or otherwise) should be excluded from the returned dictionary
```python
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, default="Test")
visibility: bool = ormar.Boolean(default=True)
class Item(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
price: float = ormar.Float(default=9.99)
categories: list[Category] = ormar.ManyToMany(Category)
category = Category()
# note that Integer pk is by default autoincrement so optional
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.model_dump() == {'id': 1, 'items': [], 'name': 'Test', 'visibility': True}
assert category2.model_dump(exclude_defaults=True) == {'id': 1, 'items': []}
```
### exclude_none
`exclude_none: bool = False`
Flag indicates whether fields which are equal to `None` should be excluded from the returned dictionary.
```python
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, default="Test", nullable=True)
visibility: bool = ormar.Boolean(default=True)
class Item(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
price: float = ormar.Float(default=9.99)
categories: list[Category] = ormar.ManyToMany(Category)
category = Category(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.model_dump(exclude_none=True) == {'items': [], 'visibility': True}
await category.save()
category2 = await Category.objects.get()
assert category2.model_dump() == {'id': 1, 'items': [], 'name': None,
'visibility': True}
assert category2.model_dump(exclude_none=True) == {'id': 1, 'items': [],
'visibility': True}
```
### exclude_primary_keys (`ormar` only)
`exclude_primary_keys: bool = False`
Setting flag to `True` will exclude all primary key columns in a tree, including nested models.
```python
class Item(ormar.Model):
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.model_dump() == {"id": 1, "name": "Test Item"}
assert item1.model_dump(exclude_primary_keys=True) == {"name": "Test Item"}
```
### exclude_through_models (`ormar` only)
`exclude_through_models: bool = False`
`Through` models are auto added for every `ManyToMany` relation, and they hold additional parameters on linking model/table.
Setting the `exclude_through_models=True` will exclude all through models, including Through models of submodels.
```python
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)
categories: list[Category] = ormar.ManyToMany(Category)
# tree defining the models
item_dict = {
"name": "test",
"categories": [{"name": "test cat"}, {"name": "test cat2"}],
}
# save whole tree
await Item(**item_dict).save_related(follow=True, save_all=True)
# get the saved values
item = await Item.objects.select_related("categories").get()
# by default you can see the through models (itemcategory)
assert item.model_dump() == {'id': 1, 'name': 'test',
'categories': [
{'id': 1, 'name': 'test cat',
'itemcategory': {'id': 1, 'category': None, 'item': None}},
{'id': 2, 'name': 'test cat2',
'itemcategory': {'id': 2, 'category': None, 'item': None}}
]}
# you can exclude those fields/ models
assert item.model_dump(exclude_through_models=True) == {
'id': 1, 'name': 'test',
'categories': [
{'id': 1, 'name': 'test cat'},
{'id': 2, 'name': 'test cat2'}
]}
```
## model_dump_json()
`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(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!** 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
Given sample ormar models like follows:
```python
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=DatabaseConnection(DATABASE_URL),
)
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)
```
You can generate pydantic models out of it with a one simple call.
```python
PydanticCategory = Category.get_pydantic(include={"id", "name"})
```
Which will generate model equivalent of:
```python
class Category(BaseModel):
id: Optional[int]
name: Optional[str] = "test"
```
!!!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)`.
To exclude or include nested fields you can use dict or double underscores.
```python
# both calls are equivalent
PydanticCategory = Category.get_pydantic(include={"id", "items__id"})
PydanticCategory = Category.get_pydantic(include={"id": ..., "items": {"id"}})
```
and results in a generated structure as follows:
```python
class Item(BaseModel):
id: Optional[int]
class Category(BaseModel):
id: Optional[int]
items: Optional[list[Item]]
```
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.
Methods decorated with `@pydantic.computed_field` are also carried over to the generated model and follow the same `include` / `exclude` rules as regular fields, so they appear in the model's schema and `model_dump()` output (e.g. in `fastapi` responses).
## load()
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).
```python
track = await Track.objects.get(name='The Bird')
track.album.pk # will return malibu album pk (1)
track.album.name # will return None
# you need to actually load the data first
await track.album.load()
track.album.name # will return 'Malibu'
```
## 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.
But you can specify the `follow=True` parameter to traverse through nested models and load all of them in the relation tree.
!!!warning
To avoid circular updates with `follow=True` set, `load_all` keeps a set of already visited Models,
and won't perform nested `loads` on Models that were already visited.
So if you have a diamond or circular relations types you need to perform the loads in a manual way.
```python
# in example like this the second Street (coming from City) won't be load_all, so ZipCode won't be reloaded
Street -> District -> City -> Street -> ZipCode
```
Method accepts also optional exclude parameter that works exactly the same as exclude_fields method in `QuerySet`.
That way you can remove fields from related models being refreshed or skip whole related models.
Method performs one database query so it's more efficient than nested calls to `load()` and `all()` on related models.
!!!tip
To read more about `exclude` read [exclude_fields][exclude_fields]
!!!warning
All relations are cleared on `load_all()`, so if you exclude some nested models they will be empty after call.
## save()
`save() -> self`
You can create new models by using `QuerySet.create()` method or by initializing your model as a normal pydantic model
and later calling `save()` method.
`save()` can also be used to persist changes that you made to the model, but only if the primary key is not set or the model does not exist in database.
The `save()` method does not check if the model exists in db, so if it does you will get a integrity error from your selected db backend if trying to save model with already existing primary key.
```python
track = Track(name='The Bird')
await track.save() # will persist the model in database
track = await Track.objects.get(name='The Bird')
await track.save() # will raise integrity error as pk is populated
```
## update()
`update(_columns: list[str] = None, **kwargs) -> self`
You can update models by using `QuerySet.update()` method or by updating your model attributes (fields) and calling `update()` method.
If you try to update a model without a primary key set a `ModelPersistenceError` exception will be thrown.
To persist a newly created model use `save()` or `upsert(**kwargs)` methods.
```python
track = await Track.objects.get(name='The Bird')
await track.update(name='The Bird Strikes Again')
```
To update only selected columns from model into the database provide a list of columns that should be updated to `_columns` argument.
In example:
```python hl_lines="16"
class Movie(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, nullable=False, name="title")
year: int = ormar.Integer()
profit: float = ormar.Float()
terminator = await Movie(name='Terminator', year=1984, profit=0.078).save()
terminator.name = "Terminator 2"
terminator.year = 1991
terminator.profit = 0.520
# update only name
await terminator.update(_columns=["name"])
# note that terminator instance was not reloaded so
assert terminator.year == 1991
# but once you load the data from db you see it was not updated
await terminator.load()
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!
## upsert()
`upsert(**kwargs) -> self`
It's a proxy to either `save()` or `update(**kwargs)` methods described above.
If the primary key is set -> the `update` method will be called.
If the pk is not set the `save()` method will be called.
```python
track = Track(name='The Bird')
await track.upsert() # will call save as the pk is empty
track = await Track.objects.get(name='The Bird')
await track.upsert(name='The Bird Strikes Again') # will call update as pk is already populated
```
## delete()
You can delete models by using `QuerySet.delete()` method or by using your model and calling `delete()` method.
```python
track = await Track.objects.get(name='The Bird')
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(follow: bool = False, save_all: bool = False, exclude=Optional[Union[set, dict]]) -> None`
Method goes through all relations of the `Model` on which the method is called,
and calls `upsert()` method on each model that is **not** saved.
To understand when a model is saved check [save status][save status] section above.
By default the `save_related` method saved 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 save all of them in the relation tree.
By default save_related saves only model that has not `saved` status, meaning that they were modified in current scope.
If you want to force saving all of the related methods use `save_all=True` flag, which will upsert all related models, regardless of their save status.
If you want to skip saving some of the relations you can pass `exclude` parameter.
`Exclude` can be a set of own model relations,
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
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,
and won't perform nested `save_related` on Models that were already visited.
So if you have circular relations types you need to perform the updates in a manual way.
Note that with `save_all=True` and `follow=True` you can use `save_related()` to save whole relation tree at once.
Example:
```python
class Department(ormar.Model):
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):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
course_name: str = ormar.String(max_length=100)
completed: bool = ormar.Boolean()
department: Optional[Department] = ormar.ForeignKey(Department)
class Student(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
courses = ormar.ManyToMany(Course)
to_save = {
"department_name": "Ormar",
"courses": [
{"course_name": "basic1",
"completed": True,
"students": [
{"name": "Jack"},
{"name": "Abi"}
]},
{"course_name": "basic2",
"completed": True,
"students": [
{"name": "Kate"},
{"name": "Miranda"}
]
},
],
}
# initialize whole tree
department = Department(**to_save)
# save all at once (one after another)
await department.save_related(follow=True, save_all=True)
department_check = await Department.objects.select_all(follow=True).get()
to_exclude = {
"id": ...,
"courses": {
"id": ...,
"students": {"id", "studentcourse"}
}
}
# after excluding ids and through models you get exact same payload used to
# construct whole tree
assert department_check.model_dump(exclude=to_exclude) == to_save
```
!!!warning
`save_related()` iterates all relations and all models and upserts() them one by one,
so it will save all models but might not be optimal in regard of number of database queries.
[fields]: ../fields.md
[relations]: ../relations/index.md
[queries]: ../queries/index.md
[pydantic]: https://pydantic-docs.helpmanual.io/
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
[sqlalchemy-metadata]: https://docs.sqlalchemy.org/en/13/core/metadata.html
[sqlalchemy-async]: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
[sqlalchemy connection string]: https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
[sqlalchemy table creation]: https://docs.sqlalchemy.org/en/13/core/metadata.html#creating-and-dropping-database-tables
[alembic]: https://alembic.sqlalchemy.org/en/latest/tutorial.html
[save status]: ../models/index/#model-save-status
[Internals]: #internals
[exclude_fields]: ../queries/select-columns.md#exclude_fields
ormar-orm-ormar-a7e7fde/docs/models/migrations.md 0000664 0000000 0000000 00000016277 15211567312 0022260 0 ustar 00root root 0000000 0000000 # Migrations
## Database Initialization
Note that all examples assume that you already have a database.
If that is not the case and you need to create your tables, that's super easy as `ormar` is using sqlalchemy for underlying table construction.
All you have to do is call `create_all()` like in the example below.
```python
import sqlalchemy
# get your database url in sqlalchemy format - same as used with DatabaseConnection in Model definition
engine = sqlalchemy.create_engine("sqlite:///test.db")
# note that this has to be the same metadata that is used in ormar Models definition
metadata.create_all(engine)
```
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 DatabaseConnection in Model definition
engine = sqlalchemy.create_engine("sqlite:///test.db")
# Artist is an ormar model from previous examples
Artist.ormar_config.table.create(engine)
```
!!!warning
You need to create the tables only once, so use a python console for that or remove the script from your production code after first use.
## Alembic usage
Likewise as with tables, since we base tables on sqlalchemy for migrations please use [alembic][alembic].
### Initialization
Use command line to reproduce this minimalistic example.
```python
alembic init alembic
alembic revision --autogenerate -m "made some changes"
alembic upgrade head
```
### Sample env.py file
A quick example of alembic migrations should be something similar to:
When you have application structure like:
```
-> app
-> alembic (initialized folder - so run alembic init alembic inside app folder)
-> models (here are the models)
-> __init__.py
-> my_models.py
```
Your `env.py` file (in alembic folder) can look something like:
```python
from logging.config import fileConfig
from sqlalchemy import create_engine
from alembic import context
import sys, os
# add app folder to system path (alternative is running it from parent folder with python -m ...)
myPath = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, myPath + '/../../')
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here (the one used in ormar)
# for 'autogenerate' support
from app.models.my_models import metadata
target_metadata = metadata
# set your url here or import from settings
# note that by default url is in saved sqlachemy.url variable in alembic.ini file
URL = "sqlite:///test.db"
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
context.configure(
url=URL,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
# if you use UUID field set also this param
# the prefix has to match sqlalchemy import name in alembic
# that can be set by sqlalchemy_module_prefix option (default 'sa.')
user_module_prefix='sa.'
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = create_engine(URL)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
# if you use UUID field set also this param
# the prefix has to match sqlalchemy import name in alembic
# that can be set by sqlalchemy_module_prefix option (default 'sa.')
user_module_prefix='sa.'
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
```
### Detecting column type changes (`compare_type`)
Alembic's `--autogenerate` does **not** compare column types by default — it only
notices added/dropped columns and changes to nullability/server defaults. Swapping
`ormar.DateTime()` for `ormar.DateTime(timezone=True)`, widening `ormar.String(max_length=...)`,
or changing numeric precision will therefore produce an empty migration unless you opt in.
Pass `compare_type=True` to `context.configure(...)` in both `run_migrations_offline`
and `run_migrations_online`:
```python
context.configure(
connection=connection,
target_metadata=target_metadata,
user_module_prefix='sa.',
compare_type=True,
)
```
Ormar already forwards `timezone` (and other column kwargs) to the underlying SQLAlchemy
column, so once `compare_type=True` is set, type-only changes show up in autogenerated
revisions like any other change.
!!!info
See the Alembic docs on
[comparing types](https://alembic.sqlalchemy.org/en/latest/autogenerate.html#comparing-types)
for the full list of caveats (some dialect-specific type swaps still need a custom
`compare_type` callable).
### Excluding tables
You can also include/exclude specific tables with `include_object` parameter passed to `context.configure`. That should be a function returning `True/False` for given objects.
A sample function excluding tables starting with `data_` in name unless it's 'data_jobs':
```python
def include_object(object, name, type_, reflected, compare_to):
if name and name.startswith('data_') and name not in ['data_jobs']:
return False
return True
```
!!!note
Function parameters for `include_objects` (you can change the name) are required and defined in alembic
to check what they do check the [alembic][alembic] documentation
And you pass it into context like (both in online and offline):
```python
context.configure(
url=URL,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
user_module_prefix='sa.',
include_object=include_object
)
```
!!!info
You can read more about table creation, altering and migrations in [sqlalchemy table creation][sqlalchemy table creation] documentation.
[fields]: ./fields.md
[relations]: ./relations/index.md
[queries]: ./queries.md
[pydantic]: https://pydantic-docs.helpmanual.io/
[sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/
[sqlalchemy-metadata]: https://docs.sqlalchemy.org/en/13/core/metadata.html
[sqlalchemy-async]: https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html
[sqlalchemy connection string]: https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
[sqlalchemy table creation]: https://docs.sqlalchemy.org/en/13/core/metadata.html#creating-and-dropping-database-tables
[alembic]: https://alembic.sqlalchemy.org/en/latest/tutorial.html
[save status]: ../models/index/#model-save-status
[Internals]: #internals
ormar-orm-ormar-a7e7fde/docs/mypy.md 0000664 0000000 0000000 00000000771 15211567312 0017607 0 ustar 00root root 0000000 0000000 To provide better errors check you should use mypy with pydantic [plugin][plugin]
Please use notation introduced in version 0.4.0.
```Python hl_lines="16-18"
--8<-- "../docs_src/models/docs012.py"
```
Note that above example is not using the type hints, so further operations with mypy might fail, depending on the context.
Preferred notation should look liked this:
```Python hl_lines="16-18"
--8<-- "../docs_src/models/docs001.py"
```
[plugin]: https://pydantic-docs.helpmanual.io/mypy_plugin/ ormar-orm-ormar-a7e7fde/docs/plugin.md 0000664 0000000 0000000 00000001471 15211567312 0020105 0 ustar 00root root 0000000 0000000 While `ormar` will work with any IDE there is a PyCharm `pydantic` plugin that enhances the user experience for this IDE.
Plugin is available on the JetBrains Plugins Repository for PyCharm: [plugin page][plugin page].
You can install the plugin for free from the plugin marketplace
(PyCharm's Preferences -> Plugin -> Marketplace -> search "pydantic").
!!!note
For plugin to work properly you need to provide valid type hints for model fields.
!!!info
Plugin supports type hints, argument inspection and more but mainly only for __init__ methods
More information can be found on the
[official plugin page](https://plugins.jetbrains.com/plugin/12861-pydantic)
and [github repository](https://github.com/koxudaxi/pydantic-pycharm-plugin).
[plugin page]: https://plugins.jetbrains.com/plugin/12861-pydantic ormar-orm-ormar-a7e7fde/docs/queries/ 0000775 0000000 0000000 00000000000 15211567312 0017737 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs/queries/aggregations.md 0000664 0000000 0000000 00000024061 15211567312 0022736 0 ustar 00root root 0000000 0000000 # Aggregation functions
Currently 6 aggregation functions are supported.
* `count(distinct: bool = True) -> int`
* `exists() -> bool`
* `sum(columns) -> Any`
* `avg(columns) -> Any`
* `min(columns) -> Any`
* `max(columns) -> Any`
* `QuerysetProxy`
* `QuerysetProxy.count(distinct=True)` method
* `QuerysetProxy.exists()` method
* `QuerysetProxy.sum(columns)` method
* `QuerysetProxy.avg(columns)` method
* `QuerysetProxy.min(column)` method
* `QuerysetProxy.max(columns)` method
## count
`count(distinct: bool = True) -> int`
Returns number of rows matching the given criteria (i.e. applied with `filter` and `exclude`).
If `distinct` is `True` (the default), this will return the number of primary rows selected. If `False`,
the count will be the total number of rows returned
(including extra rows for `one-to-many` or `many-to-many` left `select_related` table joins).
`False` is the legacy (buggy) behavior for workflows that depend on it.
```python
class Book(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
tablename="book"
)
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"],
)
```
```python
# returns count of rows in db for Books model
no_of_books = await Book.objects.count()
```
## exists
`exists() -> bool`
Returns a bool value to confirm if there are rows matching the given criteria (applied with `filter` and `exclude`)
```python
class Book(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
tablename="book"
)
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"],
)
```
```python
# returns a boolean value if given row exists
has_sample = await Book.objects.filter(title='Sample').exists()
```
## sum
`sum(columns) -> Any`
Returns sum value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).
You can pass one or many column names including related columns.
As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)
You cannot `sum` non numeric columns.
If you aggregate on one column, the single value is directly returned as a result
If you aggregate on multiple columns a dictionary with column: result pairs is returned
Given models like follows
```Python
--8<-- "../docs_src/aggregations/docs001.py"
```
A sample usage might look like following
```python
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()
assert await Book.objects.sum("year") == 5773
result = await Book.objects.sum(["year", "ranking"])
assert result == dict(year=5773, ranking=9)
try:
# cannot sum string column
await Book.objects.sum("title")
except ormar.QueryDefinitionError:
pass
assert await Author.objects.select_related("books").sum("books__year") == 5773
result = await Author.objects.select_related("books").sum(
["books__year", "books__ranking"]
)
assert result == dict(books__year=5773, books__ranking=9)
assert (
await Author.objects.select_related("books")
.filter(books__year__lt=1925)
.sum("books__year")
== 3843
)
```
## avg
`avg(columns) -> Any`
Returns avg value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).
You can pass one or many column names including related columns.
As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)
You cannot `avg` non numeric columns.
If you aggregate on one column, the single value is directly returned as a result
If you aggregate on multiple columns a dictionary with column: result pairs is returned
```Python
--8<-- "../docs_src/aggregations/docs001.py"
```
A sample usage might look like following
```python
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()
assert round(float(await Book.objects.avg("year")), 2) == 1924.33
result = await Book.objects.avg(["year", "ranking"])
assert round(float(result.get("year")), 2) == 1924.33
assert result.get("ranking") == 3.0
try:
# cannot avg string column
await Book.objects.avg("title")
except ormar.QueryDefinitionError:
pass
result = await Author.objects.select_related("books").avg("books__year")
assert round(float(result), 2) == 1924.33
result = await Author.objects.select_related("books").avg(
["books__year", "books__ranking"]
)
assert round(float(result.get("books__year")), 2) == 1924.33
assert result.get("books__ranking") == 3.0
assert (
await Author.objects.select_related("books")
.filter(books__year__lt=1925)
.avg("books__year")
== 1921.5
)
```
## min
`min(columns) -> Any`
Returns min value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).
You can pass one or many column names including related columns.
As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)
If you aggregate on one column, the single value is directly returned as a result
If you aggregate on multiple columns a dictionary with column: result pairs is returned
```Python
--8<-- "../docs_src/aggregations/docs001.py"
```
A sample usage might look like following
```python
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()
assert await Book.objects.min("year") == 1920
result = await Book.objects.min(["year", "ranking"])
assert result == dict(year=1920, ranking=1)
assert await Book.objects.min("title") == "Book 1"
assert await Author.objects.select_related("books").min("books__year") == 1920
result = await Author.objects.select_related("books").min(
["books__year", "books__ranking"]
)
assert result == dict(books__year=1920, books__ranking=1)
assert (
await Author.objects.select_related("books")
.filter(books__year__gt=1925)
.min("books__year")
== 1930
)
```
## max
`max(columns) -> Any`
Returns max value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).
Returns min value of columns for rows matching the given criteria (applied with `filter` and `exclude` if set before).
You can pass one or many column names including related columns.
As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)
If you aggregate on one column, the single value is directly returned as a result
If you aggregate on multiple columns a dictionary with column: result pairs is returned
```Python
--8<-- "../docs_src/aggregations/docs001.py"
```
A sample usage might look like following
```python
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()
assert await Book.objects.max("year") == 1930
result = await Book.objects.max(["year", "ranking"])
assert result == dict(year=1930, ranking=5)
assert await Book.objects.max("title") == "Book 3"
assert await Author.objects.select_related("books").max("books__year") == 1930
result = await Author.objects.select_related("books").max(
["books__year", "books__ranking"]
)
assert result == dict(books__year=1930, books__ranking=5)
assert (
await Author.objects.select_related("books")
.filter(books__year__lt=1925)
.max("books__year")
== 1923
)
```
## QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey`
returns the list of related models.
But at the same time it exposes a subset of QuerySet API, so you can filter, create,
select related etc related models directly from parent model.
### count
Works exactly the same as [count](./#count) function above but allows you to select columns from related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### exists
Works exactly the same as [exists](./#exists) function above but allows you to select columns from related
objects from other side of the relation.
### sum
Works exactly the same as [sum](./#sum) function above but allows you to sum columns from related
objects from other side of the relation.
### avg
Works exactly the same as [avg](./#avg) function above but allows you to average columns from related
objects from other side of the relation.
### min
Works exactly the same as [min](./#min) function above but allows you to select minimum of columns from related
objects from other side of the relation.
### max
Works exactly the same as [max](./#max) function above but allows you to select maximum of columns from related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
[querysetproxy]: ../relations/queryset-proxy.md
ormar-orm-ormar-a7e7fde/docs/queries/create.md 0000664 0000000 0000000 00000013361 15211567312 0021530 0 ustar 00root root 0000000 0000000 # Insert data into database
Following methods allow you to insert data into the database.
* `create(**kwargs) -> Model`
* `get_or_create(_defaults: Optional[dict[str, Any]] = None, **kwargs) -> tuple[Model, bool]`
* `update_or_create(**kwargs) -> Model`
* `bulk_create(objects: list[Model]) -> None`
* `Model`
* `Model.save()` method
* `Model.upsert()` method
* `Model.save_related()` method
* `QuerysetProxy`
* `QuerysetProxy.create(**kwargs)` method
* `QuerysetProxy.get_or_create(_defaults: Optional[dict[str, Any]] = None, **kwargs)` method
* `QuerysetProxy.update_or_create(**kwargs)` method
## create
`create(**kwargs): -> Model`
Creates the model instance, saves it in a database and returns the updates model
(with pk populated if not passed and autoincrement is set).
The allowed kwargs are `Model` fields names and proper value types.
```python
class Album(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="album"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
```
```python
malibu = await Album.objects.create(name="Malibu")
await Track.objects.create(album=malibu, title="The Bird", position=1)
```
The alternative is a split creation and persistence of the `Model`.
```python
malibu = Album(name="Malibu")
await malibu.save()
```
!!!tip
Check other `Model` methods in [models][models]
## get_or_create
`get_or_create(_defaults: Optional[dict[str, Any]] = None, **kwargs) -> tuple[Model, bool]`
Combination of create and get methods.
Tries to get a row meeting the criteria and if `NoMatch` exception is raised it creates
a new one with given kwargs and _defaults.
When `_defaults` dictionary is provided the values set in `_defaults` will **always** be set, including overwriting explicitly provided values.
i.e. `get_or_create(_defaults: {"title": "I win"}, title="never used")` will always use "I win" as title whether you provide your own value in kwargs or not.
```python
class Album(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="album"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
year: int = ormar.Integer()
```
```python
album, created = await Album.objects.get_or_create(name='The Cat', _defaults={"year": 1999})
assert created is True
assert album.name == "The Cat"
assert album.year == 1999
# object is created as it does not exist
album2, created = await Album.objects.get_or_create(name='The Cat')
assert created is False
assert album == album2
# return True as the same db row is returned
```
!!!warning
Despite being a equivalent row from database the `album` and `album2` in
example above are 2 different python objects!
Updating one of them will not refresh the second one until you explicitly load() the
fresh data from db.
!!!note
Note that if you want to create a new object you either have to pass pk column
value or pk column has to be set as autoincrement
## update_or_create
`update_or_create(**kwargs) -> Model`
Updates the model, or in case there is no match in database creates a new one.
```Python hl_lines="44-52"
--8<-- "../docs_src/queries/docs003.py"
```
!!!note
Note that if you want to create a new object you either have to pass pk column
value or pk column has to be set as autoincrement
## bulk_create
`bulk_create(objects: list["Model"]) -> None`
Allows you to create multiple objects at once.
A valid list of `Model` objects needs to be passed.
```python hl_lines="30-37"
--8<-- "../docs_src/queries/docs004.py"
```
## Model methods
Each model instance have a set of methods to `save`, `update` or `load` itself.
###save
You can create new models by using `QuerySet.create()` method or by initializing your model as a normal pydantic model
and later calling `save()` method.
!!!tip
Read more about `save()` method in [models-save][models-save]
###upsert
It's a proxy to either `save()` or `update(**kwargs)` methods of a Model.
If the pk is not set the `save()` method will be called.
!!!tip
Read more about `upsert()` method in [models-upsert][models-upsert]
###save_related
Method goes through all relations of the `Model` on which the method is called,
and calls `upsert()` method on each model that is **not** saved.
!!!tip
Read more about `save_related()` method in [models-save-related][models-save-related]
## QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey` returns the list of related models.
But at the same time it exposes subset of QuerySet API, so you can filter, create, select related etc related models directly from parent model.
### create
Works exactly the same as [create](./#create) function above but allows you to create related objects
from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### get_or_create
Works exactly the same as [get_or_create](./#get_or_create) function above but allows you to query or create related objects
from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### update_or_create
Works exactly the same as [update_or_create](./#update_or_create) function above but allows you to update or create related objects
from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
[models]: ../models/methods.md
[models-save]: ../models/methods.md#save
[models-upsert]: ../models/methods.md#upsert
[models-save-related]: ../models/methods.md#save_related
[querysetproxy]: ../relations/queryset-proxy.md
ormar-orm-ormar-a7e7fde/docs/queries/delete.md 0000664 0000000 0000000 00000007012 15211567312 0021523 0 ustar 00root root 0000000 0000000 # Delete data from database
Following methods allow you to delete data from the database.
* `delete(each: bool = False, **kwargs) -> int`
* `Model`
* `Model.delete()` method
* `QuerysetProxy`
* `QuerysetProxy.remove()` method
* `QuerysetProxy.clear()` method
## delete
`delete(each: bool = False, **kwargs) -> int`
QuerySet level delete is used to delete multiple records at once.
You either have to filter the QuerySet first or provide a `each=True` flag to delete
whole table.
If you do not provide this flag or a filter a `QueryDefinitionError` will be raised.
Return number of rows deleted.
```python hl_lines="44-48"
--8<-- "../docs_src/queries/docs005.py"
```
## Model methods
Each model instance have a set of methods to `save`, `update` or `load` itself.
### delete
You can delete model instance by calling `delete()` method on it.
!!!tip
Read more about `delete()` method in [models methods](../models/methods.md#delete)
## QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey`
returns the list of related models.
But at the same time it exposes subset of QuerySet API, so you can filter, create,
select related etc related models directly from parent model.
### remove
Removal of the related model one by one.
Removes the relation in the database.
If you specify the keep_reversed flag to `False` `ormar` will also delete the related model from the database.
```python
class Album(ormar.Model):
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):
ormar_config = base_ormar_config.copy()
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)
```
```python
album = await Album(name="Malibu").save()
track1 = await Track(
album=album, title="The Bird", position=1, play_count=30,
).save()
# remove through proxy from reverse side of relation
await album.tracks.remove(track1, keep_reversed=False)
# the track was also deleted
tracks = await Track.objects.all()
assert len(tracks) == 0
```
### clear
Removal of all related models in one call.
Removes also the relation in the database.
If you specify the keep_reversed flag to `False` `ormar` will also delete the related model from the database.
```python
class Album(ormar.Model):
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):
ormar_config = base_ormar_config.copy()
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)
```
```python
album = await Album(name="Malibu").save()
track1 = await Track(
album=album,
title="The Bird",
position=1,
play_count=30,
).save()
track2 = await Track(
album=album,
title="Heart don't stand a chance",
position=2,
play_count=20,
).save()
# removes the relation only -> clears foreign keys on tracks
await album.tracks.clear()
# removes also the tracks
await album.tracks.clear(keep_reversed=False)
```
[querysetproxy]: ../relations/queryset-proxy.md
ormar-orm-ormar-a7e7fde/docs/queries/filter-and-sort.md 0000664 0000000 0000000 00000067665 15211567312 0023317 0 ustar 00root root 0000000 0000000 # Filtering and sorting data
You can use following methods to filter the data (sql where clause).
* `filter(*args, **kwargs) -> QuerySet`
* `exclude(*args, **kwargs) -> QuerySet`
* `get(*args, **kwargs) -> Model`
* `get_or_none(*args, **kwargs) -> Optional[Model]`
* `get_or_create(_defaults: Optional[dict[str, Any]] = None, *args, **kwargs) -> tuple[Model, bool]`
* `all(*args, **kwargs) -> list[Optional[Model]]`
* `QuerysetProxy`
* `QuerysetProxy.filter(*args, **kwargs)` method
* `QuerysetProxy.exclude(*args, **kwargs)` method
* `QuerysetProxy.get(*args, **kwargs)` method
* `QuerysetProxy.get_or_none(*args, **kwargs)` method
* `QuerysetProxy.get_or_create(_defaults: Optional[dict[str, Any]] = None, *args, **kwargs)` method
* `QuerysetProxy.all(*args, **kwargs)` method
And following methods to sort the data (sql order by clause).
* `order_by(columns:Union[list, str, OrderAction]) -> QuerySet`
* `QuerysetProxy`
* `QuerysetProxy.order_by(columns:Union[list, str, OrderAction])` method
## Filtering
### filter
`filter(*args, **kwargs) -> QuerySet`
Allows you to filter by any `Model` attribute/field as well as to fetch instances, with
a filter across an FK relationship.
```python
class Album(ormar.Model):
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):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
name: str = ormar.String(max_length=100)
position: int = ormar.Integer()
play_count: int = ormar.Integer(nullable=True)
```
```python
track = Track.objects.filter(name="The Bird").get()
# will return a track with name equal to 'The Bird'
tracks = Track.objects.filter(album__name="Fantasies").all()
# will return all tracks where the columns album name = 'Fantasies'
```
### Django style filters
You can use special filter suffix to change the filter operands:
* **exact** - exact match to value, sql `column = `
* can be written as`album__name__exact='Malibu'`
* **iexact** - exact match sql `column = ` (case insensitive)
* can be written as`album__name__iexact='malibu'`
* **contains** - sql `column LIKE '%%'`
* can be written as`album__name__contains='Mal'`
* **icontains** - sql `column LIKE '%%'` (case insensitive)
* can be written as`album__name__icontains='mal'`
* **in** - sql ` column IN (, , ...)`
* can be written as`album__name__in=['Malibu', 'Barclay']`
* **isnull** - sql `column IS NULL` (and sql `column IS NOT NULL`)
* can be written as`album__name__isnull=True` (isnotnull `album__name__isnull=False`)
* **gt** - sql `column > ` (greater than)
* can be written as`position__gt=3`
* **gte** - sql `column >= ` (greater or equal than)
* can be written as`position__gte=3`
* **lt** - sql `column < ` (lower than)
* can be written as`position__lt=3`
* **lte** - sql `column <= ` (lower equal than)
* can be written as`position__lte=3`
* **startswith** - sql `column LIKE '%'` (exact start match)
* can be written as`album__name__startswith='Mal'`
* **istartswith** - sql `column LIKE '%'` (case insensitive)
* can be written as`album__name__istartswith='mal'`
* **endswith** - sql `column LIKE '%'` (exact end match)
* can be written as`album__name__endswith='ibu'`
* **iendswith** - sql `column LIKE '%'` (case insensitive)
* can be written as`album__name__iendswith='IBU'`
Some samples:
```python
# sql: ( product.name = 'Test' AND product.rating >= 3.0 )
Product.objects.filter(name='Test', rating__gte=3.0).get()
# sql: ( product.name = 'Test' AND product.rating >= 3.0 )
# OR (categories.name IN ('Toys', 'Books'))
Product.objects.filter(
ormar.or_(
ormar.and_(name='Test', rating__gte=3.0),
categories__name__in=['Toys', 'Books'])
).get()
# note: to read more about and_ and or_ read complex filters section below
```
### Python style filters
* **exact** - exact match to value, sql `column = `
* can be written as `Track.album.name == 'Malibu`
* **iexact** - exact match sql `column = ` (case insensitive)
* can be written as `Track.album.name.iexact('malibu')`
* **contains** - sql `column LIKE '%%'`
* can be written as `Track.album.name % 'Mal')`
* can be written as `Track.album.name.contains('Mal')`
* **icontains** - sql `column LIKE '%%'` (case insensitive)
* can be written as `Track.album.name.icontains('mal')`
* **in** - sql ` column IN (, , ...)`
* can be written as `Track.album.name << ['Malibu', 'Barclay']`
* can be written as `Track.album.name.in_(['Malibu', 'Barclay'])`
* **isnull** - sql `column IS NULL` (and sql `column IS NOT NULL`)
* can be written as `Track.album.name >> None`
* can be written as `Track.album.name.isnull(True)`
* not null can be written as `Track.album.name.isnull(False)`
* not null can be written as `~(Track.album.name >> None)`
* not null can be written as `~(Track.album.name.isnull(True))`
* **gt** - sql `column > ` (greater than)
* can be written as `Track.album.name > 3`
* **gte** - sql `column >= ` (greater or equal than)
* can be written as `Track.album.name >= 3`
* **lt** - sql `column < ` (lower than)
* can be written as `Track.album.name < 3`
* **lte** - sql `column <= ` (lower equal than)
* can be written as `Track.album.name <= 3`
* **startswith** - sql `column LIKE '%'` (exact start match)
* can be written as `Track.album.name.startswith('Mal')`
* **istartswith** - sql `column LIKE '%'` (case insensitive)
* can be written as `Track.album.name.istartswith('mal')`
* **endswith** - sql `column LIKE '%'` (exact end match)
* can be written as `Track.album.name.endswith('ibu')`
* **iendswith** - sql `column LIKE '%'` (case insensitive)
* can be written as `Track.album.name.iendswith('IBU')`
Some samples:
```python
# sql: ( product.name = 'Test' AND product.rating >= 3.0 )
Product.objects.filter(
(Product.name == 'Test') & (Product.rating >= 3.0)
).get()
# sql: ( product.name = 'Test' AND product.rating >= 3.0 )
# OR (categories.name IN ('Toys', 'Books'))
Product.objects.filter(
((Product.name == 'Test') & (Product.rating >= 3.0)) |
(Product.categories.name << ['Toys', 'Books'])
).get()
```
!!!note
A foreign-key accessor can also be compared directly to a primary-key value
or a model instance, which is equivalent to the kwargs form and stays a
single-table filter (no JOIN is added):
```python
# both produce: WHERE books.author = 5
Book.objects.filter(author=5)
Book.objects.filter(Book.author == 5)
# with an instance - the PK is extracted for you
Book.objects.filter(Book.author == tolkien)
```
This only applies to own FK columns. Many-to-many and reverse relations
still require targeting a concrete column, e.g. `Category.products.name`.
!!!note
All methods that do not return the rows explicitly returns a QuerySet instance so
you can chain them together
So operations like `filter()`, `select_related()`, `limit()` and `offset()` etc. can be chained.
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
!!!warning
Note that you do not have to specify the `%` wildcard in contains and other
filters, it's added for you. If you include `%` in your search value it will be escaped
and treated as literal percentage sign inside the text.
### exclude
`exclude(*args, **kwargs) -> QuerySet`
Works exactly the same as filter and all modifiers (suffixes) are the same, but returns
a not condition.
So if you use `filter(name='John')` which equals to `where name = 'John'` in SQL,
the `exclude(name='John')` equals to `where name <> 'John'`
Note that all conditions are joined so if you pass multiple values it becomes a union of
conditions.
`exclude(name='John', age>=35)` will become `where not (name='John' and age>=35)`
```python
class Album(ormar.Model):
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):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
name: str = ormar.String(max_length=100)
position: int = ormar.Integer()
play_count: int = ormar.Integer(nullable=True)
```
```python
notes = await Track.objects.exclude(position_gt=3).all()
# returns all tracks with position < 3
```
## Complex filters (including OR)
By default both `filter()` and `exclude()` methods combine provided filter options with
`AND` condition so `filter(name="John", age__gt=30)` translates into `WHERE name = 'John' AND age > 30`.
Sometimes it's useful to query the database with conditions that should not be applied
jointly like `WHERE name = 'John' OR age > 30`, or build a complex where query that you would
like to have bigger control over. After all `WHERE (name = 'John' OR age > 30) and city='New York'` is
completely different than `WHERE name = 'John' OR (age > 30 and city='New York')`.
In order to build `OR` and nested conditions ormar provides two functions that can be used in
`filter()` and `exclude()` in `QuerySet` and `QuerysetProxy`.
!!!note
Note that you can provide those methods in any other method like `get()` or `all()` that accepts `*args`.
Call to `or_` and `and_` can be nested in each other, as well as combined with keyword arguments.
Since it sounds more complicated than it is, let's look at some examples.
Given a sample models like this:
```python
base_ormar_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
class Author(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
```
Let's create some sample data:
```python
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()
await Book(author=tolkien, title="The Silmarillion", year=1977).save()
sapkowski = await Author(name="Andrzej Sapkowski").save()
await Book(author=sapkowski, title="The Witcher", year=1990).save()
await Book(author=sapkowski, title="The Tower of Fools", year=2002).save()
```
We can construct some sample complex queries:
Let's select books of Tolkien **OR** books written after 1970
sql:
`WHERE ( authors.name = 'J.R.R. Tolkien' OR books.year > 1970 )`
#### Django style
```python
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(author__name="J.R.R. Tolkien", year__gt=1970))
.all()
)
assert len(books) == 5
```
#### Python style
```python
books = (
await Book.objects.select_related("author")
.filter((Book.author.name=="J.R.R. Tolkien") | (Book.year > 1970))
.all()
)
assert len(books) == 5
```
Now let's select books written after 1960 or before 1940 which were written by Tolkien.
sql:
`WHERE ( books.year > 1960 OR books.year < 1940 ) AND authors.name = 'J.R.R. Tolkien'`
#### Django style
```python
# OPTION 1 - split and into separate call
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(year__gt=1960, year__lt=1940))
.filter(author__name="J.R.R. Tolkien")
.all()
)
assert len(books) == 2
# OPTION 2 - all in one
books = (
await Book.objects.select_related("author")
.filter(
ormar.and_(
ormar.or_(year__gt=1960, year__lt=1940),
author__name="J.R.R. Tolkien",
)
)
.all()
)
assert len(books) == 2
assert books[0].title == "The Hobbit"
assert books[1].title == "The Silmarillion"
```
#### Python style
```python
books = (
await Book.objects.select_related("author")
.filter((Book.year > 1960) | (Book.year < 1940))
.filter(Book.author.name == "J.R.R. Tolkien")
.all()
)
assert len(books) == 2
# OPTION 2 - all in one
books = (
await Book.objects.select_related("author")
.filter(
(
(Book.year > 1960) | (Book.year < 1940)
) & (Book.author.name == "J.R.R. Tolkien")
)
.all()
)
assert len(books) == 2
assert books[0].title == "The Hobbit"
assert books[1].title == "The Silmarillion"
```
Books of Sapkowski from before 2000 or books of Tolkien written after 1960
sql:
`WHERE ( ( books.year > 1960 AND authors.name = 'J.R.R. Tolkien' ) OR ( books.year < 2000 AND authors.name = 'Andrzej Sapkowski' ) ) `
#### Django style
```python
books = (
await Book.objects.select_related("author")
.filter(
ormar.or_(
ormar.and_(year__gt=1960, author__name="J.R.R. Tolkien"),
ormar.and_(year__lt=2000, author__name="Andrzej Sapkowski"),
)
)
.all()
)
assert len(books) == 2
```
#### Python style
```python
books = (
await Book.objects.select_related("author")
.filter(
((Book.year > 1960) & (Book.author.name == "J.R.R. Tolkien")) |
((Book.year < 2000) & (Book.author.name == "Andrzej Sapkowski"))
)
.all()
)
assert len(books) == 2
```
Of course those functions can have more than 2 conditions, so if we for example want
books that contains 'hobbit':
sql:
`WHERE ( ( books.year > 1960 AND authors.name = 'J.R.R. Tolkien' ) OR
( books.year < 2000 AND os0cec_authors.name = 'Andrzej Sapkowski' ) OR
books.title LIKE '%hobbit%' )`
#### Django style
```python
books = (
await Book.objects.select_related("author")
.filter(
ormar.or_(
ormar.and_(year__gt=1960, author__name="J.R.R. Tolkien"),
ormar.and_(year__lt=2000, author__name="Andrzej Sapkowski"),
title__icontains="hobbit",
)
)
.all()
)
```
#### Python style
```python
books = (
await Book.objects.select_related("author")
.filter(
((Book.year > 1960) & (Book.author.name == "J.R.R. Tolkien")) |
((Book.year < 2000) & (Book.author.name == "Andrzej Sapkowski")) |
(Book.title.icontains("hobbit"))
)
.all()
)
```
If you want or need to you can nest deeper conditions as deep as you want, in example to
achieve a query like this:
sql:
```
WHERE ( ( ( books.year > 1960 OR books.year < 1940 )
AND authors.name = 'J.R.R. Tolkien' ) OR
( books.year < 2000 AND authors.name = 'Andrzej Sapkowski' ) )
```
You can construct a query as follows:
#### Django style
```python
books = (
await Book.objects.select_related("author")
.filter(
ormar.or_(
ormar.and_(
ormar.or_(year__gt=1960, year__lt=1940),
author__name="J.R.R. Tolkien",
),
ormar.and_(year__lt=2000, author__name="Andrzej Sapkowski"),
)
)
.all()
)
assert len(books) == 3
assert books[0].title == "The Hobbit"
assert books[1].title == "The Silmarillion"
assert books[2].title == "The Witcher"
```
#### Python style
```python
books = (
await Book.objects.select_related("author")
.filter(
(
(
(Book.year > 1960) |
(Book.year < 1940)
) &
(Book.author.name == "J.R.R. Tolkien")
) |
(
(Book.year < 2000) &
(Book.author.name == "Andrzej Sapkowski")
)
)
.all()
)
assert len(books) == 3
assert books[0].title == "The Hobbit"
assert books[1].title == "The Silmarillion"
assert books[2].title == "The Witcher"
```
By now you should already have an idea how `ormar.or_` and `ormar.and_` works.
Of course, you could chain them in any other methods of queryset, so in example a perfectly
valid query can look like follows:
```python
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(year__gt=1980, author__name="Andrzej Sapkowski"))
.filter(title__startswith="The")
.limit(1)
.offset(1)
.order_by("-id")
.all()
)
assert len(books) == 1
assert books[0].title == "The Witcher"
```
Same applies to python style chaining and nesting.
#### Django style
Note that with django style you cannot provide the same keyword argument several times so queries like `filter(ormar.or_(name='Jack', name='John'))` are not allowed. If you want to check the same
column for several values simply use `in` operator: `filter(name__in=['Jack','John'])`.
If you pass only one parameter to `or_` or `and_` functions it's simply wrapped in parenthesis and
has no effect on actual query, so in the end all 3 queries are identical:
```python
await Book.objects.filter(title='The Hobbit').get()
await Book.objects.filter(ormar.or_(title='The Hobbit')).get()
await Book.objects.filter(ormar.and_(title='The Hobbit')).get()
```
!!!note
Note that `or_` and `and_` queries will have `WHERE (title='The Hobbit')` but the parenthesis is redundant and has no real effect.
This feature can be used if you **really** need to use the same field name twice.
Remember that you cannot pass the same keyword arguments twice to the function, so
how you can query in example `WHERE (authors.name LIKE '%tolkien%') OR (authors.name LIKE '%sapkowski%'))`?
You cannot do:
```python
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(
author__name__icontains="tolkien",
author__name__icontains="sapkowski" # you cannot use same keyword twice in or_!
)) # python syntax error
.all()
)
```
But you can do this:
```python
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(
ormar.and_(author__name__icontains="tolkien"), # one argument == just wrapped in ()
ormar.and_(author__name__icontains="sapkowski")
))
.all()
)
assert len(books) == 5
```
#### Python style
Note that with python style you can perfectly use the same fields as many times as you want.
```python
books = (
await Book.objects.select_related("author")
.filter(
(Book.author.name.icontains("tolkien")) |
(Book.author.name.icontains("sapkowski"))
))
.all()
)
```
## get
`get(*args, **kwargs) -> Model`
Gets the first row from the db meeting the criteria set by kwargs.
When any args and/or kwargs are passed it's a shortcut equivalent to calling `filter(*args, **kwargs).get()`
!!!tip
To read more about `filter` go to [filter](./#filter).
To read more about `get` go to [read/get](../read/#get)
## get_or_none
Exact equivalent of get described above but instead of raising the exception returns `None` if no db record matching the criteria is found.
## get_or_create
`get_or_create(_defaults: Optional[dict[str, Any]] = None, *args, **kwargs) -> tuple[Model, bool]`
Combination of create and get methods.
When any args and/or kwargs are passed it's a shortcut equivalent to calling `filter(*args, **kwargs).get_or_create()`
!!!tip
To read more about `filter` go to [filter](./#filter).
To read more about `get_or_create` go to [read/get_or_create](../read/#get_or_create)
!!!warning
When given item does not exist you need to pass kwargs for all required fields of the
model, including but not limited to primary_key column (unless it's autoincrement).
## all
`all(*args, **kwargs) -> list[Optional["Model"]]`
Returns all rows from a database for given model for set filter options.
When any kwargs are passed it's a shortcut equivalent to calling `filter(*args, **kwargs).all()`
!!!tip
To read more about `filter` go to [filter](./#filter).
To read more about `all` go to [read/all](../read/#all)
### QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey`
returns the list of related models.
But at the same time it exposes subset of QuerySet API, so you can filter, create,
select related etc related models directly from parent model.
#### filter
Works exactly the same as [filter](./#filter) function above but allows you to filter related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
#### exclude
Works exactly the same as [exclude](./#exclude) function above but allows you to filter related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
#### get
Works exactly the same as [get](./#get) function above but allows you to filter related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
#### get_or_none
Exact equivalent of get described above but instead of raising the exception returns `None` if no db record matching the criteria is found.
#### get_or_create
Works exactly the same as [get_or_create](./#get_or_create) function above but allows
you to filter related objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
#### all
Works exactly the same as [all](./#all) function above but allows you to filter related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
## Sorting
### order_by
`order_by(columns: Union[list, str, OrderAction]) -> QuerySet`
With `order_by()` you can order the results from database based on your choice of
fields.
You can provide a string with field name or list of strings with different fields.
Ordering in sql will be applied in order of names you provide in order_by.
!!!tip
By default if you do not provide ordering `ormar` explicitly orders by all
primary keys
!!!warning
If you are sorting by nested models that causes that the result rows are
unsorted by the main model
`ormar` will combine those children rows into one main model.
Sample raw database rows result (sort by child model desc):
```
MODEL: 1 - Child Model - 3
MODEL: 2 - Child Model - 2
MODEL: 1 - Child Model - 1
```
will result in 2 rows of result:
```
MODEL: 1 - Child Models: [3, 1] # encountered first in result, all children rows combined
MODEL: 2 - Child Models: [2]
```
The main model will never duplicate in the result
Given sample Models like following:
```python
--8<-- "../docs_src/queries/docs007.py"
```
To order by main model field just provide a field name
#### Django style
```python
toys = await Toy.objects.select_related("owner").order_by("name").all()
assert [x.name.replace("Toy ", "") for x in toys] == [
str(x + 1) for x in range(6)
]
assert toys[0].owner == zeus
assert toys[1].owner == aphrodite
```
#### Python style
```python
toys = await Toy.objects.select_related("owner").order_by(Toy.name.asc()).all()
assert [x.name.replace("Toy ", "") for x in toys] == [
str(x + 1) for x in range(6)
]
assert toys[0].owner == zeus
assert toys[1].owner == aphrodite
```
To sort on nested models separate field names with dunder '__'.
You can sort this way across all relation types -> `ForeignKey`, reverse virtual FK
and `ManyToMany` fields.
#### Django style
```python
toys = await Toy.objects.select_related("owner").order_by("owner__name").all()
assert toys[0].owner.name == toys[1].owner.name == "Aphrodite"
assert toys[2].owner.name == toys[3].owner.name == "Hermes"
assert toys[4].owner.name == toys[5].owner.name == "Zeus"
```
#### Python style
```python
toys = await Toy.objects.select_related("owner").order_by(Toy.owner.name.asc()).all()
assert toys[0].owner.name == toys[1].owner.name == "Aphrodite"
assert toys[2].owner.name == toys[3].owner.name == "Hermes"
assert toys[4].owner.name == toys[5].owner.name == "Zeus"
```
To sort in descending order provide a hyphen in front of the field name
#### Django style
```python
owner = (
await Owner.objects.select_related("toys")
.order_by("-toys__name")
.filter(name="Zeus")
.get()
)
assert owner.toys[0].name == "Toy 4"
assert owner.toys[1].name == "Toy 1"
```
#### Python style
```python
owner = (
await Owner.objects.select_related("toys")
.order_by(Owner.toys.name.desc())
.filter(Owner.name == "Zeus")
.get()
)
assert owner.toys[0].name == "Toy 4"
assert owner.toys[1].name == "Toy 1"
```
!!!note
All methods that do not return the rows explicitly returns a QuerySet instance so
you can chain them together
So operations like `filter()`, `select_related()`, `limit()` and `offset()` etc. can be chained.
Something like `Track.objects.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
#### Controlling NULL placement with `nulls_ordering`
Both `.asc()` and `.desc()` accept an optional `nulls_ordering` argument so you can
decide whether `NULL` values come first or last in the result:
```python
import ormar
# nulls go to the end
await Song.objects.order_by(
Song.sort_order.asc(nulls_ordering=ormar.NullsOrdering.LAST)
).all()
# nulls come first
await Owner.objects.order_by(
Owner.toys.name.desc(nulls_ordering=ormar.NullsOrdering.FIRST)
).all()
```
!!!note
On PostgreSQL and SQLite (3.30+) this emits the standard `NULLS FIRST` /
`NULLS LAST` clause. MySQL does not support that syntax, so `ormar`
emulates it by prepending an `IS NULL` / `IS NOT NULL` sort key — the
result set is unchanged, only the ordering.
### Default sorting in ormar
Since order of rows in a database is not guaranteed, `ormar` **always** issues an `order by` sql clause to each (part of) query even if you do not provide order yourself.
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 `ormar_config`.
!!!tip
To read more about models sort order visit [models](../models/index.md#model-sort-order) section of documentation
By default the relations follow the same ordering, but you can modify the order in which related models are loaded during query by providing `orders_by` and `related_orders_by`
parameters to relations.
!!!tip
To read more about models sort order visit [relations](../relations/index.md#relationship-default-sort-order) section of documentation
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'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).**
That means that if you provide explicit `order_by` for a model in a query, the `Relation` and `Model` sort orders are skipped.
If you provide a `Relation` one, the `Model` sort is skipped.
Finally, if you provide one for `Model` the default one by `primary_key` is skipped.
### QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey`
returns the list of related models.
But at the same time it exposes subset of QuerySet API, so you can filter, create,
select related etc related models directly from parent model.
#### order_by
Works exactly the same as [order_by](./#order_by) function above but allows you to sort related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
[querysetproxy]: ../relations/queryset-proxy.md
ormar-orm-ormar-a7e7fde/docs/queries/index.md 0000664 0000000 0000000 00000014227 15211567312 0021376 0 ustar 00root root 0000000 0000000 # Querying database with ormar
## QuerySet
Each Model is auto registered with a `QuerySet` that represents the underlying query,
and it's options.
Most of the methods are also available through many to many relations and on reverse
foreign key relations through `QuerysetProxy` interface.
!!!info
To see which relations are supported and how to construct relations
visit [relations][relations].
For simplicity available methods to fetch and save the data into the database are
divided into categories according to the function they fulfill.
Note that some functions/methods are in multiple categories.
For completeness, Model and relation methods are listed.
To read more about any specific section or function please refer to the details subpage.
###[Insert data into database](./create.md)
* `create(**kwargs) -> Model`
* `get_or_create(_defaults: Optional[dict[str, Any]] = None, **kwargs) -> tuple[Model, bool]`
* `update_or_create(**kwargs) -> Model`
* `bulk_create(objects: list[Model]) -> None`
* `Model`
* `Model.save()` method
* `Model.upsert()` method
* `Model.save_related()` method
* `QuerysetProxy`
* `QuerysetProxy.create(**kwargs)` method
* `QuerysetProxy.get_or_create(_defaults: Optional[dict[str, Any]] = None, **kwargs)` method
* `QuerysetProxy.update_or_create(**kwargs)` method
!!!tip
To read more about any or all of those functions visit [create](./create.md) section.
### [Read data from database](./read.md)
* `get(**kwargs) -> Model`
* `get_or_none(**kwargs) -> Optional[Model]`
* `get_or_create(_defaults: Optional[dict[str, Any]] = None, **kwargs) -> tuple[Model, bool]`
* `first() -> Model`
* `all(**kwargs) -> list[Optional[Model]]`
* `Model`
* `Model.load()` method
* `QuerysetProxy`
* `QuerysetProxy.get(**kwargs)` method
* `QuerysetProxy.get_or_none(**kwargs)` method
* `QuerysetProxy.get_or_create(_defaults: Optional[dict[str, Any]] = None, **kwargs)` method
* `QuerysetProxy.first()` method
* `QuerysetProxy.all(**kwargs)` method
!!!tip
To read more about any or all of those functions visit [read](./read.md) section.
### [Read raw data from database](./raw-data.md)
Instead of ormar models return raw data in form list of dictionaries or tuples.
* `values(fields = None, exclude_through = False) -> list[dict]`
* `values_list(fields = None, exclude_through = False, flatten = False) -> list`
* `QuerysetProxy`
* `QuerysetProxy.values(fields = None, exclude_through = False)` method
* `QuerysetProxy.values_list(fields = None, exclude_through= False, flatten = False)` method
!!!tip
To read more about any or all of those functions visit [raw data](./raw-data.md) section.
### [Update data in database](./update.md)
* `update(each: bool = False, **kwargs) -> int`
* `update_or_create(**kwargs) -> Model`
* `bulk_update(objects: list[Model], columns: list[str] = None) -> None`
* `Model`
* `Model.update()` method
* `Model.upsert()` method
* `Model.save_related()` method
* `QuerysetProxy`
* `QuerysetProxy.update_or_create(**kwargs)` method
!!!tip
To read more about any or all of those functions visit [update](./update.md) section.
### [Delete data from database](./delete.md)
* `delete(each: bool = False, **kwargs) -> int`
* `Model`
* `Model.delete()` method
* `QuerysetProxy`
* `QuerysetProxy.remove()` method
* `QuerysetProxy.clear()` method
!!!tip
To read more about any or all of those functions visit [delete](./delete.md) section.
### [Joins and subqueries](./joins-and-subqueries.md)
* `select_related(related: Union[list, str]) -> QuerySet`
* `prefetch_related(related: Union[list, str]) -> QuerySet`
* `Model`
* `Model.load()` method
* `QuerysetProxy`
* `QuerysetProxy.select_related(related: Union[list, str])` method
* `QuerysetProxy.prefetch_related(related: Union[list, str])` method
!!!tip
To read more about any or all of those functions visit [joins and subqueries](./joins-and-subqueries.md) section.
### [Filtering and sorting](./filter-and-sort.md)
* `filter(**kwargs) -> QuerySet`
* `exclude(**kwargs) -> QuerySet`
* `order_by(columns:Union[list, str]) -> QuerySet`
* `get(**kwargs) -> Model`
* `get_or_none(**kwargs) -> Optional[Model]`
* `get_or_create(_defaults: Optional[dict[str, Any]] = None, **kwargs) -> tuple[Model, bool]`
* `all(**kwargs) -> list[Optional[Model]]`
* `QuerysetProxy`
* `QuerysetProxy.filter(**kwargs)` method
* `QuerysetProxy.exclude(**kwargs)` method
* `QuerysetProxy.order_by(columns:Union[list, str])` method
* `QuerysetProxy.get(**kwargs)` method
* `QuerysetProxy.get_or_none(**kwargs)` method
* `QuerysetProxy.get_or_create(_defaults: Optional[dict[str, Any]] = None, **kwargs)` method
* `QuerysetProxy.all(**kwargs)` method
!!!tip
To read more about any or all of those functions visit [filtering and sorting](./filter-and-sort.md) section.
### [Selecting columns](./select-columns.md)
* `fields(columns: Union[list, str, set, dict]) -> QuerySet`
* `exclude_fields(columns: Union[list, str, set, dict]) -> QuerySet`
* `QuerysetProxy`
* `QuerysetProxy.fields(columns: Union[list, str, set, dict])` method
* `QuerysetProxy.exclude_fields(columns: Union[list, str, set, dict])` method
!!!tip
To read more about any or all of those functions visit [selecting columns](./select-columns.md) section.
### [Pagination and rows number](./pagination-and-rows-number.md)
* `paginate(page: int) -> QuerySet`
* `limit(limit_count: int) -> QuerySet`
* `offset(offset: int) -> QuerySet`
* `get() -> Model`
* `first() -> Model`
* `QuerysetProxy`
* `QuerysetProxy.paginate(page: int)` method
* `QuerysetProxy.limit(limit_count: int)` method
* `QuerysetProxy.offset(offset: int)` method
!!!tip
To read more about any or all of those functions visit [pagination](./pagination-and-rows-number.md) section.
### [Aggregated functions](./aggregations.md)
* `count(distinct: bool = True) -> int`
* `exists() -> bool`
* `QuerysetProxy`
* `QuerysetProxy.count(distinct=True)` method
* `QuerysetProxy.exists()` method
!!!tip
To read more about any or all of those functions visit [aggregations](./aggregations.md) section.
[relations]: ../relations/index.md
ormar-orm-ormar-a7e7fde/docs/queries/joins-and-subqueries.md 0000664 0000000 0000000 00000041050 15211567312 0024330 0 ustar 00root root 0000000 0000000 # Joins and subqueries
To join one table to another, so load also related models you can use following methods.
* `select_related(related: Union[list, str]) -> QuerySet`
* `select_all(follow: bool = True) -> QuerySet`
* `prefetch_related(related: Union[list, str]) -> QuerySet`
* `Model`
* `Model.load()` method
* `QuerysetProxy`
* `QuerysetProxy.select_related(related: Union[list, str])` method
* `QuerysetProxy.select_all(follow: bool=True)` method
* `QuerysetProxy.prefetch_related(related: Union[list, str])` method
## select_related
`select_related(related: Union[list, str]) -> QuerySet`
Allows to prefetch related models during the same query.
**With `select_related` always only one query is run against the database**, meaning
that one (sometimes complicated) join is generated and later nested models are processed in
python.
To fetch related model use `ForeignKey` names.
To chain related `Models` relation use double underscores between names.
!!!note
If you are coming from `django` note that `ormar` `select_related` differs ->
in `django` you can `select_related`
only single relation types, while in `ormar` you can select related across `ForeignKey`
relation, reverse side of `ForeignKey` (so virtual auto generated keys) and `ManyToMany`
fields (so all relations as of current version).
!!!tip
To control which model fields to select use `fields()`
and `exclude_fields()` `QuerySet` methods.
!!!tip
To control order of models (both main or nested) use `order_by()` method.
```python
class Album(ormar.Model):
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):
ormar_config = base_ormar_config.copy()
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)
```
```python
# Django style
album = await Album.objects.select_related("tracks").all()
# Python style
album = await Album.objects.select_related(Album.tracks).all()
# will return album with all columns tracks
```
You can provide a string or a list of strings (or a field/ list of fields)
```python
class SchoolClass(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="schoolclasses")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
department: Optional[Department] = ormar.ForeignKey(Department, nullable=False)
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 Student(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
class Teacher(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
```
```python
# Django style
classes = await SchoolClass.objects.select_related(
["teachers__category", "students"]).all()
# Python style
classes = await SchoolClass.objects.select_related(
[SchoolClass.teachers.category, SchoolClass.students]).all()
# will return classes with teachers and teachers categories
# as well as classes students
```
Exactly the same behavior is for Many2Many fields, where you put the names of Many2Many
fields and the final `Models` are fetched for you.
!!!warning
If you set `ForeignKey` field as not nullable (so required) during all
queries the not nullable `Models` will be auto prefetched, even if you do not include
them in select_related.
!!!note
All methods that do not return the rows explicitly returns a QuerySet instance so
you can chain them together
So operations like `filter()`, `select_related()`, `limit()` and `offset()` etc. can be chained.
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
## select_all
`select_all(follow: bool = False) -> QuerySet`
By default when you select `all()` none of the relations are loaded, likewise,
when `select_related()` is used you need to explicitly specify all relations that should
be loaded. If you want to include also nested relations this can be cumberstone.
That's why `select_all()` was introduced, so by default load all relations of a model
(so kind of opposite as with `all()` approach).
By default adds only directly related models of a parent model (from which the query is run).
If `follow=True` is set it adds also related models of related models.
!!!info
To not get stuck in an infinite loop as related models also keep a relation
to parent model visited models set is kept.
That way already visited models that are nested are loaded, but the load do not
follow them inside. So Model A -> Model B -> Model C -> Model A -> Model X
will load second Model A but will never follow into Model X.
Nested relations of those kind need to be loaded manually.
With sample date like follow:
```python
base_ormar_config = OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
class Address(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="addresses")
id: int = ormar.Integer(primary_key=True)
street: str = ormar.String(max_length=100, nullable=False)
number: int = ormar.Integer(nullable=False)
post_code: str = ormar.String(max_length=20, nullable=False)
class Branch(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="branches")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, nullable=False)
address = ormar.ForeignKey(Address)
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, nullable=False, name="company_name")
founded: int = ormar.Integer(nullable=True)
branches = ormar.ManyToMany(Branch)
```
To select all `Companies` with all `Branches` and `Addresses` you can simply query:
```python
companies = await Company.objects.select_all(follow=True).all()
# which is equivalent to:
companies = await Company.objects.select_related('branches__address').all()
```
Of course in this case it's quite easy to issue explicit relation names in `select_related`,
but the benefit of `select_all()` shows when you have multiple relations.
If for example `Company` would have 3 relations and all of those 3 relations have it's own
3 relations you would have to issue 9 relation strings to `select_related`, `select_all()`
is also resistant to change in names of relations.
!!!note
Note that you can chain `select_all()` with other `QuerySet` methods like `filter`, `exclude_fields` etc.
To exclude relations use `exclude_fields()` call with names of relations (also nested) to exclude.
## prefetch_related
`prefetch_related(related: Union[list, str]) -> QuerySet`
Allows to prefetch related models during query - but opposite to `select_related` each
subsequent model is fetched in a separate database query.
**With `prefetch_related` always one query per Model is run against the database**,
meaning that you will have multiple queries executed one after another.
To fetch related model use `ForeignKey` names.
To chain related `Models` relation use double underscores between names.
!!!tip
To control which model fields to select use `fields()`
and `exclude_fields()` `QuerySet` methods.
!!!tip
To control order of models (both main or nested) use `order_by()` method.
```python
class Album(ormar.Model):
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):
ormar_config = base_ormar_config.copy()
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)
```
```python
# Django style
album = await Album.objects.prefetch_related("tracks").all()
# Python style
album = await Album.objects.prefetch_related(Album.tracks).all()
# will return album will all columns tracks
```
You can provide a string, or a list of strings
```python
class SchoolClass(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="schoolclasses")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
department: Optional[Department] = ormar.ForeignKey(Department, nullable=False)
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 Student(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
class Teacher(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
```
```python
# Django style
classes = await SchoolClass.objects.prefetch_related(
["teachers__category", "students"]).all()
# Python style
classes = await SchoolClass.objects.prefetch_related(
[SchoolClass.teachers.category, SchoolClass.students]).all()
# will return classes with teachers and teachers categories
# as well as classes students
```
Exactly the same behavior is for Many2Many fields, where you put the names of Many2Many
fields and the final `Models` are fetched for you.
!!!warning
If you set `ForeignKey` field as not nullable (so required) during all
queries the not nullable `Models` will be auto prefetched, even if you do not include
them in select_related.
!!!note
All methods that do not return the rows explicitly returns a QuerySet instance so
you can chain them together
So operations like `filter()`, `select_related()`, `limit()` and `offset()` etc. can be chained.
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
## select_related vs prefetch_related
Which should you use -> `select_related` or `prefetch_related`?
Well, it really depends on your data. The best answer is try yourself and see which one
performs faster/better in your system constraints.
What to keep in mind:
### Performance
**Number of queries**:
`select_related` always executes one query against the database,
while `prefetch_related` executes multiple queries. Usually the query (I/O) operation is
the slowest one but it does not have to be.
**Number of rows**:
Imagine that you have 10 000 object in one table A and each of those objects have 3
children in table B, and subsequently each object in table B has 2 children in table C.
Something like this:
```
Model C
/
Model B - Model C
/
Model A - Model B - Model C
\ \
\ Model C
\
Model B - Model C
\
Model C
```
That means that `select_related` will always return 60 000 rows (10 000 * 3 * 2) later
compacted to 10 000 models.
How many rows will return `prefetch_related`?
Well, that depends, if each of models B and C is unique it will return 10 000 rows in
first query, 30 000 rows
(each of 3 children of A in table B are unique) in second query and 60 000 rows (each of
2 children of model B in table C are unique) in 3rd query.
In this case `select_related` seems like a better choice, not only it will run one query
comparing to 3 of
`prefetch_related` but will also return 60 000 rows comparing to 100 000
of `prefetch_related` (10+30+60k).
But what if each Model A has exactly the same 3 models B and each models C has exactly
same models C? `select_related`
will still return 60 000 rows, while `prefetch_related` will return 10 000 for model A,
3 rows for model B and 2 rows for Model C. So in total 10 006 rows. Now depending on the
structure of models (i.e. if it has long Text() fields etc.) `prefetch_related`
might be faster despite it needs to perform three separate queries instead of one.
#### Memory
`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.
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
persist your changes into the database the change **will be available only after reload
(either each child separately or the whole query again)**. That means
that `select_related` will use more memory as each child is instantiated as a new object
- obviously using it's own space.
!!!note
This might change in future versions if we decide to introduce caching.
!!!warning
By default all children (or event the same models loaded 2+ times) are
completely independent, distinct python objects, despite that they represent the same
row in db.
They will evaluate to True when compared, so in example above:
```python
# will return True if child1 of both rows is the same child db row
row1.child1 == row100.child1
# same here:
model1 = await Model.get(pk=1)
model2 = await Model.get(pk=1) # same pk = same row in db
# will return `True`
model1 == model2
```
but
```python
# will return False (note that id is a python `builtin` function not ormar one).
id(row1.child1) == id(ro100.child1)
# from above - will also return False
id(model1) == id(model2)
```
On the contrary - with `prefetch_related` each unique distinct child model is
instantiated only once and the same child models is shared across all parent models.
That means that in `prefetch_related` example above if there are 3 distinct models in
table B and 2 in table C, there will be only 5 children nested models shared between all
model A instances. That also means that if you update any attribute it will be updated
on all parents as they share the same child object.
## Model methods
Each model instance have a set of methods to `save`, `update` or `load` itself.
### load
You can load the `ForeignKey` related model by calling `load()` method.
`load()` can be used to refresh the model from the database (if it was changed by some other process).
!!!tip
Read more about `load()` method in [models methods](../models/methods.md#load)
## QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey`
returns the list of related models.
But at the same time it exposes subset of QuerySet API, so you can filter, create,
select related etc related models directly from parent model.
### select_related
Works exactly the same as [select_related](./#select_related) function above but allows you to fetch related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### select_all
Works exactly the same as [select_all](./#select_all) function above but allows you to fetch related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### prefetch_related
Works exactly the same as [prefetch_related](./#prefetch_related) function above but allows you to fetch related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
[querysetproxy]: ../relations/queryset-proxy.md
ormar-orm-ormar-a7e7fde/docs/queries/pagination-and-rows-number.md 0000664 0000000 0000000 00000017224 15211567312 0025436 0 ustar 00root root 0000000 0000000 #Pagination and rows number
Following methods allow you to paginate and limit number of rows in queries.
* `paginate(page: int) -> QuerySet`
* `limit(limit_count: int) -> QuerySet`
* `offset(offset: int) -> QuerySet`
* `__getitem__(key: int | slice) -> QuerySet`
* `get() -> Model`
* `first() -> Model`
* `first_or_none() -> Optional[Model]`
* `last() -> Model`
* `last_or_none() -> Optional[Model]`
* `QuerysetProxy`
* `QuerysetProxy.paginate(page: int)` method
* `QuerysetProxy.limit(limit_count: int)` method
* `QuerysetProxy.offset(offset: int)` method
* `QuerysetProxy.__getitem__(key: int | slice)` method
* `QuerysetProxy.first_or_none()` method
* `QuerysetProxy.last()` method
* `QuerysetProxy.last_or_none()` method
## paginate
`paginate(page: int, page_size: int = 20) -> QuerySet`
Combines the `offset` and `limit` methods based on page number and size
```python
class Track(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
tablename="track"
)
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
name: str = ormar.String(max_length=100)
position: int = ormar.Integer()
```
```python
tracks = await Track.objects.paginate(3).all()
# will return 20 tracks starting at row 41
# (with default page size of 20)
```
Note that `paginate(2)` is equivalent to `offset(20).limit(20)`
## limit
`limit(limit_count: int, limit_raw_sql: bool = None) -> QuerySet`
You can limit the results to desired number of parent models.
To limit the actual number of database query rows instead of number of main models
use the `limit_raw_sql` parameter flag, and set it to `True`.
```python
class Track(ormar.Model):
ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
tablename="track"
)
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
name: str = ormar.String(max_length=100)
position: int = ormar.Integer()
```
```python
tracks = await Track.objects.limit(1).all()
# will return just one Track
```
!!!note
All methods that do not return the rows explicitly returns a QuerySet instance so you can chain them together
So operations like `filter()`, `select_related()`, `limit()` and `offset()` etc. can be chained.
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
## offset
`offset(offset: int, limit_raw_sql: bool = None) -> QuerySet`
You can also offset the results by desired number of main models.
To offset the actual number of database query rows instead of number of main models
use the `limit_raw_sql` parameter flag, and set it to `True`.
```python
class Track(ormar.Model):
ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
tablename="track"
)
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
name: str = ormar.String(max_length=100)
position: int = ormar.Integer()
```
```python
tracks = await Track.objects.offset(1).limit(1).all()
# will return just one Track, but this time the second one
```
!!!note
All methods that do not return the rows explicitly returns a QuerySet instance so you can chain them together
So operations like `filter()`, `select_related()`, `limit()` and `offset()` etc. can be chained.
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
## slicing with `__getitem__`
A `QuerySet` can also be sliced with Python integer or slice syntax. Each
call returns a new `QuerySet` with LIMIT/OFFSET set accordingly — you still
need to `await` it with `.all()` to actually run the query.
```python
# positive bounds map directly to LIMIT/OFFSET
first_ten = await Track.objects[:10].all()
page_two = await Track.objects[10:20].all()
single = await Track.objects[5].all() # returns a one-element list
```
Negative indices and negative slice bounds are supported too. Internally
they are translated into a reversed-order query plus an in-memory list
reversal, so the caller still sees results in the original ordering:
```python
last_five = await Track.objects[-5:].all()
last_one = await Track.objects[-1].all() # one-element list
tail_slice = await Track.objects[-10:-5].all()
```
!!!note
Slice shapes that would require a `COUNT(*)` round-trip to resolve — a
bare `[:-N]`, or mixed positive/negative bounds like `[3:-2]` — raise
`QueryDefinitionError`. If you need "all except the last N", fetch
`.count()` explicitly and combine it with `.offset()`/`.limit()`.
A `step` other than `1` is not supported, and a non-integer / non-slice
key (e.g. `objects["foo"]`) also raises `QueryDefinitionError`.
Each slice replaces any previous pagination state rather than composing
with it — avoid chaining multiple slices on the same queryset.
## get
`get(**kwargs) -> Model`
Gets the first row from the db meeting the criteria set by kwargs.
If no criteria is set it will return the last row in db sorted by pk.
(The criteria cannot be set also with filter/exclude).
!!!tip
To read more about `get` visit [read/get](./read/#get)
## first
`first() -> Model`
Gets the first row from the db ordered by primary key column ascending.
!!!tip
To read more about `first` visit [read/first](./read/#first)
## first_or_none
`first_or_none() -> Optional[Model]`
Same as `first()` but returns `None` instead of raising `NoMatch` when no
row matches.
!!!tip
To read more about `first_or_none` visit [read/first_or_none](./read/#first_or_none)
## last
`last() -> Model`
Gets the last row from the db ordered by primary key column descending.
Complementary to `first()` — the default pk ordering is flipped and the top
row is returned.
```python
newest = await Track.objects.last()
newest_by_name = await Track.objects.order_by("name").last()
```
!!!tip
To read more about `last` visit [read/last](./read/#last)
## last_or_none
`last_or_none() -> Optional[Model]`
Same as `last()` but returns `None` instead of raising `NoMatch` when no
row matches.
!!!tip
To read more about `last_or_none` visit [read/last_or_none](./read/#last_or_none)
## QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey`
returns the list of related models.
But at the same time it exposes subset of QuerySet API, so you can filter, create,
select related etc related models directly from parent model.
### paginate
Works exactly the same as [paginate](./#paginate) function above but allows you to paginate related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### limit
Works exactly the same as [limit](./#limit) function above but allows you to paginate related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### offset
Works exactly the same as [offset](./#offset) function above but allows you to paginate related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### slicing with `__getitem__`
Works exactly the same as [slicing](./#slicing-with-__getitem__) above but on the relation
side:
```python
recent_cars = await user.cars[-3:].all()
```
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
[querysetproxy]: ../relations/queryset-proxy.md
ormar-orm-ormar-a7e7fde/docs/queries/raw-data.md 0000664 0000000 0000000 00000031474 15211567312 0021772 0 ustar 00root root 0000000 0000000 # Return raw data
Following methods allow you to execute a query but instead of returning ormar models those will return list of dicts or tuples.
* `values(fields = None, exclude_through = False) -> list[dict]`
* `values_list(fields = None, exclude_through = False, flatten = False) -> list`
* `QuerysetProxy`
* `QuerysetProxy.values(fields = None, exclude_through = False)` method
* `QuerysetProxy.values_list(fields = None, exclude_through= False, flatten = False)` method
!!!danger
Note that `values` and `values_list` skips parsing the result to ormar models so skips also the validation of the result!
!!!warning
Note that each entry in a result list is one to one reflection of a query result row.
Since rows are not parsed if you have one-to-many or many-to-many relation expect
duplicated columns values in result entries if one parent row have multiple related rows.
## values
`values(fields: Union[list, str, set, dict] = None, exclude_through: bool = False) -> list[dict]`
Return a list of dictionaries representing the values of the columns coming from the database.
You can select a subset of fields with fields parameter, that accepts the same set of parameters as `fields()` method.
Note that passing fields to `values(fields)` is actually a shortcut for calling `fields(fields).values()`.
!!!tip
To read more about what you can pass to fields and how to select nested models fields read [selecting columns](./select-columns.md#fields) docs
You can limit the number of rows by providing conditions in `filter()` and `exclude()`, but note that even if only one row (or no rows!) match your criteria you will return a list in response.
Example:
```python
# declared models
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=40)
sort_order: int = ormar.Integer(nullable=True)
class Post(ormar.Model):
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)
# sample data
news = await Category(name="News", sort_order=0).save()
await Post(name="Ormar strikes again!", category=news).save()
await Post(name="Why don't you use ormar yet?", category=news).save()
await Post(name="Check this out, ormar now for free", category=news).save()
```
Access Post models:
```python
posts = await Post.objects.values()
assert posts == [
{"id": 1, "name": "Ormar strikes again!", "category": 1},
{"id": 2, "name": "Why don't you use ormar yet?", "category": 1},
{"id": 3, "name": "Check this out, ormar now for free", "category": 1},
]
```
To select also related models use `select_related` or `prefetch_related`.
Note how nested models columns will be prefixed with full relation path coming from the main model (the one used in a query).
```python
# declare models
class User(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Role(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
users: list[User] = ormar.ManyToMany(User)
# sample data
creator = await User(name="Anonymous").save()
admin = await Role(name="admin").save()
editor = await Role(name="editor").save()
await creator.roles.add(admin)
await creator.roles.add(editor)
```
Select user with roles
```python
user = await User.objects.select_related("roles").values()
# note nested prefixes: roleuser and roles
assert user == [
{
"id": 1,
"name": "Anonymous",
"roleuser__id": 1,
"roleuser__role": 1,
"roleuser__user": 1,
"roles__id": 1,
"roles__name": "admin",
},
{
"id": 1,
"name": "Anonymous",
"roleuser__id": 2,
"roleuser__role": 2,
"roleuser__user": 1,
"roles__id": 2,
"roles__name": "editor",
},
]
```
!!!note
Note how role to users relation is a `ManyToMany` relation so by default you also get through model columns.
Combine select related and fields to select only 3 fields.
!!!note
`values()` and `values_list()` project only the columns referenced in
`fields()`. Related models pulled into the query (either via
`select_related()` directly, or implicitly by a filter like
`filter(category__id=1)`) but never named in `fields()` are dropped
from the result - including their through models for many-to-many
relations.
This differs from `fields()` on a regular ORM-loading query (`get`,
`all`, etc.), where unmentioned related models in `select_related()`
are still loaded in full so model construction does not lose
mandatory columns.
!!!note
Note that in contrary to other queryset methods here you can exclude the
in-between models but keep the end columns, which does not make sense
when parsing the raw data into models.
So in relation category -> category_x_post -> post -> user you can exclude
category_x_post and post models but can keep the user one. (in ormar model
context that is not possible as if you would exclude through and post model
there would be no way to reach user model from category model).
```python
user = (
await Role.objects.select_related("users__categories")
.filter(name="admin")
.fields({"name": ..., "users": {"name": ..., "categories": {"name"}}})
.exclude_fields("roleuser")
.values()
)
assert user == [
{
"name": "admin",
"users__name": "Anonymous",
"users__categories__name": "News",
}
]
```
If you have multiple ManyToMany models in your query you would have to exclude each through model manually.
To avoid this burden `ormar` provides you with `exclude_through=False` parameter.
If you set this flag to True **all through models will be fully excluded**.
```python
# equivalent to query above, note lack of exclude_fields call
user = (
await Role.objects.select_related("users__categories")
.filter(name="admin")
.fields({"name": ..., "users": {"name": ..., "categories": {"name"}}})
.values(exclude_through=True)
)
assert user == [
{
"name": "admin",
"users__name": "Anonymous",
"users__categories__name": "News",
}
]
```
## values_list
`values_list(fields: Union[list, str, set, dict] = None, flatten: bool = False, exclude_through: bool = False) -> list`
Return a list of tuples representing the values of the columns coming from the database.
You can select a subset of fields with fields parameter, that accepts the same set of parameters as `fields()` method.
Note that passing fields to `values_list(fields)` is actually a shortcut for calling `fields(fields).values_list()`.
!!!tip
To read more about what you can pass to fields and how to select nested models fields read [selecting columns](./select-columns.md#fields) docs
If you select only one column/field you can pass `flatten=True` which will return you a list of values instead of list of one element tuples.
!!!warning
Setting `flatten=True` if more than one (or none which means all) fields are selected will raise `QueryDefinitionError` exception.
You can limit the number of rows by providing conditions in `filter()` and `exclude()`, but note that even if only one row (or no rows!) match your criteria you will return a list in response.
Example:
```python
# declared models
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=40)
sort_order: int = ormar.Integer(nullable=True)
class Post(ormar.Model):
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)
# sample data
news = await Category(name="News", sort_order=0).save()
await Post(name="Ormar strikes again!", category=news).save()
await Post(name="Why don't you use ormar yet?", category=news).save()
await Post(name="Check this out, ormar now for free", category=news).save()
```
Access Post models:
```python
posts = await Post.objects.values_list()
# note how columns refer to id, name and category (fk)
assert posts == [
(1, "Ormar strikes again!", 1),
(2, "Why don't you use ormar yet?", 1),
(3, "Check this out, ormar now for free", 1),
]
```
To select also related models use `select_related` or `prefetch_related`.
Let's complicate the relation and modify the previously mentioned Category model to refer to User model.
```python
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=40)
sort_order: int = ormar.Integer(nullable=True)
# new column below
created_by: Optional[User] = ormar.ForeignKey(User, related_name="categories")
```
Now create the sample data with link to user.
```python
creator = await User(name="Anonymous").save()
admin = await Role(name="admin").save()
editor = await Role(name="editor").save()
await creator.roles.add(admin)
await creator.roles.add(editor)
news = await Category(name="News", sort_order=0, created_by=creator).save()
```
Combine select related and fields to select only 3 fields.
!!!note
`values()` and `values_list()` project only the columns referenced in
`fields()`. Related models pulled into the query (either via
`select_related()` directly, or implicitly by a filter like
`filter(category__id=1)`) but never named in `fields()` are dropped
from the result - including their through models for many-to-many
relations.
This differs from `fields()` on a regular ORM-loading query (`get`,
`all`, etc.), where unmentioned related models in `select_related()`
are still loaded in full so model construction does not lose
mandatory columns.
!!!note
Note that in contrary to other queryset methods here you can exclude the
in-between models but keep the end columns, which does not make sense
when parsing the raw data into models.
So in relation category -> category_x_post -> post -> user you can exclude
category_x_post and post models but can keep the user one. (in ormar model
context that is not possible as if you would exclude through and post model
there would be no way to reach user model from category model).
```python
user = (
await Role.objects.select_related("users__categories")
.filter(name="admin")
.fields({"name": ..., "users": {"name": ..., "categories": {"name"}}})
.exclude_fields("roleuser")
.values_list()
)
assert user == [("admin", "Anonymous", "News")]
```
If you have multiple ManyToMany models in your query you would have to exclude each through model manually.
To avoid this burden `ormar` provides you with `exclude_through=False` parameter.
If you set this flag to True **all through models will be fully excluded**.
```python
# equivalent to query above, note lack of exclude_fields call
user = (
await Role.objects.select_related("users__categories")
.filter(name="admin")
.fields({"name": ..., "users": {"name": ..., "categories": {"name"}}})
.values_list(exclude_through=True)
)
assert user == [("admin", "Anonymous", "News")]
```
Use flatten to get list of values.
```python
# using flatten with more than one field will raise exception!
await Role.objects.fields({"name", "id"}).values_list(flatten=True)
# proper usage
roles = await Role.objects.fields("name").values_list(flatten=True)
assert roles == ["admin", "editor"]
```
## QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey`
returns the list of related models.
But at the same time it exposes subset of QuerySet API, so you can filter, create,
select related etc related models directly from parent model.
!!!warning
Because using `values` and `values_list` skips parsing of the models and validation, in contrast to all other read methods in querysetproxy those 2 **does not clear currently loaded related models** and **does not overwrite the currently loaded models** with result of own call!
### values
Works exactly the same as [values](./#values) function above but allows you to fetch related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### values_list
Works exactly the same as [values_list](./#values_list) function above but allows
you to query or create related objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
[querysetproxy]: ../relations/queryset-proxy.md
ormar-orm-ormar-a7e7fde/docs/queries/read.md 0000664 0000000 0000000 00000026576 15211567312 0021214 0 ustar 00root root 0000000 0000000 # Read data from database
Following methods allow you to load data from the database.
* `get(*args, **kwargs) -> Model`
* `get_or_none(*args, **kwargs) -> Optional[Model]`
* `get_or_create(_defaults: Optional[dict[str, Any]] = None, *args, **kwargs) -> tuple[Model, bool]`
* `first(*args, **kwargs) -> Model`
* `first_or_none(*args, **kwargs) -> Optional[Model]`
* `last(*args, **kwargs) -> Model`
* `last_or_none(*args, **kwargs) -> Optional[Model]`
* `all(*args, **kwargs) -> list[Optional[Model]]`
* `iterate(*args, **kwargs) -> AsyncGenerator[Model]`
* `Model`
* `Model.load()` method
* `QuerysetProxy`
* `QuerysetProxy.get(*args, **kwargs)` method
* `QuerysetProxy.get_or_none(*args, **kwargs)` method
* `QuerysetProxy.get_or_create(_defaults: Optional[dict[str, Any]] = None, *args, **kwargs)` method
* `QuerysetProxy.first(*args, **kwargs)` method
* `QuerysetProxy.first_or_none(*args, **kwargs)` method
* `QuerysetProxy.last(*args, **kwargs)` method
* `QuerysetProxy.last_or_none(*args, **kwargs)` method
* `QuerysetProxy.all(*args, **kwargs)` method
## get
`get(*args, **kwargs) -> Model`
Gets a single row from the db matching the criteria set by args/kwargs.
When criteria are set (either through args/kwargs or via a chained `filter`/`exclude`)
the query fetches every matching row and asserts that exactly one row comes back.
If no criteria are set, `get()` falls back to returning the last row of the table
ordered by primary key descending.
Passing a criteria is equivalent to calling `filter(*args, **kwargs).get()`.
```python
class Track(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="track"
)
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
name: str = ormar.String(max_length=100)
position: int = ormar.Integer()
```
```python
track = await Track.objects.get(name='The Bird')
# note that above is equivalent to await Track.objects.filter(name='The Bird').get()
track2 = track = await Track.objects.get()
track == track2
# True since it's the only row in db in our example
# and get without arguments returns the last row by pk column desc
```
!!!warning
If no row meets the criteria `NoMatch` exception is raised.
If criteria are set (via `filter`/`exclude` or args/kwargs) and more than
one row matches them, the `MultipleMatches` exception is raised. The no-criteria
fallback always returns a single row and cannot raise `MultipleMatches`.
## get_or_none
`get_or_none(*args, **kwargs) -> Model`
Exact equivalent of get described above but instead of raising the exception returns `None` if no db record matching the criteria is found.
## get_or_create
`get_or_create(_defaults: Optional[dict[str, Any]] = None, *args, **kwargs) -> tuple[Model, bool]`
Combination of create and get methods.
Tries to get a row meeting the criteria and if `NoMatch` exception is raised it creates
a new one with given kwargs and _defaults.
```python
class Album(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="album")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
year: int = ormar.Integer()
```
```python
album, created = await Album.objects.get_or_create(name='The Cat', _defaults={"year": 1999})
assert created is True
assert album.name == "The Cat"
assert album.year == 1999
# object is created as it does not exist
album2, created = await Album.objects.get_or_create(name='The Cat')
assert created is False
assert album == album2
# return True as the same db row is returned
```
!!!warning
Despite being an equivalent row from database the `album` and `album2` in
example above are 2 different python objects!
Updating one of them will not refresh the second one until you explicitly load() the
fresh data from db.
!!!note
Note that if you want to create a new object you either have to pass pk column
value or pk column has to be set as autoincrement
!!!note
Concurrent calls fall back to a second `get` if `create` trips a unique
constraint, returning the winning row with `created=False`. Wrap in
`database.transaction()` for stricter atomicity.
## first
`first(*args, **kwargs) -> Model`
Gets the first row from the db ordered by primary key column ascending.
Passing args and/or kwargs is a shortcut and equals to calling
`filter(*args, **kwargs).first()`.
The query is always executed with `LIMIT 1` so at most one row is fetched —
unlike `get()`, `first()` does not assert that the criteria match exactly one
row and never raises `MultipleMatches`. Use `get()` when you want that
assertion.
```python
class Album(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="album")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
```
```python
await Album.objects.create(name='The Cat')
await Album.objects.create(name='The Dog')
album = await Album.objects.first()
# first row by primary_key column asc
assert album.name == 'The Cat'
```
!!!warning
If no row meets the criteria `NoMatch` exception is raised.
## first_or_none
`first_or_none(*args, **kwargs) -> Optional[Model]`
Exact equivalent of `first` described above but instead of raising `NoMatch`
returns `None` if no db record matching the criteria is found.
```python
empty = await Album.objects.first_or_none()
# None — no rows in the table
missing = await Album.objects.first_or_none(name='The Missing')
# None — no row matches the filter
```
## last
`last(*args, **kwargs) -> Model`
Gets the last row from the db ordered by primary key column descending.
Complementary to `first()` — the default pk ordering is flipped and the top
row is returned. When you combine `last()` with `order_by(...)`, the user's
ordering is flipped too, so `order_by("name").last()` returns the row that
would sort last by name.
```python
await Album.objects.create(name='The Cat')
await Album.objects.create(name='The Dog')
album = await Album.objects.last()
# last row by primary_key column desc
assert album.name == 'The Dog'
album = await Album.objects.order_by("name").last()
# last row by name (alphabetical)
assert album.name == 'The Dog'
```
!!!warning
Same as `first()` — raises `NoMatch` if no rows match. Like `first()`,
`last()` always runs with `LIMIT 1` and never raises `MultipleMatches`;
use `get()` if you want that assertion.
## last_or_none
`last_or_none(*args, **kwargs) -> Optional[Model]`
Exact equivalent of `last` described above but instead of raising `NoMatch`
returns `None` if no db record matching the criteria is found.
```python
empty = await Album.objects.last_or_none()
# None — no rows in the table
```
## all
`all(*args, **kwargs) -> list[Optional["Model"]]`
Returns all rows from a database for given model for set filter options.
Passing kwargs is a shortcut and equals to calling `filter(*args, **kwargs).all()`.
If there are no rows meeting the criteria an empty list is returned.
```python
class Album(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="track")
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
title: str = ormar.String(max_length=100)
position: int = ormar.Integer()
```
```python
tracks = await Track.objects.select_related("album").all(album__title='Sample')
# will return a list of all Tracks for album Sample
# for more on joins visit joining and subqueries section
tracks = await Track.objects.all()
# will return a list of all Tracks in database
```
## iterate
`iterate(*args, **kwargs) -> AsyncGenerator["Model"]`
Return async iterable generator for all rows from a database for given model.
Passing args and/or kwargs is a shortcut and equals to calling `filter(*args, **kwargs).iterate()`.
If there are no rows meeting the criteria an empty async generator is returned.
```python
class Album(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="album")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
```
```python
await Album.objects.create(name='The Cat')
await Album.objects.create(name='The Dog')
# will asynchronously iterate all Album models yielding one main model at a time from the generator
async for album in Album.objects.iterate():
print(album.name)
# The Cat
# The Dog
```
!!!warning
Use of `iterate()` causes previous `prefetch_related()` calls to be ignored;
since these two optimizations do not make sense together.
If `iterate()` & `prefetch_related()` are used together the `QueryDefinitionError` exception is raised.
## Model methods
Each model instance have a set of methods to `save`, `update` or `load` itself.
### load
You can load the `ForeignKey` related model by calling `load()` method.
`load()` can be used to refresh the model from the database (if it was changed by some other process).
!!!tip
Read more about `load()` method in [models methods](../models/methods.md#load)
## QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey`
returns the list of related models.
But at the same time it exposes subset of QuerySet API, so you can filter, create,
select related etc related models directly from parent model.
### get
Works exactly the same as [get](./#get) function above but allows you to fetch related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### get_or_none
Exact equivalent of get described above but instead of raising the exception returns `None` if no db record matching the criteria is found.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### get_or_create
Works exactly the same as [get_or_create](./#get_or_create) function above but allows
you to query or create related objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### first
Works exactly the same as [first](./#first) function above but allows you to query
related objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### first_or_none
Works exactly the same as [first_or_none](./#first_or_none) function above but
returns `None` instead of raising `NoMatch`, and works on the relation side.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### last
Works exactly the same as [last](./#last) function above but allows you to query
related objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### last_or_none
Works exactly the same as [last_or_none](./#last_or_none) function above but
returns `None` instead of raising `NoMatch`, and works on the relation side.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### all
Works exactly the same as [all](./#all) function above but allows you to query related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
[querysetproxy]: ../relations/queryset-proxy.md
ormar-orm-ormar-a7e7fde/docs/queries/select-columns.md 0000664 0000000 0000000 00000031121 15211567312 0023214 0 ustar 00root root 0000000 0000000 # Selecting subset of columns
To select only chosen columns of your model you can use following functions.
* `fields(columns: Union[list, str, set, dict]) -> QuerySet`
* `exclude_fields(columns: Union[list, str, set, dict]) -> QuerySet`
* `flatten_fields(columns: Union[list, str, set, tuple, dict, FieldAccessor]) -> QuerySet`
* `QuerysetProxy`
* `QuerysetProxy.fields(columns: Union[list, str, set, dict])` method
* `QuerysetProxy.exclude_fields(columns: Union[list, str, set, dict])` method
## fields
`fields(columns: Union[list, str, set, dict]) -> QuerySet`
With `fields()` you can select subset of model columns to limit the data load.
!!!note
Note that `fields()` and `exclude_fields()` works both for main models (on
normal queries like `get`, `all` etc.)
as well as `select_related` and `prefetch_related` models (with nested notation).
Given a sample data like following:
```python
--8<-- "../docs_src/select_columns/docs001.py"
```
You can select specified fields by passing a `str, list[str], set[str] or dict` with
nested definition.
To include related models use
notation `{related_name}__{column}[__{optional_next} etc.]`.
```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'])
# 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.
If you include related models into `select_related()` call but you won't specify columns
for those models in fields
- implies a list of all fields for those nested models.
```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
```
!!!warning
Mandatory fields cannot be excluded as it will raise `ValidationError`, to
exclude a field it has to be nullable.
The `values()` method can be used to exclude mandatory fields, though data will
be returned as a `dict`.
You cannot exclude mandatory model columns - `manufacturer__name` in this example.
```python
await (
Car.objects
.select_related('manufacturer')
.fields(['id', 'name', 'manufacturer__founded'])
.all()
)
# will raise pydantic ValidationError as company.name is required
```
!!!tip
Pk column cannot be excluded - it's always auto added even if not explicitly
included.
You can also pass fields to include as dictionary or set.
To mark a field as included in a dictionary use it's name as key and ellipsis as value.
To traverse nested models use nested dictionaries.
To include fields at last level instead of nested dictionary a set can be used.
To include whole nested model specify model related field name and ellipsis.
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()
)
# 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()
)
```
!!!note
All methods that do not return the rows explicitly returns a QuerySet instance so
you can chain them together
So operations like `filter()`, `select_related()`, `limit()` and `offset()` etc. can be chained.
Something like `Track.objects.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
## exclude_fields
`exclude_fields(columns: Union[list, str, set, dict]) -> QuerySet`
With `exclude_fields()` you can select subset of model columns that will be excluded to
limit the data load.
It's the opposite of `fields()` method so check documentation above to see what options
are available.
Especially check above how you can pass also nested dictionaries and sets as a mask to
exclude fields from whole hierarchy.
!!!note
Note that `fields()` and `exclude_fields()` works both for main models (on
normal queries like `get`, `all` etc.)
as well as `select_related` and `prefetch_related` models (with nested notation).
Below you can find few simple examples:
```python
--8<-- "../docs_src/select_columns/docs001.py"
```
```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
# 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 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()
)
# will raise pydantic ValidationError as company.name is required
```
!!!warning
Mandatory fields cannot be excluded as it will raise `ValidationError`, to
exclude a field it has to be nullable.
The `values()` method can be used to exclude mandatory fields, though data will
be returned as a `dict`.
!!!tip
Pk column cannot be excluded - it's always auto added even if explicitly
excluded.
!!!note
All methods that do not return the rows explicitly returns a QuerySet instance so
you can chain them together
So operations like `filter()`, `select_related()`, `limit()` and `offset()` etc. can be chained.
Something like `Track.object.select_related("album").filter(album__name="Malibu").offset(1).limit(1).all()`
## flatten_fields
`flatten_fields(columns: Union[list, str, set, tuple, dict, FieldAccessor]) -> QuerySet`
With `flatten_fields()` you can render selected related models as their primary
key value on `model_dump()` instead of the default nested dict. This is useful
when your API clients expect `{"manufacturer": 1}` rather than
`{"manufacturer": {"id": 1, "name": "Toyota", ...}}`.
Accepts the same input forms as `fields()` / `exclude_fields()` (string, list,
set, tuple, dict-with-Ellipsis) plus `FieldAccessor` / list of accessors.
Works across foreign keys, many-to-many, and reverse relations.
```python hl_lines="1-6"
all_cars = await (
Car.objects
.select_related('manufacturer')
.flatten_fields('manufacturer')
.all()
)
assert all_cars[0].model_dump() == {
'id': 1,
'name': 'Corolla',
'manufacturer': 1, # flattened from nested dict to pk value
}
```
The same can be written in nested-dict form:
```python
Car.objects.flatten_fields({'manufacturer': ...})
```
Or with a `FieldAccessor`:
```python
Car.objects.flatten_fields(Car.manufacturer)
```
Deeply nested relations use `__`:
```python hl_lines="1-6"
cars = await (
Car.objects
.select_related('manufacturer__hq')
.flatten_fields('manufacturer__hq')
.all()
)
assert cars[0].model_dump()['manufacturer']['hq'] == 7 # just the hq pk
```
Lists of pks for many-to-many and reverse relations:
```python
posts = await Post.objects.flatten_fields('categories').all()
posts[0].model_dump()['categories'] # [1, 2, 3]
```
!!!note
Relations listed in `flatten_fields()` are **auto-loaded** — single-valued
foreign keys are added to `select_related()`, many-to-many and reverse
relations to `prefetch_related()`. You don't have to load them yourself.
### flatten_all on model_dump
`model.model_dump(flatten_all=True)` collapses every related model at every depth
to its primary key in one shot. `model.model_dump(flatten_fields=...)` accepts
the same input forms as the queryset method and works even on models not loaded
via a queryset.
```python
car.model_dump(flatten_all=True)
# {"id": 1, "name": "Corolla", "manufacturer": 1, "lead_manager": 2}
car.model_dump(flatten_fields={'manufacturer': ...})
# {"id": 1, "name": "Corolla", "manufacturer": 1, "lead_manager": {...}}
```
### Validation rules
Flatten directives conflict with sub-field selection on the flattened relation
— you can't attach children to a scalar pk. The following raise
`QueryDefinitionError`:
* `flatten_fields('manufacturer')` combined with `fields({'manufacturer': {'name'}})`
* `flatten_fields('manufacturer')` combined with `exclude_fields({'manufacturer': {'name'}})`
* `flatten_fields(['manufacturer', 'manufacturer__hq'])` — the deeper path is unreachable
* `flatten_fields('name')` on a non-relation column
* `flatten_fields('manufacturer__nonexistent')` — unknown relation
* `model_dump(flatten_all=True, exclude_primary_keys=True)` — directly contradictory
Whole-relation `include`/`exclude` (e.g. `fields({'manufacturer'})`) is fine
alongside a flatten directive.
Filtering *through* a flattened relation still works — the join is generated
for the filter, and the rendered output is just the pk:
```python
await (
Car.objects
.filter(manufacturer__hq__city='Tokyo')
.flatten_fields('manufacturer')
.all()
)
# join visits hq for the filter; manufacturer still rendered as its pk
```
## QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey`
returns the list of related models.
But at the same time it exposes subset of QuerySet API, so you can filter, create,
select related etc related models directly from parent model.
### fields
Works exactly the same as [fields](./#fields) function above but allows you to select columns from related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### exclude_fields
Works exactly the same as [exclude_fields](./#exclude_fields) function above but allows you to select columns from related
objects from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
### flatten_fields
Works exactly the same as [flatten_fields](./#flatten_fields) function above
but applied to the related-side queryset.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
[querysetproxy]: ../relations/queryset-proxy.md
ormar-orm-ormar-a7e7fde/docs/queries/update.md 0000664 0000000 0000000 00000007200 15211567312 0021542 0 ustar 00root root 0000000 0000000 # Update data in database
Following methods and functions allow updating existing data in the database.
* `update(each: bool = False, **kwargs) -> int`
* `update_or_create(**kwargs) -> Model`
* `bulk_update(objects: list[Model], columns: list[str] = None) -> None`
* `Model`
* `Model.update()` method
* `Model.upsert()` method
* `Model.save_related()` method
* `QuerysetProxy`
* `QuerysetProxy.update_or_create(**kwargs)` method
## update
`update(each: bool = False, **kwargs) -> int`
QuerySet level update is used to update multiple records with the same value at once.
You either have to filter the QuerySet first or provide a `each=True` flag to update
whole table.
If you do not provide this flag or a filter a `QueryDefinitionError` will be raised.
Return number of rows updated.
```Python hl_lines="46-48"
--8<-- "../docs_src/queries/docs002.py"
```
!!!warning
Queryset needs to be filtered before updating to prevent accidental overwrite.
To update whole database table `each=True` needs to be provided as a safety switch
## update_or_create
`update_or_create(**kwargs) -> Model`
Updates the model, or in case there is no match in database creates a new one.
```Python hl_lines="44-52"
--8<-- "../docs_src/queries/docs003.py"
```
!!!note
Note that if you want to create a new object you either have to pass pk column
value or pk column has to be set as autoincrement
## bulk_update
`bulk_update(objects: list["Model"], columns: list[str] = None) -> None`
Allows to update multiple instance at once.
All `Models` passed need to have primary key column populated.
You can also select which fields to update by passing `columns` list as a list of string
names.
```python hl_lines="8"
# continuing the example from bulk_create
# update objects
for todo in todoes:
todo.completed = False
# perform update of all objects at once
# objects need to have pk column set, otherwise exception is raised
await ToDo.objects.bulk_update(todoes)
completed = await ToDo.objects.filter(completed=False).all()
assert len(completed) == 3
```
## Model methods
Each model instance have a set of methods to `save`, `update` or `load` itself.
###update
You can update models by updating your model attributes (fields) and calling `update()` method.
If you try to update a model without a primary key set a `ModelPersistenceError` exception will be thrown.
!!!tip
Read more about `update()` method in [models-update](../models/methods.md#update)
###upsert
It's a proxy to either `save()` or `update(**kwargs)` methods of a Model.
If the pk is set the `update()` method will be called.
!!!tip
Read more about `upsert()` method in [models-upsert][models-upsert]
###save_related
Method goes through all relations of the `Model` on which the method is called,
and calls `upsert()` method on each model that is **not** saved.
!!!tip
Read more about `save_related()` method in [models-save-related][models-save-related]
## QuerysetProxy methods
When access directly the related `ManyToMany` field as well as `ReverseForeignKey` returns the list of related models.
But at the same time it exposes subset of QuerySet API, so you can filter, create, select related etc related models directly from parent model.
### update_or_create
Works exactly the same as [update_or_create](./#update_or_create) function above but allows you to update or create related objects
from other side of the relation.
!!!tip
To read more about `QuerysetProxy` visit [querysetproxy][querysetproxy] section
[querysetproxy]: ../relations/queryset-proxy.md
[models-upsert]: ../models/methods.md#upsert
[models-save-related]: ../models/methods.md#save_related
ormar-orm-ormar-a7e7fde/docs/relations/ 0000775 0000000 0000000 00000000000 15211567312 0020262 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs/relations/foreign-key.md 0000664 0000000 0000000 00000026233 15211567312 0023031 0 ustar 00root root 0000000 0000000 # ForeignKey
`ForeignKey(to: Model, *, name: str = None, unique: bool = False, nullable: bool = True,
related_name: str = None, virtual: bool = False, onupdate: Union[ReferentialAction, str] = None,
ondelete: Union[ReferentialAction, str] = None, foreign_key_name: str = None, **kwargs: Any)`
has required parameters `to` that takes target `Model` class.
Sqlalchemy column and type are automatically taken from target `Model`.
* Sqlalchemy column: class of a target `Model` primary key column
* Type (used for pydantic): type of a target `Model`
## Defining Models
To define a relation add `ForeignKey` field that points to related `Model`.
```Python hl_lines="31"
--8<-- "../docs_src/fields/docs003.py"
```
## Reverse Relation
`ForeignKey` fields are automatically registering reverse side of the relation.
By default it's child (source) `Model` name + s, like courses in snippet below:
```Python hl_lines="34 41"
--8<-- "../docs_src/fields/docs001.py"
```
Reverse relation exposes API to manage related objects also from parent side.
### Skipping reverse relation
If you are sure you don't want the reverse relation you can use `skip_reverse=True`
flag of the `ForeignKey`.
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:
* 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 `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):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=80)
last_name: str = ormar.String(max_length=80)
class Post(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
author: Optional[Author] = ormar.ForeignKey(Author, skip_reverse=True)
# create sample data
author = Author(first_name="Test", last_name="Author")
post = Post(title="Test Post", author=author)
assert post.author == author # ok
assert author.posts # Attribute error!
# but still can use in order_by
authors = (
await Author.objects.select_related("posts").order_by("posts__title").all()
)
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 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()
assert authors[0].first_name == "Test"
assert len(authors) == 1
```
### add
Adding child model from parent side causes adding related model to currently loaded parent relation,
as well as sets child's model foreign key value and updates the model.
```python
department = await Department(name="Science").save()
course = Course(name="Math", completed=False) # note - not saved
await department.courses.add(course)
assert course.pk is not None # child model was saved
# relation on child model is set and FK column saved in db
assert course.department == department
# relation on parent model is also set
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
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()`.
!!!warning
This method will not work on `ManyToMany` relations - there, both sides of the relation have to be saved before adding to relation.
### remove
Removal of the related model one by one.
In reverse relation calling `remove()` does not remove the child model, but instead nulls it ForeignKey value.
```python
# continuing from above
await department.courses.remove(course)
assert len(department.courses) == 0
# course still exists and was saved in remove
assert course.pk is not None
assert course.department is None
# to remove child from db
await course.delete()
```
But if you want to clear the relation and delete the child at the same time you can issue:
```python
# this will not only clear the relation
# but also delete related course from db
await department.courses.remove(course, keep_reversed=False)
```
### clear
Removal of all related models in one call.
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
await department.courses.clear()
```
If you want to remove the children altogether from the database, set `keep_reversed=False`
```python
# deletes from db all courses related to this department
await department.courses.clear(keep_reversed=False)
```
## QuerysetProxy
Reverse relation exposes QuerysetProxy API that allows you to query related model like you would issue a normal Query.
To read which methods of QuerySet are available read below [querysetproxy][querysetproxy]
## related_name
You can overwrite related model field name by providing `related_name` parameter like below:
```Python hl_lines="28-30 36"
--8<-- "../docs_src/fields/docs002.py"
```
!!!tip
The reverse relation on access returns list of `wekref.proxy` to avoid circular references.
!!!warning
When you provide multiple relations to the same model `ormar` can no longer auto generate
the `related_name` for you. Therefore, in that situation you **have to** provide `related_name`
for all but one (one can be default and generated) or all related fields.
## Referential Actions
When an object referenced by a ForeignKey is changed (deleted or updated),
ormar will set the SQL constraint specified by the `ondelete` and `onupdate` argument.
The possible values for `ondelete` and `onupdate` are found in `ormar.ReferentialAction`:
!!!note
Instead of `ormar.ReferentialAction`, you can directly pass string values to these two arguments, but this is not recommended because it will break the integrity.
### CASCADE
Whenever rows in the parent (referenced) table are deleted (or updated), the respective rows of the child (referencing) table with a matching foreign key column will be deleted (or updated) as well. This is called a cascade delete (or update).
### RESTRICT
A value cannot be updated or deleted when a row exists in a referencing or child table that references the value in the referenced table.
Similarly, a row cannot be deleted as long as there is a reference to it from a referencing or child table.
### SET_NULL
Set the ForeignKey to `None`; this is only possible if `nullable` is True.
### SET_DEFAULT
Set the ForeignKey to its default value; a `server_default` for the ForeignKey must be set.
!!!note
Note that the `default` value is not allowed and you must do this through `server_default`, which you can read about in [this section][server_default].
### DO_NOTHING
Take `NO ACTION`; NO ACTION and RESTRICT are very much alike. The main difference between NO ACTION and RESTRICT is that with NO ACTION the referential integrity check is done after trying to alter the table. RESTRICT does the check before trying to execute the UPDATE or DELETE statement. Both referential actions act the same if the referential integrity check fails: the UPDATE or DELETE statement will result in an error.
## Overriding the foreign key constraint name
By default ormar generates the foreign key constraint name as
`fk_{source_table}_{target_table}_{target_pk}_{field_name}`. On databases with a
short identifier length limit (for example MySQL's 64 character limit) the
auto-generated name can be truncated or rejected. Pass `foreign_key_name` to use
a custom name instead:
```python
class Book(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
author = ormar.ForeignKey(
Author,
foreign_key_name="fk_books_author",
)
```
When used on an abstract base class, each subclass suffixes the name with its
own tablename to avoid constraint name collisions across sibling tables.
## Relation Setup
You have several ways to set-up a relationship connection.
### `Model` instance
The most obvious one is to pass a related `Model` instance to the constructor.
```Python hl_lines="35-36"
--8<-- "../docs_src/relations/docs001.py"
```
### Primary key value
You can setup the relation also with just the pk column value of the related model.
```Python hl_lines="38-39"
--8<-- "../docs_src/relations/docs001.py"
```
### Dictionary
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 `model_dump()` method.
```Python hl_lines="41-42"
--8<-- "../docs_src/relations/docs001.py"
```
### None
Finally you can explicitly set it to None (default behavior if no value passed).
```Python hl_lines="44-45"
--8<-- "../docs_src/relations/docs001.py"
```
You can also clear a `ForeignKey` relation on an already-loaded model by
assigning `None` — the field must be `nullable` for this to persist. The
related model is unregistered from the instance immediately, so subsequent
reads return `None` without requiring a reload:
```python
track = await Track.objects.select_related("album").get(id=track_id)
assert track.album is not None
track.album = None
assert track.album is None # in-memory state cleared
await track.update(_columns=["album"]) # persists NULL to the database
```
!!!note
Assigning `None` to a non-nullable `ForeignKey` is ignored at the
in-memory level. Make the field `nullable=True` (the default) if you
need to unassign it.
!!!warning
In all not None cases the primary key value for related model **has to exist in database**.
Otherwise an IntegrityError will be raised by your database driver library.
[queries]: ./queries.md
[querysetproxy]: ./queryset-proxy.md
[get]: ./queries.md#get
[all]: ./queries.md#all
[create]: ./queries.md#create
[get_or_create]: ./queries.md#get_or_create
[update_or_create]: ./queries.md#update_or_create
[filter]: ./queries.md#filter
[exclude]: ./queries.md#exclude
[select_related]: ./queries.md#select_related
[prefetch_related]: ./queries.md#prefetch_related
[limit]: ./queries.md#limit
[offset]: ./queries.md#offset
[count]: ./queries.md#count
[exists]: ./queries.md#exists
[fields]: ./queries.md#fields
[exclude_fields]: ./queries.md#exclude_fields
[order_by]: ./queries.md#order_by
[server_default]: ../fields/common-parameters.md#server-default
ormar-orm-ormar-a7e7fde/docs/relations/index.md 0000664 0000000 0000000 00000015216 15211567312 0021720 0 ustar 00root root 0000000 0000000 # Relations
Currently `ormar` supports two types of relations:
* One-to-many (and many-to-one) with `ForeignKey` field
* Many-to-many with `ManyToMany` field
Below you can find a very basic examples of definitions for each of those relations.
To read more about methods, possibilities, definition etc. please read the subsequent section of the documentation.
## ForeignKey
To define many-to-one relation use `ForeignKey` field.
```Python hl_lines="27"
--8<-- "../docs_src/relations/docs003.py"
```
!!!tip
To read more about one-to-many relations visit [foreign-keys][foreign-keys] section
## Reverse ForeignKey
The definition of one-to-many relation also uses `ForeignKey`, and it's registered for you automatically.
So in relation to example above.
```Python hl_lines="7-8"
class Department(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
# there is a virtual field here like follows
courses: Optional[list[Course]] = ormar.ForeignKey(Course, virtual=True)
# note that you DO NOT define it yourself, ormar does it for you.
```
!!!tip
To read more about many-to-one relations (i.e changing the name of generated field) visit [foreign-keys][foreign-keys] section
!!!tip
Reverse ForeignKey allows you to query the related models with [queryset-proxy][queryset-proxy].
It allows you to use `await department.courses.all()` to fetch data related only to specific department etc.
##ManyToMany
To define many-to-many relation use `ManyToMany` field.
```python hl_lines="19"
class Category(ormar.Model):
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):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[list[Category]] = ormar.ManyToMany(Category)
```
!!!tip
To read more about many-to-many relations visit [many-to-many][many-to-many] section
!!!tip
ManyToMany allows you to query the related models with [queryset-proxy][queryset-proxy].
It allows you to use `await post.categories.all()` but also `await category.posts.all()` to fetch data related only to specific post, category etc.
## Through fields
As part of the `ManyToMany` relation you can define a through model, that can contain additional
fields that you can use to filter, order etc. Fields defined like this are exposed on the reverse
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="12-21"
class Category(ormar.Model):
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):
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)
param_name: str = ormar.String(default="Name", max_length=200)
class Post(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories = ormar.ManyToMany(Category, through=PostCategory)
```
!!!tip
To read more about many-to-many relations and through fields visit [many-to-many][many-to-many] section
!!!tip
ManyToMany allows you to query the related models with [queryset-proxy][queryset-proxy].
It allows you to use `await post.categories.all()` but also `await category.posts.all()` to fetch data related only to specific post, category etc.
## Relationship default sort order
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
But you can modify the order in which related models are loaded during query by providing `orders_by` and `related_orders_by`
parameters to relations.
In relations you can sort only by directly related model columns or for `ManyToMany`
columns also `Through` model columns `{through_field_name}__{column_name}`
Sample configuration might look like this:
```python hl_lines="23"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
class Author(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(
Author, orders_by=["name"], related_orders_by=["-year"]
)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
ranking: int = ormar.Integer(nullable=True)
```
Now calls:
`await Author.objects.select_related("books").get()` - the books will be sorted by the book year descending
`await Book.objects.select_related("author").all()` - the authors will be sorted by author name ascending
## Self-reference and postponed references
In order to create auto-relation or create two models that reference each other in at least two
different relations (remember the reverse side is auto-registered for you), you need to use
`ForwardRef` from `typing` module.
```python hl_lines="1 9 12"
PersonRef = ForwardRef("Person")
class Person(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
supervisor: PersonRef = ormar.ForeignKey(PersonRef, related_name="employees")
Person.update_forward_refs()
```
!!!tip
To read more about self-reference and postponed relations visit [postponed-annotations][postponed-annotations] section
[foreign-keys]: ./foreign-key.md
[many-to-many]: ./many-to-many.md
[queryset-proxy]: ./queryset-proxy.md
[postponed-annotations]: ./postponed-annotations.md
ormar-orm-ormar-a7e7fde/docs/relations/many-to-many.md 0000664 0000000 0000000 00000036610 15211567312 0023140 0 ustar 00root root 0000000 0000000 # ManyToMany
`ManyToMany(to, through)` has required parameters `to` and optional `through` that takes target and relation `Model` classes.
Sqlalchemy column and Type are automatically taken from target `Model`.
* Sqlalchemy column: class of a target `Model` primary key column
* Type (used for pydantic): type of a target `Model`
## Defining Models
```Python hl_lines="35"
--8<-- "../docs_src/relations/docs002.py"
```
Create sample data:
```Python
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")
```
## Reverse relation
`ForeignKey` fields are automatically registering reverse side of the relation.
By default it's child (source) `Model` name + s, like `posts` in snippet below:
```python hl_lines="25-26"
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=40)
class Post(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[list[Category]] = ormar.ManyToMany(Category)
# create some sample data
post = await Post.objects.create(title="Hello, M2M")
news = await Category.objects.create(name="News")
await post.categories.add(news)
# now you can query and access from both sides:
post_check = Post.objects.select_related("categories").get()
assert post_check.categories[0] == news
# query through auto registered reverse side
category_check = Category.objects.select_related("posts").get()
assert category_check.posts[0] == post
```
Reverse relation exposes API to manage related objects also from parent side.
### related_name
By default, the related_name is generated in the same way as for the `ForeignKey` relation (class.name.lower()+'s'),
but in the same way you can overwrite this name by providing `related_name` parameter like below:
```Python
categories: Optional[Union[Category, list[Category]]] = ormar.ManyToMany(
Category, through=PostCategory, related_name="new_categories"
)
```
!!!warning
When you provide multiple relations to the same model `ormar` can no longer auto generate
the `related_name` for you. Therefore, in that situation you **have to** provide `related_name`
for all but one (one can be default and generated) or all related fields.
### Skipping reverse relation
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:
* `filter` by related models fields from reverse model
* `order_by` by related models fields from reverse model
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 `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):
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):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[list[Category]] = ormar.ManyToMany(Category, skip_reverse=True)
# create some sample data
post = await Post.objects.create(title="Hello, M2M")
news = await Category.objects.create(name="News")
await post.categories.add(news)
assert post.categories[0] == news # ok
assert news.posts # Attribute error!
# but still can use in order_by
categories = (
await Category.objects.select_related("posts").order_by("posts__title").all()
)
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 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()
assert categories[0].name == "News"
assert len(categories) == 1
```
## Through Model
Optionally if you want to add additional fields you can explicitly create and pass
the through model class.
```Python hl_lines="20-25 33"
--8<-- "../docs_src/relations/docs004.py"
```
!!!warning
Note that even of you do not provide through model it's going to be created for you automatically and
still has to be included in example in `alembic` migrations.
!!!tip
Note that you need to provide `through` model if you want to
customize the `Through` model name or the database table name of this model.
If you do not provide the Through field it will be generated for you.
The default naming convention is:
* for class name it's union of both classes name (parent+other) so in example above
it would be `PostCategory`
* for table name it similar but with underscore in between and s in the end of class
lowercase name, in example above would be `posts_categorys`
### Customizing Through relation names
By default `Through` model relation names default to related model name in lowercase.
So in example like this:
```python
... # course declaration omitted
class Student(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
courses = ormar.ManyToMany(Course)
# will produce default Through model like follows (example simplified)
class StudentCourse(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="students_courses")
id: int = ormar.Integer(primary_key=True)
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`:
* `through_relation_name` - name of the field leading to the model in which `ManyToMany` is declared
* `through_reverse_relation_name` - name of the field leading to the model to which `ManyToMany` leads to
Example:
```python
... # course declaration omitted
base_ormar_config = ormar.OrmarConfig(
database=DatabaseConnection("sqlite+aiosqlite:///db.sqlite"),
metadata=sqlalchemy.MetaData(),
)
class Student(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
courses = ormar.ManyToMany(Course,
through_relation_name="student_id",
through_reverse_relation_name="course_id")
# will produce Through model like follows (example simplified)
class StudentCourse(ormar.Model):
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
course_id = ormar.ForeignKey(Course) # set by through_reverse_relation_name
```
!!!note
Note that explicitly declaring relations in Through model is forbidden, so even if you
provide your own custom Through model you cannot change the names there and you need to use
same `through_relation_name` and `through_reverse_relation_name` parameters.
## Overriding foreign key constraint names on the through model
Auto-generated foreign key constraint names on the through model can be
overridden with:
* `through_foreign_key_name` - name of the FK constraint on the column that
references the model where `ManyToMany` is declared (the owner side).
* `through_reverse_foreign_key_name` - name of the FK constraint on the column
that references the target model.
This is primarily useful for databases with short identifier limits (for
example MySQL's 64 character limit) where the auto-generated name would be
truncated.
```python
class Student(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
courses = ormar.ManyToMany(
Course,
through_foreign_key_name="fk_sc_student",
through_reverse_foreign_key_name="fk_sc_course",
)
```
## Making through relation columns non-nullable
By default the auto-generated foreign key columns on the through table are
nullable (matching SQLAlchemy's default for non primary-key columns). This can
be overridden per column with:
* `through_relation_nullable` - controls nullability of the column pointing to
the model where `ManyToMany` is declared (the owner side). Defaults to `True`.
* `through_reverse_relation_nullable` - controls nullability of the column
pointing to the target model. Defaults to `True`.
Set either (or both) to `False` when you want the database to enforce that a
through row always references both sides.
```python
class Student(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
courses = ormar.ManyToMany(
Course,
through_relation_nullable=False,
through_reverse_relation_nullable=False,
)
```
## Through Fields
The through field is auto added to the reverse side of the relation.
The exposed field is named as lowercase `Through` class name.
The exposed field **explicitly has no relations loaded** as the relation is already populated in `ManyToMany` field,
so it's useful only when additional fields are provided on `Through` model.
In a sample model setup as following:
```Python hl_lines="20-25 33"
--8<-- "../docs_src/relations/docs004.py"
```
the through field can be used as a normal model field in most of the QuerySet operations.
Note that through field is attached only to related side of the query so:
```python
post = await Post.objects.select_related("categories").get()
# source model has no through field
assert post.postcategory is None
# related models have through field
assert post.categories[0].postcategory is not None
# same is applicable for reversed query
category = await Category.objects.select_related("posts").get()
assert category.postcategory is None
assert category.posts[0].postcategory is not None
```
Through field can be used for filtering the data.
```python
post = (
await Post.objects.select_related("categories")
.filter(postcategory__sort_order__gt=1)
.get()
)
```
!!!tip
Note that despite that the actual instance is not populated on source model,
in queries, order by statements etc you can access through model from both sides.
So below query has exactly the same effect (note access through `categories`)
```python
post = (
await Post.objects.select_related("categories")
.filter(categories__postcategory__sort_order__gt=1)
.get()
)
```
Through model can be used in order by queries.
```python
post = (
await Post.objects.select_related("categories")
.order_by("-postcategory__sort_order")
.get()
)
```
You can also select subset of the columns in a normal `QuerySet` way with `fields`
and `exclude_fields`.
```python
post2 = (
await Post.objects.select_related("categories")
.exclude_fields("postcategory__param_name")
.get()
)
```
!!!warning
Note that because through fields explicitly nullifies all relation fields, as relation
is populated in ManyToMany field, you should not use the standard model methods like
`save()` and `update()` before re-loading the field from database.
If you want to modify the through field in place remember to reload it from database.
Otherwise you will set relations to None so effectively make the field useless!
```python
# always reload the field before modification
await post2.categories[0].postcategory.load()
# only then update the field
await post2.categories[0].postcategory.update(sort_order=3)
```
Note that reloading the model effectively reloads the relations as `pk_only` models
(only primary key is set) so they are not fully populated, but it's enough to preserve
the relation on update.
!!!warning
If you use i.e. `fastapi` the partially loaded related models on through field might cause
`pydantic` validation errors (that's the primary reason why they are not populated by default).
So either you need to exclude the related fields in your response, or fully load the related
models. In example above it would mean:
```python
await post2.categories[0].postcategory.post.load()
await post2.categories[0].postcategory.category.load()
```
Alternatively you can use `load_all()`:
```python
await post2.categories[0].postcategory.load_all()
```
**Preferred way of update is through queryset proxy `update()` method**
```python
# filter the desired related model with through field and update only through field params
await post2.categories.filter(name='Test category').update(postcategory={"sort_order": 3})
```
## Relation methods
### add
`add(item: Model, **kwargs)`
Allows you to add model to ManyToMany relation.
```python
# Add a category to a post.
await post.categories.add(news)
# or from the other end:
await news.posts.add(post)
```
!!!warning
In all not `None` cases the primary key value for related model **has to exist in database**.
Otherwise an IntegrityError will be raised by your database driver library.
If you declare your models with a Through model with additional fields, you can populate them
during adding child model to relation.
In order to do so, pass keyword arguments with field names and values to `add()` call.
Note that this works only for `ManyToMany` relations.
```python
post = await Post(title="Test post").save()
category = await Category(name="Test category").save()
# apart from model pass arguments referencing through model fields
await post.categories.add(category, sort_order=1, param_name='test')
```
### remove
Removal of the related model one by one.
Removes also the relation in the database.
```python
await news.posts.remove(post)
```
### clear
Removal of all related models in one call.
Removes also the relation in the database.
```python
await news.posts.clear()
```
### QuerysetProxy
Reverse relation exposes QuerysetProxy API that allows you to query related model like you would issue a normal Query.
To read which methods of QuerySet are available read below [querysetproxy][querysetproxy]
[queries]: ./queries.md
[querysetproxy]: ./queryset-proxy.md
[get]: ./queries.md#get
[all]: ./queries.md#all
[create]: ./queries.md#create
[get_or_create]: ./queries.md#get_or_create
[update_or_create]: ./queries.md#update_or_create
[filter]: ./queries.md#filter
[exclude]: ./queries.md#exclude
[select_related]: ./queries.md#select_related
[prefetch_related]: ./queries.md#prefetch_related
[limit]: ./queries.md#limit
[offset]: ./queries.md#offset
[count]: ./queries.md#count
[exists]: ./queries.md#exists
[fields]: ./queries.md#fields
[exclude_fields]: ./queries.md#exclude_fields
[order_by]: ./queries.md#order_by
ormar-orm-ormar-a7e7fde/docs/relations/postponed-annotations.md 0000664 0000000 0000000 00000010644 15211567312 0025157 0 ustar 00root root 0000000 0000000 # Postponed annotations
## Self-referencing Models
When you want to reference the same model during declaration to create a
relation you need to declare the referenced model as a `ForwardRef`, as during the declaration
the class is not yet ready and python by default won't let you reference it.
Although you might be tempted to use __future__ annotations or simply quote the name with `""` it won't work
as `ormar` is designed to work with explicitly declared `ForwardRef`.
First, you need to import the required ref from typing.
```python
from typing import ForwardRef
```
Now we need a sample model and a reference to the same model,
which will be used to create a self referencing relation.
```python
# create the forwardref to model Person
PersonRef = ForwardRef("Person")
class Person(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
# use the forwardref as to parameter
supervisor: PersonRef = ormar.ForeignKey(PersonRef, related_name="employees")
```
That's so simple. But before you can use the model you need to manually update the references
so that they lead to the actual models.
!!!warning
If you try to use the model without updated references, `ModelError` exception will be raised.
So in our example above any call like following will cause exception
```python
# creation of model - exception
await Person.objects.create(name="Test")
# initialization of model - exception
Person2(name="Test")
# usage of model's QuerySet - exception
await Person2.objects.get()
```
To update the references call the `update_forward_refs` method on **each model**
with forward references, only **after all related models were declared.**
So in order to make our previous example work we need just one extra line.
```python hl_lines="14"
PersonRef = ForwardRef("Person")
class Person(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
supervisor: PersonRef = ormar.ForeignKey(PersonRef, related_name="employees")
Person.update_forward_refs()
```
Of course the same can be done with ManyToMany relations in exactly same way, both for to
and through parameters.
```python
# declare the reference
ChildRef = ForwardRef("Child")
class ChildFriend(ormar.Model):
ormar_config = base_ormar_config.copy()
class Child(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
# use it in relation
friends = ormar.ManyToMany(ChildRef, through=ChildFriend,
related_name="also_friends")
Child.update_forward_refs()
```
## Cross model relations
The same mechanism and logic as for self-reference model can be used to link multiple different
models between each other.
Of course `ormar` links both sides of relation for you,
creating a reverse relation with specified (or default) `related_name`.
But if you need two (or more) relations between any two models, that for whatever reason
should be stored on both sides (so one relation is declared on one model,
and other on the second model), you need to use `ForwardRef` to achieve that.
Look at the following simple example.
```python
# teacher is not yet defined
TeacherRef = ForwardRef("Teacher")
class Student(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
# so we use reference instead of actual model
primary_teacher: TeacherRef = ormar.ForeignKey(TeacherRef,
related_name="own_students")
class StudentTeacher(ormar.Model):
ormar_config = base_ormar_config.copy(tablename='students_x_teachers')
class Teacher(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
# we need students for other relation hence the order
students = ormar.ManyToMany(Student, through=StudentTeacher,
related_name="teachers")
# now the Teacher model is already defined we can update references
Student.update_forward_refs()
```
!!!warning
Remember that `related_name` needs to be unique across related models regardless
of how many relations are defined.
ormar-orm-ormar-a7e7fde/docs/relations/queryset-proxy.md 0000664 0000000 0000000 00000023413 15211567312 0023647 0 ustar 00root root 0000000 0000000 # QuerySetProxy
When access directly the related `ManyToMany` field as well as `ReverseForeignKey` returns the list of related models.
But at the same time it exposes subset of QuerySet API, so you can filter, create, select related etc related models directly from parent model.
!!!note
By default exposed QuerySet is already filtered to return only `Models` related to parent `Model`.
So if you issue `post.categories.all()` you will get all categories related to that post, not all in table.
!!!note
Note that when accessing QuerySet API methods through QuerysetProxy you don't
need to use `objects` attribute like in normal queries.
So note that it's `post.categories.all()` and **not** `post.categories.objects.all()`.
To learn more about available QuerySet methods visit [queries][queries]
!!!warning
Querying related models from ManyToMany cleans list of related models loaded on parent model:
Example: `post.categories.first()` will set post.categories to list of 1 related model -> the one returned by first()
Example 2: if post has 4 categories so `len(post.categories) == 4` calling `post.categories.limit(2).all()`
-> will load only 2 children and now `assert len(post.categories) == 2`
This happens for all QuerysetProxy methods returning data: `get`, `all` and `first` and in `get_or_create` if model already exists.
Note that value returned by `create` or created in `get_or_create` and `update_or_create`
if model does not exist will be added to relation list (not clearing it).
## Read data from database
### get
`get(**kwargs): -> Model`
To grab just one of related models filtered by name you can use `get(**kwargs)` method.
```python
# grab one category
assert news == await post.categories.get(name="News")
# note that method returns the category so you can grab this value
# but it also modifies list of related models in place
# so regardless of what was previously loaded on parent model
# now it has only one value -> just loaded with get() call
assert len(post.categories) == 1
assert post.categories[0] == news
```
!!!tip
Read more in queries documentation [get][get]
### get_or_create
`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.
!!!tip
Read more in queries documentation [get_or_create][get_or_create]
### all
`all(**kwargs) -> list[Optional["Model"]]`
To get a list of related models use `all()` method.
Note that you can filter the queryset, select related, exclude fields etc. like in normal query.
```python
# with all Queryset methods - filtering, selecting columns, counting etc.
await news.posts.filter(title__contains="M2M").all()
await Category.objects.filter(posts__author=guido).get()
# columns models of many to many relation can be prefetched
news_posts = await news.posts.select_related("author").all()
assert news_posts[0].author == guido
```
!!!tip
Read more in queries documentation [all][all]
### iterate
`iterate(**kwargs) -> AsyncGenerator["Model"]`
To iterate on related models use `iterate()` method.
Note that you can filter the queryset, select related, exclude fields etc. like in normal query.
```python
# iterate on categories of this post with an async generator
async for category in post.categories.iterate():
print(category.name)
```
!!!tip
Read more in queries documentation [iterate][iterate]
## Insert/ update data into database
### create
`create(**kwargs): -> Model`
Create related `Model` directly from parent `Model`.
The link table is automatically populated, as well as relation ids in the database.
```python
# Creating columns object from instance:
await post.categories.create(name="Tips")
assert len(await post.categories.all()) == 2
# newly created instance already have relation persisted in the database
```
!!!tip
Read more in queries documentation [create][create]
For `ManyToMany` relations there is an additional functionality of passing parameters
that will be used to create a through model if you declared additional fields on explicitly
provided Through model.
Given sample like this:
```Python hl_lines="20-25 33"
--8<-- "../docs_src/relations/docs004.py"
```
You can populate fields on through model in the `create()` call in a following way:
```python
post = await Post(title="Test post").save()
await post.categories.create(
name="Test category1",
# in arguments pass a dictionary with name of the through field and keys
# corresponding to through model fields
postcategory={"sort_order": 1, "param_name": "volume"},
)
```
### get_or_create
`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.
!!!tip
Read more in queries documentation [get_or_create][get_or_create]
### update_or_create
`update_or_create(**kwargs) -> Model`
Updates the model, or in case there is no match in database creates a new one.
!!!tip
Read more in queries documentation [update_or_create][update_or_create]
### update
`update(**kwargs, each:bool = False) -> int`
Updates the related model with provided keyword arguments, return number of updated rows.
!!!tip
Read more in queries documentation [update][update]
Note that for `ManyToMany` relations update can also accept an argument with through field
name and a dictionary of fields.
```Python hl_lines="20-25 33"
--8<-- "../docs_src/relations/docs004.py"
```
In example above you can update attributes of `postcategory` in a following call:
```python
await post.categories.filter(name="Test category3").update(
postcategory={"sort_order": 4}
)
```
## Filtering and sorting
### filter
`filter(*args, **kwargs) -> QuerySet`
Allows you to filter by any Model attribute/field as well as to fetch instances, with a filter across an FK relationship.
!!!tip
Read more in queries documentation [filter][filter]
### exclude
`exclude(*args, **kwargs) -> QuerySet`
Works exactly the same as filter and all modifiers (suffixes) are the same, but returns a not condition.
!!!tip
Read more in queries documentation [exclude][exclude]
### order_by
`order_by(columns:Union[list, str]) -> QuerySet`
With order_by() you can order the results from database based on your choice of fields.
!!!tip
Read more in queries documentation [order_by][order_by]
## Joins and subqueries
### select_related
`select_related(related: Union[list, str]) -> QuerySet`
Allows to prefetch related models during the same query.
With select_related always only one query is run against the database, meaning that one (sometimes complicated) join is generated and later nested models are processed in python.
!!!tip
Read more in queries documentation [select_related][select_related]
### prefetch_related
`prefetch_related(related: Union[list, str]) -> QuerySet`
Allows to prefetch related models during query - but opposite to select_related each subsequent model is fetched in a separate database query.
With prefetch_related always one query per Model is run against the database, meaning that you will have multiple queries executed one after another.
!!!tip
Read more in queries documentation [prefetch_related][prefetch_related]
## Pagination and rows number
### paginate
`paginate(page: int, page_size: int = 20) -> QuerySet`
Combines the offset and limit methods based on page number and size.
!!!tip
Read more in queries documentation [paginate][paginate]
### limit
`limit(limit_count: int) -> QuerySet`
You can limit the results to desired number of parent models.
!!!tip
Read more in queries documentation [limit][limit]
### offset
`offset(offset: int) -> QuerySet`
You can offset the results by desired number of main models.
!!!tip
Read more in queries documentation [offset][offset]
## Selecting subset of columns
### fields
`fields(columns: Union[list, str, set, dict]) -> QuerySet`
With fields() you can select subset of model columns to limit the data load.
!!!tip
Read more in queries documentation [fields][fields]
### exclude_fields
`exclude_fields(columns: Union[list, str, set, dict]) -> QuerySet`
With exclude_fields() you can select subset of model columns that will be excluded to limit the data load.
!!!tip
Read more in queries documentation [exclude_fields][exclude_fields]
## Aggregated functions
### count
`count(distinct: bool = True) -> int`
Returns number of rows matching the given criteria (i.e. applied with filter and exclude)
!!!tip
Read more in queries documentation [count][count]
### exists
`exists() -> bool`
Returns a bool value to confirm if there are rows matching the given criteria (applied with filter and exclude)
!!!tip
Read more in queries documentation [exists][exists]
[queries]: ../queries/index.md
[get]: ../queries/read.md#get
[all]: ../queries/read.md#all
[iterate]: ../queries/read.md#iterate
[create]: ../queries/create.md#create
[get_or_create]: ../queries/read.md#get_or_create
[update_or_create]: ../queries/update.md#update_or_create
[update]: ../queries/update.md#update
[filter]: ../queries/filter-and-sort.md#filter
[exclude]: ../queries/filter-and-sort.md#exclude
[select_related]: ../queries/joins-and-subqueries.md#select_related
[prefetch_related]: ../queries/joins-and-subqueries.md#prefetch_related
[limit]: ../queries/pagination-and-rows-number.md#limit
[offset]: ../queries/pagination-and-rows-number.md#offset
[paginate]: ../queries/pagination-and-rows-number.md#paginate
[count]: ../queries/aggregations.md#count
[exists]: ../queries/aggregations.md#exists
[fields]: ../queries/select-columns.md#fields
[exclude_fields]: ../queries/select-columns.md#exclude_fields
[order_by]: ../queries/filter-and-sort.md#order_by
ormar-orm-ormar-a7e7fde/docs/releases.md 0000664 0000000 0000000 00000235605 15211567312 0020422 0 ustar 00root root 0000000 0000000 # Release notes
## 0.26.0
### ✨ Features
* Add `OrmarConfig(comment=...)` to attach a SQL `COMMENT ON TABLE` value to a
model — forwarded to the underlying SQLAlchemy `Table` and emitted by
`metadata.create_all()` on dialects that support table comments (PostgreSQL,
MySQL; SQLite ignores it). [#1240](https://github.com/collerek/ormar/issues/1240)
### 🐛 Fixes
* Fix an unexpected cartesian product (cross join) when filtering on two
foreign keys that reach the same target table through the same relation
(e.g. `a_port__switch__name` and `z_port__switch__name`). The filter now
resolves complex join aliases in the same depth-first order as the join
builder, and ignores complex aliases left in the globally shared alias
manager by earlier queries, so a repeated query is no longer corrupted.
[#1706](https://github.com/collerek/ormar/issues/1706)
* Require [`ormar-utils`](https://pypi.org/project/ormar-utils/) `>=0.2.0`,
which ships CPython stable-ABI (`abi3`) wheels. A single wheel now works on
Python 3.10+ — including 3.14 and future releases — so installing ormar no
longer fails for lack of a matching `ormar-utils` wheel on a new Python.
[#1702](https://github.com/collerek/ormar/issues/1702)
* Fix race in `get_or_create` (and the m2m / reverse-fk variant) where
concurrent callers leaked `IntegrityError`. [#1016](https://github.com/collerek/ormar/issues/1016)
* Fix `update_or_create` raising `NoMatch` instead of creating when called
with a pk that doesn't exist in the database.
* Fix `values()` / `values_list()` leaking columns of related models that
were pulled in by a filter (e.g. `filter(project__id=...)`) or by a bare
`select_related()` but never named in `fields()`. Such relations - and
their through models for m2m - are now excluded from the projected
result, matching the columns the caller actually requested.
[#800](https://github.com/collerek/ormar/issues/800)
* Fix `values()` / `values_list()` leaking main-model columns when
`fields()` named only nested fields (e.g.
`values_list("garage__owner", flatten=True)`). The flat projection now
drops every column from any model the caller never named - including
the parent FK column and any in-between models the path passes through
- so `flatten=True` returns the requested nested value instead of the
first leaked main-model column.
[#1060](https://github.com/collerek/ormar/issues/1060)
## 0.25.0
### ⚡ Performance
A broad performance pass across model construction, row hydration, relation
resolution, alias lookups, and result merging — combining per-class caching,
lazy initialization of relation machinery, and porting the hottest helpers to
Rust via the new required [`ormar-utils`](https://pypi.org/project/ormar-utils/)
dependency. End-to-end speedups vs 0.24.0 range from ~12 % on `select_related`,
through ~30 % on `iterate` and `first`, to ~60 - 80 % on initialization of models, and up to x2.5 speedup in getting `all`.
### ✨ Features
* Add `flatten_fields()` queryset method and `model_dump(flatten_fields=...)` /
`model_dump(flatten_all=True)` arguments to render related models as their
primary key value instead of nested dicts — see
[docs](./queries/select-columns.md#flatten_fields)
[#1641](https://github.com/collerek/ormar/pull/1641)
```python
cars = await Car.objects.flatten_fields("manufacturer").all()
assert cars[0].model_dump() == {"id": 1, "name": "Corolla", "manufacturer": 1} # Note the id 1 and not the nested model
car.model_dump(flatten_all=True)
car.model_dump(flatten_fields={"manufacturer": ...})
```
* Add **proxy model inheritance** (`OrmarConfig(proxy=True)`) — child class
shares the parent's table and only adds methods / computed fields / a custom
`queryset_class` — thanks @SepehrBazyar — see
[docs](./models/inheritance.md#proxy-models)
[#760](https://github.com/collerek/ormar/pull/760)
```python
class User(Human):
ormar_config = base_ormar_config.copy(proxy=True)
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
```
* Add per-table `schema=` support on `OrmarConfig` so models can live in
non-default database schemas (PostgreSQL full, MySQL with caveats, SQLite
not supported for cross-schema FKs) — thanks @hk3dva — see
[docs](./models/index.md#schemas)
[#1493](https://github.com/collerek/ormar/pull/1493)
* Add Python-style indexing and slicing on `QuerySet` — `Track.objects[:10]`,
`Track.objects[-5:]`, `Track.objects[5]` — thanks @SepehrBazyar — see
[docs](./queries/pagination-and-rows-number.md#slicing-with-__getitem__)
[#765](https://github.com/collerek/ormar/pull/765)
* Add `nulls_ordering` argument to `.asc()` / `.desc()` (`ormar.NullsOrdering.FIRST`
/ `LAST`) with MySQL emulation — thanks @SepehrBazyar — see
[docs](./queries/filter-and-sort.md#controlling-null-placement-with-nulls_ordering)
[#769](https://github.com/collerek/ormar/pull/769)
* Allow filtering by a foreign-key column directly (`Book.objects.filter(Book.author == 5)`
or with a model instance) without adding a JOIN — thanks @djalouehz — see
[docs](./queries/filter-and-sort.md#filtering-by-foreign-key-columns)
[#854](https://github.com/collerek/ormar/pull/854)
* Add `through_relation_nullable` and `through_reverse_relation_nullable` to
`ormar.ManyToMany` to enforce `NOT NULL` on the through table's FK columns —
thanks @evadev-eva — see
[docs](./relations/many-to-many.md#making-through-relation-columns-non-nullable)
[#852](https://github.com/collerek/ormar/pull/852)
* Add `last()` / `last_or_none()` on `QuerySet` and expose `first_or_none()` /
`last()` / `last_or_none()` on `QuerysetProxy` — see
[docs](./queries/pagination-and-rows-number.md)
[#765](https://github.com/collerek/ormar/pull/765)
* Forward unrecognized `**kwargs` from `ormar.ForeignKey` / `ormar.ManyToMany`
to `BaseField` — fixes `comment=` (and other `BaseField` kwargs) being
silently dropped on relation fields — thanks @booqoffsky
[#1239](https://github.com/collerek/ormar/pull/1239)
### 🐛 Fixes
* Clearing a nullable `ForeignKey` with `instance.fk = None` now also clears the
in-memory relation, so subsequent reads return `None` without a reload —
originally @amit12297, rebased — see
[docs](./relations/foreign-key.md)
[#1230](https://github.com/collerek/ormar/pull/1230)
* Drop the unnecessary post-`INSERT` pk reload in `save()`, and raise
`ModelPersistenceError` on backends that cannot return a generated pk
(Oracle MySQL with non-`AUTO_INCREMENT` server-default pk) instead of
silently assigning the row count to `Model.pk` — thanks @vnicolaichuk
[#919](https://github.com/collerek/ormar/pull/919)
* `SelectAction` aggregate identifiers now go through `sqlalchemy.column()`
instead of string concatenation
[#1637](https://github.com/collerek/ormar/pull/1637)
* Fix the `mkdocs` deployment workflow
[#1638](https://github.com/collerek/ormar/pull/1638)
## 0.24.0
### ✨ Features
* Add `on_update` option to fields to customize SQL `ON UPDATE` behavior - thanks @vvanglro [#1273](https://github.com/collerek/ormar/pull/1273)
* Allow overriding auto-generated foreign key constraint names via `fk_name` parameter on `ormar.ForeignKey`, useful when the generated name exceeds the database's identifier length limit - thanks @nikelborm [#849](https://github.com/collerek/ormar/pull/849)
* Forward `autoincrement` property from ormar field to the underlying SQLAlchemy column, enabling DDL with manually managed integer primary keys - thanks @smuething [#674](https://github.com/collerek/ormar/pull/674)
### 🐛 Fixes
* Run standalone queries under AUTOCOMMIT isolation to remove the per-query `BEGIN`/`COMMIT` round-trip introduced in 0.22.0. Restores (and exceeds) 0.21-era performance on per-row CRUD and single-row reads while keeping explicit `database.transaction()` and savepoints working. `bulk_update` and `iterate` are internally wrapped in an explicit transaction so their batch-commit and server-side-cursor semantics are preserved [#1629](https://github.com/collerek/ormar/discussions/1629) [#1635](https://github.com/collerek/ormar/pull/1635)
## 0.23.1
### ‼️🚨 High vulnerability fixed – please upgrade ASAP
* In this version of ormar a high severity vulnerability (CVE-2026-27953) in model initialization was patched. The vulnerability allowed injection of `__pk_only__` and `__excluded__` parameters through user-supplied `**kwargs` (e.g. JSON request bodies). Passing `__pk_only__=True` bypassed all Pydantic validation, and `__excluded__` could nullify arbitrary fields. Thanks @Mistz1 for reporting!
* Affected versions:
* All versions prior to `0.23.1`
## 0.23.0
### ‼️🚨 Critical vulnerability fixed – please upgrade ASAP
* In this version of ormar the critical vulnerability (`CVE-2026-26198`) in aggregate functions was patched - thanks @AAtomical
for reporting. The vulnerability was caused by the way ormar generated SQL queries for aggregate functions, allowing arbitrary SQL execution through user input.
* Affected versions:
* `0.9.9 - 0.12.2`
* `0.20.0b1 - 0.22.0 (latest)`
### ✨ Breaking changes
* Drop support for Python 3.9
### 🐛 Fixes
* Fix selecting data with nested models with json fields [#1530](https://github.com/collerek/ormar/pull/1530)
* Fix prefetching JSON list field throwing TypeError - thanks @jannyware-inc [#1402](https://github.com/collerek/ormar/pull/1402)
## 0.22.0
### ✨ Breaking changes
* **Migration from `databases` library to native async SQLAlchemy**
Version 0.22.0 migrates from the `databases` library to native async SQLAlchemy using ormar's `DatabaseConnection` wrapper. This provides better integration with SQLAlchemy's async ecosystem and improved transaction handling and avoid dependency on archived `databases` library.
* **Import changes**
Replace `databases` import with `DatabaseConnection` from ormar:
```python
# ormar < 0.22
import databases
database = databases.Database("sqlite:///db.sqlite")
# ormar >= 0.22
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
```
* **Database URLs require async drivers**
Database connection strings must now use async-compatible drivers:
- SQLite: `sqlite+aiosqlite://` (not `sqlite://`)
- PostgreSQL: `postgresql+asyncpg://` (not `postgresql://`)
- MySQL: `mysql+aiomysql://` (not `mysql://`)
```python
# ormar < 0.22
database = databases.Database("sqlite:///db.sqlite")
# ormar >= 0.22
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
```
* **Engine parameter removed from OrmarConfig**
The `engine` parameter is no longer needed in `OrmarConfig` - it's created internally by `DatabaseConnection`:
```python
# ormar < 0.22
import databases
import sqlalchemy
database = databases.Database("sqlite:///db.sqlite")
engine = sqlalchemy.create_engine("sqlite:///db.sqlite")
base_ormar_config = ormar.OrmarConfig(
database=database,
metadata=sqlalchemy.MetaData(),
engine=engine, # <- No longer needed
)
# ormar >= 0.22
from ormar import DatabaseConnection
import sqlalchemy
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
base_ormar_config = ormar.OrmarConfig(
database=database,
metadata=sqlalchemy.MetaData(),
# engine is created internally
)
```
* **Table creation requires sync engine**
When using `metadata.create_all()`, you must create a separate sync engine:
```python
# ormar >= 0.22
import sqlalchemy
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///db.sqlite"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
# Create a sync engine for table creation
sync_engine = sqlalchemy.create_engine(
DATABASE_URL.replace('+aiosqlite', '')
)
metadata.create_all(sync_engine)
```
### ✨ Features
* **Improved transaction handling**
Transactions now use context variables and SQLAlchemy savepoints for better nested transaction support:
```python
# Nested transactions with automatic savepoints
async with database.transaction():
await Model1.objects.create(...)
async with database.transaction(): # Uses savepoint
await Model2.objects.create(...)
```
* **Enhanced testing support**
The `force_rollback` parameter for transactions makes testing easier.
Like in databases `force_rollback` can also be used with DatabaseConnection directly to use one global transaction.
```python
async with database.transaction(force_rollback=True):
# Your test code - will rollback even on success
await Model.objects.create(...)
```
### 💬 Other
* Improved pyright support - thanks @MaximSrour - [#1491](https://github.com/collerek/ormar/pull/1491)
## 0.21.0
### ✨ Breaking changes
* Drop support for Python 3.8
* Remove the possibility to exclude parents' fields in children models (discouraged as bad practice anyway)
* Add support for Sqlalchemy 2.0 and drop for 1.4
### 💬 Other
* Bump dependencies to newer versions: among others pydantic, databases and fastapi
* Move setuptools to dev dependencies
* Solve vulnerabilities in dependencies
### 🐛 Fixes
* Fix mutable default argument in translate list to dict - thanks @cadlagtrader [#1382](https://github.com/collerek/ormar/pull/1382)
* Fix fastapi docs - thanks @inktrap [#1362](https://github.com/collerek/ormar/pull/1362)
* Fix clashing many to many fields names [#1407](https://github.com/collerek/ormar/pull/1407)
### 💬 Other
* Add official support for python 3.12 - thanks @ChristopherMacGown [#1395](https://github.com/collerek/ormar/pull/1395)
* Unpin pydantic allowing pydantic versions <2.9.0 - thanks @camillol [#1388](https://github.com/collerek/ormar/pull/1388)
## 0.20.2
### 🐛 Fixes
* Fix mutable default argument in translate list to dict - thanks @cadlagtrader [#1382](https://github.com/collerek/ormar/pull/1382)
* Fix fastapi docs - thanks @inktrap [#1362](https://github.com/collerek/ormar/pull/1362)
* Fix clashing many to many fields names [#1407](https://github.com/collerek/ormar/pull/1407)
### 💬 Other
* Add official support for python 3.12 - thanks @ChristopherMacGown [#1395](https://github.com/collerek/ormar/pull/1395)
* Unpin pydantic allowing pydantic versions <2.9.0 - thanks @camillol [#1388](https://github.com/collerek/ormar/pull/1388)
# Release notes
## 0.20.1
### ✨ Breaking changes
* Note that this is the first non-beta release of ormar with support for Pydantic v2. Check release notes for 0.20.0 and https://collerek.github.io/ormar/0.20.0b1/migration/
### 🐛 Fixes
* Fix merging same target models when using `select_related` with `prefetch_related` [#906](https://github.com/collerek/ormar/issues/906)
* Fix saving related with pk only models [#812](https://github.com/collerek/ormar/issues/812)
* Fix adding the same relation multiple times corrupting relation cache [#1335](https://github.com/collerek/ormar/issues/1335)
### ✨ Features
* Allow adding indexed on foreign keys by @cmflynn [#1276](https://github.com/collerek/ormar/pull/1276)
### 💬 Other
* Some docs fixes by @Chaoyingz, thanks!
## 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
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
###✨ 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
* 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
###✨ 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
* `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
* 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
###✨ 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
* Fix limiting query result to 0 should return empty list (by @SepehrBazyar - thanks!) [#766](https://github.com/collerek/ormar/issues/713)
###💬 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
###🐛 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
###🐛 Fixes
* Fix deepcopy issues introduced in pydantic 1.9 [#685](https://github.com/collerek/ormar/issues/685)
##0.11.0
###✨ 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
* Added support for python 3.10
###🐛 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
###✨ 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
* 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
###✨ Features
* Add `post_bulk_update` signal (by @ponytailer - thanks!) [#524](https://github.com/collerek/ormar/pull/524)
###🐛 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
* 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
###✨ Features
* Add ability to pass `comment` to sqlalchemy when creating a column [#485](https://github.com/collerek/ormar/issues/485)
###🐛 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
###🐛 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
###🐛 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
###✨ 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
* 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
###✨ 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
* 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
* Bump supported `databases` version to `<=5.2`.
##0.10.18
###🐛 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
###✨ 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
###✨ 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
* 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
###🐛 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
###✨ 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
* Fix dependencies from `psycopg2` to `psycopg2-binary` [#255](https://github.com/collerek/ormar/issues/255)
##0.10.13
###✨ 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:
```python
await Post.objects.select_related(["author", "categories"]).get()
await Author.objects.prefetch_related("posts__categories").get()
```
* Now also:
```python
await Post.objects.select_related([Post.author, Post.categories]).get()
await Author.objects.prefetch_related(Author.posts.categories).get()
```
###🐛 Fixes
* Fix overwriting default value for inherited primary key [#253](https://github.com/collerek/ormar/issues/253)
##0.10.12
###🐛 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
###✨ 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
* Skips parsing the data to ormar model so skips also the validation
* 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
* Fix creation of auto through model for m2m relation with ForwardRef [#226](https://github.com/collerek/ormar/issues/226)
##0.10.10
###✨ 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
* 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
* 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
###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
* Fix OpenAPi schema for LargeBinary [#204](https://github.com/collerek/ormar/issues/204)
##0.10.8
###🐛 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
###✨ 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)
* Add `represent_as_base64_str: bool = False` parameter that allows conversion of bytes `LargeBinary` field to base64 encoded string. String is returned in `dict()`,
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
* 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
* 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
###✨ 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`
attribute which is now deprecated [#160](https://github.com/collerek/ormar/issues/160).
Pydantic fields should be declared normally as in pydantic model next to ormar fields,
note that (obviously) `ormar` does not save and load the value for this field in
database that mean that **ONE** of the following has to be true:
* pydantic field declared on ormar model has to be `Optional` (defaults to None)
* pydantic field has to have a default value set
* pydantic field has `default_factory` function set
* ormar.Model with pydantic field has to overwrite `__init__()` and provide the value there
If none of the above `ormar` (or rather pydantic) will fail during loading data from the database,
with missing required value for declared pydantic field.
* Ormar provides now a meaningful examples in openapi schema, including nested models.
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
* 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
* 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
###🐛 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
###✨ 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_`)
* Field access overloads set of python operators and provide a set of functions to allow same functionality as with dunder separated param names in `**kwargs`, that means that querying from sample model `Track` related to model `Album` now you have more options:
* exact - exact match to value, sql `column = `
* OLD: `album__name__exact='Malibu'`
* NEW: can be also written as `Track.album.name == 'Malibu`
* iexact - exact match sql `column = ` (case insensitive)
* OLD: `album__name__iexact='malibu'`
* NEW: can be also written as `Track.album.name.iexact('malibu')`
* contains - sql `column LIKE '%%'`
* OLD: `album__name__contains='Mal'`
* NEW: can be also written as `Track.album.name % 'Mal')`
* NEW: can be also written as `Track.album.name.contains('Mal')`
* icontains - sql `column LIKE '%%'` (case insensitive)
* OLD: `album__name__icontains='mal'`
* NEW: can be also written as `Track.album.name.icontains('mal')`
* in - sql ` column IN (, , ...)`
* OLD: `album__name__in=['Malibu', 'Barclay']`
* NEW: can be also written as `Track.album.name << ['Malibu', 'Barclay']`
* NEW: can be also written as `Track.album.name.in_(['Malibu', 'Barclay'])`
* isnull - sql `column IS NULL` (and sql `column IS NOT NULL`)
* OLD: `album__name__isnull=True` (isnotnull `album__name__isnull=False`)
* NEW: can be also written as `Track.album.name >> None`
* NEW: can be also written as `Track.album.name.isnull(True)`
* NEW: not null can be also written as `Track.album.name.isnull(False)`
* NEW: not null can be also written as `~(Track.album.name >> None)`
* NEW: not null can be also written as `~(Track.album.name.isnull(True))`
* gt - sql `column > ` (greater than)
* OLD: `position__gt=3`
* NEW: can be also written as `Track.album.name > 3`
* gte - sql `column >= ` (greater or equal than)
* OLD: `position__gte=3`
* NEW: can be also written as `Track.album.name >= 3`
* lt - sql `column < ` (lower than)
* OLD: `position__lt=3`
* NEW: can be also written as `Track.album.name < 3`
* lte - sql `column <= ` (lower equal than)
* OLD: `position__lte=3`
* NEW: can be also written as `Track.album.name <= 3`
* startswith - sql `column LIKE '%'` (exact start match)
* OLD: `album__name__startswith='Mal'`
* NEW: can be also written as `Track.album.name.startswith('Mal')`
* istartswith - sql `column LIKE '%'` (case insensitive)
* OLD: `album__name__istartswith='mal'`
* NEW: can be also written as `Track.album.name.istartswith('mal')`
* endswith - sql `column LIKE '%'` (exact end match)
* OLD: `album__name__endswith='ibu'`
* NEW: can be also written as `Track.album.name.endswith('ibu')`
* iendswith - sql `column LIKE '%'` (case insensitive)
* OLD: `album__name__iendswith='IBU'`
* NEW: can be also written as `Track.album.name.iendswith('IBU')`
* You can provide `FilterGroups` not only in `filter()` and `exclude()` but also in:
* `get()`
* `get_or_none()`
* `get_or_create()`
* `first()`
* `all()`
* `delete()`
* With `FilterGroups` (`ormar.and_` and `ormar.or_`) you can now use:
* `&` - as `and_` instead of next level of nesting
* `|` - as `or_' instead of next level of nesting
* `~` - as negation of the filter group
* To combine groups of filters into one set of conditions use `&` (sql `AND`) and `|` (sql `OR`)
```python
# Following queries are equivalent:
# sql: ( product.name = 'Test' AND product.rating >= 3.0 )
# ormar OPTION 1 - OLD one
Product.objects.filter(name='Test', rating__gte=3.0).get()
# ormar OPTION 2 - OLD one
Product.objects.filter(ormar.and_(name='Test', rating__gte=3.0)).get()
# ormar OPTION 3 - NEW one (field access)
Product.objects.filter((Product.name == 'Test') & (Product.rating >=3.0)).get()
```
* Same applies to nested complicated filters
```python
# Following queries are equivalent:
# sql: ( product.name = 'Test' AND product.rating >= 3.0 )
# OR (categories.name IN ('Toys', 'Books'))
# ormar OPTION 1 - OLD one
Product.objects.filter(ormar.or_(
ormar.and_(name='Test', rating__gte=3.0),
categories__name__in=['Toys', 'Books'])
).get()
# ormar OPTION 2 - NEW one (instead of nested or use `|`)
Product.objects.filter(
ormar.and_(name='Test', rating__gte=3.0) |
ormar.and_(categories__name__in=['Toys', 'Books'])
).get()
# ormar OPTION 3 - NEW one (field access)
Product.objects.filter(
((Product.name='Test') & (Product.rating >= 3.0)) |
(Product.categories.name << ['Toys', 'Books'])
).get()
```
* Now you can also use field access to provide OrderActions to `order_by()`
* Order ascending:
* OLD: `Product.objects.order_by("name").all()`
* NEW: `Product.objects.order_by(Product.name.asc()).all()`
* Order descending:
* OLD: `Product.objects.order_by("-name").all()`
* NEW: `Product.objects.order_by(Product.name.desc()).all()`
* 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
* 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
### ✨ 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
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:
* 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`)
* the relation won't be populated in `dict()` and `json()`
* you cannot pass the nested related objects when populating from `dict()` or `json()` (also through `fastapi`). It will be either ignored or raise error depending on `extra` setting in pydantic `Config`.
* `Model.save_related()` now can save whole data tree in once [#148](https://github.com/collerek/ormar/discussions/148)
meaning:
* it knows if it should save main `Model` or related `Model` first to preserve the relation
* it saves main `Model` if
* it's not `saved`,
* has no `pk` value
* or `save_all=True` flag is set
in those cases you don't have to split save into two calls (`save()` and `save_related()`)
* it supports also `ManyToMany` relations
* it supports also optional `Through` model values for m2m relations
* Add possibility to customize `Through` model relation field names.
* By default `Through` model relation names default to related model name in lowercase.
So in example like this:
```python
... ## course declaration omitted
class Student(ormar.Model):
class Meta:
database = database
metadata = metadata
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
courses = ormar.ManyToMany(Course)
## will produce default Through model like follows (example simplified)
class StudentCourse(ormar.Model):
class Meta:
database = database
metadata = metadata
tablename = "students_courses"
id: int = ormar.Integer(primary_key=True)
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`:
* `through_relation_name` - name of the field leading to the model in which `ManyToMany` is declared
* `through_reverse_relation_name` - name of the field leading to the model to which `ManyToMany` leads to
Example:
```python
... # course declaration omitted
class Student(ormar.Model):
class Meta:
database = database
metadata = metadata
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
courses = ormar.ManyToMany(Course,
through_relation_name="student_id",
through_reverse_relation_name="course_id")
# will produce default Through model like follows (example simplified)
class StudentCourse(ormar.Model):
class Meta:
database = database
metadata = metadata
tablename = "students_courses"
id: int = ormar.Integer(primary_key=True)
student_id = ormar.ForeignKey(Student) # set by through_relation_name
course_id = ormar.ForeignKey(Course) # set by through_reverse_relation_name
```
### 🐛 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
* 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
### ✨ 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),
with `save_all=True` all related models are saved, regardless of `saved` status, which might be useful if updated
models comes from api call, so are not changed in the backend.
* `exclude: Union[Set, Dict, None]` -> set/dict of relations to exclude from save, those relation won't be saved even with `follow=True` and `save_all=True`.
To exclude nested relations pass a nested dictionary like: `exclude={"child":{"sub_child": {"exclude_sub_child_realtion"}}}`. The allowed values follow
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.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
`exclude()` calls that generates NOT filter. So either `each=True` needs to be set to delete whole table or at least one of `filter/exclude` clauses.
* Same thing applies to `QuerySet.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.
* 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
* 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()`.
* Fix bug when two non-relation fields were merged (appended) in query result when they were not relation fields (i.e. JSON)
* Fix bug when during translation to dict from list the same relation name is used in chain but leads to different models
* 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
* Performance optimizations
* Split tests into packages based on tested area
## 0.10.1
### 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
* Fix dialect dependent quoting of column and table names in order_by clauses not working
properly in postgres.
## 0.10.0
### 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
* 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.
By default `select_all()` adds only directly related models, with `follow=True` also related models
of related models are added without loops in relations. Note that it's not and end `async` model
so you still have to issue `get()`, `all()` etc. as `select_all()` returns a QuerySet (or proxy)
like `fields()` or `order_by()`.
### Internals
* `ormar` fields are no longer stored as classes in `Meta.model_fields` dictionary
but instead they are stored as instances.
## 0.9.9
### 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]`
* To change reverse relation order_by pass `related_orders_by = [columns]` where `columns: List[str]`
* Arguments can be column names or `-{col_name}` to sort descending
* In relations you can sort only by directly related model columns
or for `ManyToMany` columns also `Through` model columns `"{through_field_name}__{column_name}"`
* Order in which order_by clauses are applied is as follows:
* Explicitly passed `order_by()` calls in query
* Relation passed `orders_by` if exists
* Model `Meta` class `orders_by`
* Model primary key column asc (fallback, used if none of above provided)
* Add 4 new aggregated functions -> `min`, `max`, `sum` and `avg` that are their
corresponding sql equivalents.
* You can pass one or many column names including related columns.
* As of now each column passed is aggregated separately (so `sum(col1+col2)` is not possible,
you can have `sum(col1, col2)` and later add 2 returned sums in python)
* You cannot `sum` and `avg` non numeric columns
* If you aggregate on one column, the single value is directly returned as a result
* If you aggregate on multiple columns a dictionary with column: result pairs is returned
* Add 4 new signals -> `pre_relation_add`, `post_relation_add`, `pre_relation_remove` and `post_relation_remove`
* The newly added signals are emitted for `ManyToMany` relations (both sides)
and reverse side of `ForeignKey` relation (same as `QuerysetProxy` is exposed).
* Signals receive following args: `sender: Type[Model]` - sender class,
`instance: Model` - instance to which related model is added, `child: Model` - model being added,
`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
* `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
pre/post save/update/delete signals
* `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
* `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
### 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`
* When custom backend is selected you need to provide your backend class that subclasses `ormar.fields.EncryptBackend`
* You cannot encrypt `primary_key` column and relation columns (FK and M2M).
* Provided are 2 backends: HASH and FERNET
* HASH is a one-way hash (like for password), never decrypted on retrieval
* FERNET is a two-way encrypt/decrypt backend
* Note that in FERNET backend you loose `filtering` possibility altogether as part of the encrypted value is a timestamp.
* 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
* (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
### Features
* Add `isnull` operator to filter and exclude methods.
```python
album__name__isnull=True #(sql: album.name is null)
album__name__isnull=False #(sql: album.name is not null))
```
* Add `ormar.or_` and `ormar.and_` functions that can be used to compose
complex queries with nested conditions.
Sample query:
```python
books = (
await Book.objects.select_related("author")
.filter(
ormar.and_(
ormar.or_(year__gt=1960, year__lt=1940),
author__name="J.R.R. Tolkien",
)
)
.all()
)
```
Check the updated docs in Queries -> Filtering and sorting -> Complex filters
### Other
* Setting default on `ForeignKey` or `ManyToMany` raises and `ModelDefinition` exception as it is (and was) not supported
## 0.9.6
##Important
* `Through` model for `ManyToMany` relations now **becomes optional**. It's not a breaking change
since if you provide it everything works just fine as it used to. So if you don't want or need any additional
fields on `Through` model you can skip it. Note that it's going to be created for you automatically and
still has to be included in example in `alembic` migrations.
If you want to delete existing one check the default naming convention to adjust your existing database structure.
Note that you still need to provide it if you want to
customize the `Through` model name or the database table name.
### 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
the related models (i.e. `await model_instance.related.all()`)
* Add `load_all(follow=False, exclude=None)` model method that allows to load current instance of the model
with all related models in one call. By default it loads only directly related models but setting
`follow=True` causes traversing the tree (avoiding loops). You can also pass `exclude` parameter
that works the same as `QuerySet.exclude_fields()` method.
* Added possibility to add more fields on `Through` model for `ManyToMany` relationships:
* name of the through model field is the lowercase name of the Through class
* you can pass additional fields when calling `add(child, **kwargs)` on relation (on `QuerysetProxy`)
* you can pass additional fields when calling `create(**kwargs)` on relation (on `QuerysetProxy`)
when one of the keyword arguments should be the through model name with a dict of values
* you can order by on through model fields
* you can filter on through model fields
* you can include and exclude fields on through models
* through models are attached only to related models (i.e. if you query from A to B -> only on B)
* note that through models are explicitly loaded without relations -> relation is already populated in ManyToMany field.
* note that just like before you cannot declare the relation fields on through model, they will be populated for you by `ormar`,
but now if you try to do so `ModelDefinitionError` will be thrown
* check the updated ManyToMany relation docs for more information
## Other
* Updated docs and api docs
* Refactors and optimisations mainly related to filters, exclusions and order bys
## 0.9.5
### 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
### Fixes
* Fix `fastapi` OpenAPI schema generation for automatic docs when multiple models refer to the same related one
## 0.9.3
### 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
* Fix `choices` not being verified when setting the attribute after initialization
* Fix `choices` not being verified during `update` call from `QuerySet`
## 0.9.2
### 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
### Features
* Add choices values to `OpenAPI` specs, so it looks like native `Enum` field in the result schema.
### Fixes
* Fix `choices` behavior with `fastapi` usage when special fields can be not initialized yet but passed as strings etc.
## 0.9.0
### 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
that most of the `ormar` functions are working your database **CREATED with ormar (or ormar + alembic)**
does not have relations and suffer from perspective of performance and data integrity.
* If you were using `ormar` to connect to existing database your performance and integrity
should be fine nevertheless you should update to reflect all future schema updates in your models.
### 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
include `metadata` or `database` now `ModelDefinitionError` will be raised instead of generic `AttributeError`.
* **Breaking:** `encode/databases` used for running the queries does not have a connection pool
for sqlite backend, meaning that each query is run with a new connection and there is no way to
enable enforcing ForeignKeys constraints as those are by default turned off on every connection.
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
* Update api docs.
* Add tests for fk creation in db and for cascades in db
## 0.8.1
### Features
* Introduce processing of `ForwardRef` in relations.
Now you can create self-referencing models - both `ForeignKey` and `ManyToMany` relations.
`ForwardRef` can be used both for `to` and `through` `Models`.
* Introduce the possibility to perform two **same relation** joins in one query, so to process complex relations like:
```
B = X = Y
//
A
\
C = X = Y <= before you could link from X to Y only once in one query
unless two different relation were used
(two relation fields with different names)
```
* Introduce the `paginate` method that allows to limit/offset by `page` and `page_size`.
Available for `QuerySet` and `QuerysetProxy`.
### 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
### 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
number of rows is limited but just number of main models (first one in the query, the one used to construct it). You can still limit all rows from db response with `limit_raw_sql=True` flag on either `limit` or `offset` (or both)
* **Breaking:** issuing `first()` now fetches the first row ordered by the primary key asc (so first one inserted (can be different for non number primary keys - i.e. alphabetical order of string))
* **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
* 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
* 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
* Split and cleanup in docs:
* Divide models section into subsections
* Divide relations section into subsections
* Divide fields section into subsections
* Add model inheritance section
* Add API (BETA) documentation
## 0.7.5
* Fix for wrong relation column name in many_to_many relation joins (fix [#73][#73])
## 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])
* Fix for wrong relation population for m2m relations when also fk relation present for same model.
* 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
* Fix for setting fetching related model with UUDI pk, which is a string in raw (fix [#71][#71])
## 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
* Fix for overwriting related models with pk only in `Model.save()` (fix [#68][#68])
## 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`
* Added **Signals**, with pre-defined list signals and decorators: `post_delete`, `post_save`, `post_update`, `pre_delete`,
`pre_save`, `pre_update`
* Add `py.typed` and modify `setup.py` for mypy support
* Performance optimization
* Updated docs
## 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.model_dump()` and in `fastapi` response
* Update docs
## 0.6.1
* Explicitly set None to excluded nullable fields to avoid pydantic setting a default value (fix [#60][#60]).
## 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
* **Breaking:** ReverseForeignKey relation now exposes QuerySetProxy API like ManyToMany relation
* **Breaking:** querying related models from ManyToMany cleans list of related models loaded on parent model:
* Example: `post.categories.first()` will set post.categories to list of 1 related model -> the one returned by first()
* Example 2: if post has 4 categories so `len(post.categories) == 4` calling `post.categories.limit(2).all()` -> will load only 2 children and now `assert len(post.categories) == 2`
* Added `get_or_create`, `update_or_create`, `fields`, `exclude_fields`, `exclude`, `prefetch_related` and `order_by` to QuerySetProxy
so now you can use those methods directly from relation
* Update docs
## 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
* 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
format='string' to use 36 (with dashes) instead to adjust to existing db or other libraries.
Sample:
* hex value = c616ab438cce49dbbf4380d109251dce
* string value = c616ab43-8cce-49db-bf43-80d109251dce
## 0.5.3
* Fixed bug in `Model.model_dump()` method that was ignoring exclude parameter and not include dictionary argument.
## 0.5.2
* Added `prefetch_related` method to load subsequent models in separate queries.
* Update docs
## 0.5.1
* Switched to github actions instead of travis
* Update badges in the docs
## 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
* Model is saved after `create/get/first/all/get_or_create/update_or_create` method
* Model is saved when passed to `bulk_update` and `bulk_create`
* Model is saved after adding/removing `ManyToMany` related objects (through model instance auto saved/deleted)
* Model is **not** saved after change of any own field (including pk as `Model.pk` alias)
* Model is **not** saved after adding/removing `ForeignKey` related object (fk column not saved)
* Model is **not** saved after instantiation with `__init__` (w/o `QuerySet.create` or before calling `save`)
* Added `Model.upsert(**kwargs)` that performs `save()` if pk not set otherwise `update(**kwargs)`
* Added `Model.save_related(follow=False)` that iterates all related objects in all relations and checks if they are saved. If not it calls `upsert()` on each of them.
* **Breaking:** added raising exceptions if `add`-ing/`remove`-ing not saved (pk is None) models to `ManyToMany` relation
* Allow passing dictionaries and sets to fields and exclude_fields
* Auto translate str and lists to dicts for fields and exclude_fields
* **Breaking:** passing nested models to fields and exclude_fields is now by related ForeignKey name and not by target model name
* Performance optimizations - in modelproxy, newbasemodel - > less queries, some properties are cached on models
* Cleanup of unused relations code
* Optional performance dependency orjson added (**strongly recommended**)
* Updated docs
## 0.4.4
* add exclude_fields() method to exclude fields from sql
* refactor column names setting (aliases)
* fix ordering by for column with aliases
* additional tests for fields and exclude_fields
* update docs
## 0.4.3
* include properties in models.model_dump() and model.model_dump_json()
## 0.4.2
* modify creation of pydantic models to allow returning related models with only pk populated
## 0.4.1
* add order_by method to queryset to allow sorting
* update docs
## 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
* Type hint docs and test
* Use mypy for tests also not, only ormar package
* Fix scale and precision translation with max_digits and decimal_places pydantic Decimal field
* Update docs - add best practices for dependencies
* Refactor metaclass and model_fields to play nice with type hints
* Add mypy and pydantic plugin to docs
* Expand the docs on ManyToMany relation
## 0.3.11
* Fix setting server_default as default field value in python
## 0.3.10
* Fix postgresql check to avoid exceptions with drivers not installed if using different backend
## 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
* 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
* Publish documentation and update readme
## 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
* Added bulk_create and bulk_update for operations on multiple objects.
## 0.3.4
Add queryset level methods
* delete
* update
* get_or_create
* update_or_create
## 0.3.3
* Add additional filters - startswith and endswith
## 0.3.2
* Add choices parameter to all fields - limiting the accepted values to ones provided
## 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
* Added ManyToMany field and support for many to many relations
[#19]: https://github.com/collerek/ormar/issues/19
[#60]: https://github.com/collerek/ormar/issues/60
[#68]: https://github.com/collerek/ormar/issues/68
[#70]: https://github.com/collerek/ormar/issues/70
[#71]: https://github.com/collerek/ormar/issues/71
[#73]: https://github.com/collerek/ormar/issues/73 ormar-orm-ormar-a7e7fde/docs/signals.md 0000664 0000000 0000000 00000023376 15211567312 0020257 0 ustar 00root root 0000000 0000000 # Signals
Signals are a mechanism to fire your piece of code (function / method) whenever given type of event happens in `ormar`.
To achieve this you need to register your receiver for a given type of signal for selected model(s).
## Defining receivers
Given a sample model like following:
```Python
import sqlalchemy
import ormar
from ormar import DatabaseConnection
base_ormar_config = ormar.OrmarConfig(
database=DatabaseConnection("sqlite+aiosqlite:///db.sqlite"),
metadata=sqlalchemy.MetaData(),
)
class Album(ormar.Model):
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)
```
You can for example define a trigger that will set `album.is_best_seller` status if it will be played more than 50 times.
Import `pre_update` decorator, for list of currently available decorators/ signals check below.
```Python hl_lines="7"
--8<-- "../docs_src/signals/docs002.py"
```
Define your function.
Note that each receiver function:
* has to be **callable**
* has to accept first **`sender`** argument that receives the class of sending object
* has to accept **`**kwargs`** argument as the parameters send in each `ormar.Signal` can change at any time so your function has to serve them.
* has to be **`async`** cause callbacks are gathered and awaited.
`pre_update` currently sends only one argument apart from `sender` and it's `instance` one.
Note how `pre_update` decorator accepts a `senders` argument that can be a single model or a list of models,
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="31-34"
--8<-- "../docs_src/signals/docs002.py"
```
!!!note
Note that receivers are defined on a class level -> so even if you connect/disconnect function through instance
it will run/ stop running for all operations on that `ormar.Model` class.
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="44-51"
--8<-- "../docs_src/signals/docs002.py"
```
You can define same receiver for multiple models at once by passing a list of models to signal decorator.
```python
# define a dummy debug function
@pre_update([Album, Track])
async def before_update(sender, instance, **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.
```python
@pre_update(Album)
async def before_update(sender, instance, **kwargs):
print(f"{sender.get_name()}: {instance.model_dump_json()}: {kwargs}")
@pre_update(Album)
async def before_update2(sender, instance, **kwargs):
print(f'About to update {sender.get_name()} with pk: {instance.pk}')
```
Note that `ormar` decorators are the syntactic sugar, you can directly connect your function or method for given signal for
given model. Connect accept only one parameter - your `receiver` function / method.
```python hl_lines="11 13 16"
class AlbumAuditor:
def __init__(self):
self.event_type = "ALBUM_INSTANCE"
async def before_save(self, sender, instance, **kwargs):
await AuditLog(
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.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)
```
!!!warning
Note that signals keep the reference to your receiver (not a `weakref`) so keep that in mind to avoid circular references.
## Disconnecting the receivers
To disconnect the receiver and stop it for running for given model you need to disconnect it.
```python hl_lines="7 10"
@pre_update(Album)
async def before_update(sender, instance, **kwargs):
if instance.play_count > 50 and not instance.is_best_seller:
instance.is_best_seller = True
# disconnect given function from signal for given Model
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)
```
## Available signals
!!!warning
Note that signals are **not** send for:
* 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 like raw sql update/delete operations) and do not have specific instance.
### pre_save
`pre_save(sender: type["Model"], instance: "Model")`
Send for `Model.save()` and `Model.objects.create()` methods.
`sender` is a `ormar.Model` class and `instance` is the model to be saved.
### post_save
`post_save(sender: type["Model"], instance: "Model")`
Send for `Model.save()` and `Model.objects.create()` methods.
`sender` is a `ormar.Model` class and `instance` is the model that was saved.
### pre_update
`pre_update(sender: type["Model"], instance: "Model")`
Send for `Model.update()` method.
`sender` is a `ormar.Model` class and `instance` is the model to be updated.
### post_update
`post_update(sender: type["Model"], instance: "Model")`
Send for `Model.update()` method.
`sender` is a `ormar.Model` class and `instance` is the model that was updated.
### pre_delete
`pre_delete(sender: type["Model"], instance: "Model")`
Send for `Model.save()` and `Model.objects.create()` methods.
`sender` is a `ormar.Model` class and `instance` is the model to be deleted.
### post_delete
`post_delete(sender: type["Model"], instance: "Model")`
Send for `Model.update()` method.
`sender` is a `ormar.Model` class and `instance` is the model that was deleted.
### pre_relation_add
`pre_relation_add(sender: type["Model"], instance: "Model", child: "Model",
relation_name: str, passed_args: dict)`
Send for `Model.relation_name.add()` method for `ManyToMany` relations and reverse side of `ForeignKey` relation.
`sender` - sender class, `instance` - instance to which related model is added, `child` - model being added,
`relation_name` - name of the relation to which child is added, for add signals also `passed_kwargs` - dict of kwargs passed to `add()`
### post_relation_add
`post_relation_add(sender: type["Model"], instance: "Model", child: "Model",
relation_name: str, passed_args: dict)`
Send for `Model.relation_name.add()` method for `ManyToMany` relations and reverse side of `ForeignKey` relation.
`sender` - sender class, `instance` - instance to which related model is added, `child` - model being added,
`relation_name` - name of the relation to which child is added, for add signals also `passed_kwargs` - dict of kwargs passed to `add()`
### pre_relation_remove
`pre_relation_remove(sender: type["Model"], instance: "Model", child: "Model",
relation_name: str)`
Send for `Model.relation_name.remove()` method for `ManyToMany` relations and reverse side of `ForeignKey` relation.
`sender` - sender class, `instance` - instance to which related model is added, `child` - model being added,
`relation_name` - name of the relation to which child is added.
### post_relation_remove
`post_relation_remove(sender: type["Model"], instance: "Model", child: "Model",
relation_name: str, passed_args: dict)`
Send for `Model.relation_name.remove()` method for `ManyToMany` relations and reverse side of `ForeignKey` relation.
`sender` - sender class, `instance` - instance to which related model is added, `child` - model being added,
`relation_name` - name of the relation to which child is added.
### post_bulk_update
`post_bulk_update(sender: type["Model"], instances: list["Model"], **kwargs)`,
Send for `Model.objects.bulk_update(list[objects])` method.
## Defining your own signals
Note that you can create your own signals although you will have to send them manually in your code or subclass `ormar.Model`
and trigger your signals there.
Creating new signal is super easy. Following example will set a new signal with name your_custom_signal.
```python hl_lines="21"
import sqlalchemy
import ormar
from ormar import DatabaseConnection
base_ormar_config = ormar.OrmarConfig(
database=DatabaseConnection("sqlite+aiosqlite:///db.sqlite"),
metadata=sqlalchemy.MetaData(),
)
class Album(ormar.Model):
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.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
to access them as attributes. When you try to access a signal that does not exist `SignalEmitter` will create one for you.
So example above can be simplified to. The `Signal` will be created for you.
```
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.ormar_config.signals.your_custom_signal.send(sender=Album)
```
Note that sender is the only required parameter and it should be ormar Model class.
Additional parameters have to be passed as keyword arguments.
```python
await Album.ormar_config.signals.your_custom_signal.send(sender=Album, my_param=True)
```
ormar-orm-ormar-a7e7fde/docs/transactions.md 0000664 0000000 0000000 00000020573 15211567312 0021323 0 ustar 00root root 0000000 0000000 # Transactions
Database transactions are supported through ormar's `DatabaseConnection` class, which internally uses SQLAlchemy async to manage transactions with context variables and savepoints.
## Basic usage
To use transactions use `database.transaction()` as async context manager:
```python
async with database.transaction():
# everything called here will be one transaction
await Model1().save()
await Model2().save()
# if any exception occurs, all changes will be rolled back
# if successful, changes will be committed automatically
```
!!!note
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 `ormar_config.database` and can
be reached from both class and instance.
```python
import sqlalchemy
import ormar
from ormar import DatabaseConnection
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=DatabaseConnection("sqlite+aiosqlite:///db.sqlite"),
)
class Author(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=255)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=255)
author: Author = ormar.ForeignKey(Author)
# database is accessible from class
database = Author.ormar_config.database
# as well as from instance
author = Author(name="Stephen King")
database = author.ormar_config.database
# Example: Using transaction to ensure atomicity
async def create_author_with_book():
async with database:
async with database.transaction():
author = await Author.objects.create(name="Stephen King")
book = await Book.objects.create(
title="The Shining",
author=author
)
# Both author and book are created in one transaction
# If book creation fails, author creation is rolled back
```
## Nested Transactions
Transaction blocks are managed as task-local state using context variables. Nested transactions
are fully supported and are implemented using SQLAlchemy savepoints.
```python
async def create_multiple_authors_with_books():
async with database:
# Outer transaction
async with database.transaction():
author1 = await Author.objects.create(name="Stephen King")
# Nested transaction (uses savepoint)
try:
async with database.transaction():
book1 = await Book.objects.create(
title="The Shining",
author=author1
)
# Simulate an error
raise ValueError("Something went wrong!")
except ValueError:
# Inner transaction is rolled back to savepoint
# author1 is still in the outer transaction
pass
# Continue with outer transaction
author2 = await Author.objects.create(name="J.K. Rowling")
book2 = await Book.objects.create(
title="Harry Potter",
author=author2
)
# author1, author2, and book2 are committed
# book1 was rolled back
```
## Force Rollback for Testing
Transactions can be extremely useful during testing when you can apply force rollback
and you do not have to clean the data after each test. The `force_rollback=True` parameter
will rollback the transaction even if it completes successfully.
```python
import pytest
@pytest.mark.asyncio
async def test_author_creation():
async with database:
async with database.transaction(force_rollback=True):
# Create test data
author = await Author.objects.create(name="Test Author")
book = await Book.objects.create(
title="Test Book",
author=author
)
# Verify the data was created
assert await Author.objects.count() == 1
assert await Book.objects.count() == 1
# After the transaction exits, everything is rolled back
# No cleanup needed!
# Verify data was rolled back
assert await Author.objects.count() == 0
assert await Book.objects.count() == 0
```
## Complete Example with Error Handling
Here's a comprehensive example showing transaction usage with error handling:
```python
import sqlalchemy
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///db.sqlite"
base_ormar_config = ormar.OrmarConfig(
metadata=sqlalchemy.MetaData(),
database=DatabaseConnection(DATABASE_URL),
)
class Author(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=255)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=255)
author: Author = ormar.ForeignKey(Author)
async def create_author_and_books_transactional(author_name: str, book_titles: list[str]):
"""
Create an author and multiple books in a single transaction.
If any book fails to create, the entire operation is rolled back.
"""
database = Author.ormar_config.database
async with database:
try:
async with database.transaction():
# Create author
author = await Author.objects.create(name=author_name)
# Create all books
for title in book_titles:
await Book.objects.create(title=title, author=author)
print(f"Successfully created {author_name} with {len(book_titles)} books")
return author
except Exception as e:
# Transaction is automatically rolled back on exception
print(f"Failed to create author and books: {e}")
# Author and all books are rolled back
raise
# Usage example
async def main():
database = Author.ormar_config.database
async with database:
# Create tables
sync_engine = sqlalchemy.create_engine(
DATABASE_URL.replace('+aiosqlite', '')
)
base_ormar_config.metadata.create_all(sync_engine)
# Example 1: Successful transaction
await create_author_and_books_transactional(
"Stephen King",
["The Shining", "It", "The Stand"]
)
# Example 2: Failed transaction (will rollback)
try:
await create_author_and_books_transactional(
"Test Author",
["Book 1", None, "Book 3"] # None will cause an error
)
except Exception:
print("Transaction rolled back as expected")
# Verify: Only Stephen King and his books exist
authors = await Author.objects.all()
print(f"Total authors: {len(authors)}") # Should be 1
books = await Book.objects.all()
print(f"Total books: {len(books)}") # Should be 3
if __name__ == "__main__":
import asyncio
asyncio.run(main())
```
## Transaction Context Management
Ormar manages transactions using context variables, which means:
1. **Thread-safe**: Each async task has its own transaction context
2. **Automatic connection reuse**: Within a transaction, all queries use the same database connection
3. **Savepoint support**: Nested transactions create savepoints automatically
4. **Rollback on exception**: If an exception occurs, the transaction is automatically rolled back
5. **Automatic commit**: If the transaction block completes successfully, changes are committed
## Best Practices
1. **Keep transactions short**: Long-running transactions can cause lock contention
2. **Don't mix transaction and non-transaction operations**: Once in a transaction, all operations should be part of it
3. **Use force_rollback for tests**: Avoid test data pollution by rolling back test transactions
4. **Handle exceptions appropriately**: Let exceptions propagate to trigger rollback, or catch and handle them within the transaction
5. **Use nested transactions for partial rollbacks**: When you need fine-grained control over what gets rolled back
ormar-orm-ormar-a7e7fde/docs_src/ 0000775 0000000 0000000 00000000000 15211567312 0017131 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0021230 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/aggregations/ 0000775 0000000 0000000 00000000000 15211567312 0021603 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/aggregations/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0023702 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/aggregations/docs001.py 0000664 0000000 0000000 00000001612 15211567312 0023326 0 ustar 00root root 0000000 0000000 from typing import Optional
import sqlalchemy
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///aggregations_docs001.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
metadata=metadata,
database=database,
)
class Author(ormar.Model):
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):
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)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
ranking: int = ormar.Integer(nullable=True)
ormar-orm-ormar-a7e7fde/docs_src/fastapi/ 0000775 0000000 0000000 00000000000 15211567312 0020560 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/fastapi/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0022657 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/fastapi/docs001.py 0000664 0000000 0000000 00000002777 15211567312 0022320 0 ustar 00root root 0000000 0000000 from typing import Optional
from fastapi import FastAPI
from tests.lifespan import lifespan
from tests.settings import create_config
import ormar
base_ormar_config = create_config()
app = FastAPI(lifespan=lifespan(base_ormar_config))
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(tablename="items")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
@app.get("/items/", response_model=list[Item])
async def get_items():
items = await Item.objects.select_related("category").all()
return items
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
await item.save()
return item
@app.post("/categories/", response_model=Category)
async def create_category(category: Category):
await category.save()
return 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.model_dump())
@app.delete("/items/{item_id}")
async def delete_item(item_id: int, item: Item = None):
if item:
return {"deleted_rows": await item.delete()}
item_db = await Item.objects.get(pk=item_id)
return {"deleted_rows": await item_db.delete()}
ormar-orm-ormar-a7e7fde/docs_src/fastapi/mypy/ 0000775 0000000 0000000 00000000000 15211567312 0021556 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/fastapi/mypy/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0023655 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/fastapi/mypy/docs001.py 0000664 0000000 0000000 00000000644 15211567312 0023305 0 ustar 00root root 0000000 0000000 import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
id = ormar.Integer(primary_key=True)
name = ormar.String(max_length=100)
completed = ormar.Boolean(default=False)
ormar-orm-ormar-a7e7fde/docs_src/fields/ 0000775 0000000 0000000 00000000000 15211567312 0020377 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/fields/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0022476 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/fields/docs001.py 0000664 0000000 0000000 00000002422 15211567312 0022122 0 ustar 00root root 0000000 0000000 import asyncio
from typing import Optional
import sqlalchemy
from examples import create_drop_database
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///fields_docs001.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
ormar_base_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
class Department(ormar.Model):
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):
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)
@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()
asyncio.run(verify())
ormar-orm-ormar-a7e7fde/docs_src/fields/docs002.py 0000664 0000000 0000000 00000002033 15211567312 0022121 0 ustar 00root root 0000000 0000000 from typing import Optional
import sqlalchemy
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///fields_docs002.db"
ormar_base_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL), metadata=sqlalchemy.MetaData()
)
class Department(ormar.Model):
ormar_config = ormar_base_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Course(ormar.Model):
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 = Department(name="Science")
course = Course(name="Math", completed=False, department=department)
print(department.my_courses[0])
# Will produce:
# Course(id=None,
# name='Math',
# completed=False,
# department=Department(id=None, name='Science'))
ormar-orm-ormar-a7e7fde/docs_src/fields/docs003.py 0000664 0000000 0000000 00000001364 15211567312 0022130 0 ustar 00root root 0000000 0000000 from typing import Optional
import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Department(ormar.Model):
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):
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)
department: Optional[Department] = ormar.ForeignKey(Department)
ormar-orm-ormar-a7e7fde/docs_src/fields/docs004.py 0000664 0000000 0000000 00000001237 15211567312 0022130 0 ustar 00root root 0000000 0000000 from datetime import datetime
import sqlalchemy
from sqlalchemy import func, text
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///fields_docs004.db")
metadata = sqlalchemy.MetaData()
class Product(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database, metadata=metadata, tablename="product"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
company: str = ormar.String(max_length=200, server_default="Acme")
sort_order: int = ormar.Integer(server_default=text("10"))
created: datetime = ormar.DateTime(server_default=func.now())
ormar-orm-ormar-a7e7fde/docs_src/models/ 0000775 0000000 0000000 00000000000 15211567312 0020414 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/models/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0022513 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/models/docs001.py 0000664 0000000 0000000 00000000664 15211567312 0022145 0 ustar 00root root 0000000 0000000 import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
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)
ormar-orm-ormar-a7e7fde/docs_src/models/docs002.py 0000664 0000000 0000000 00000001137 15211567312 0022142 0 ustar 00root root 0000000 0000000 import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
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",
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
completed: bool = ormar.Boolean(default=False)
ormar-orm-ormar-a7e7fde/docs_src/models/docs003.py 0000664 0000000 0000000 00000001710 15211567312 0022140 0 ustar 00root root 0000000 0000000 import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
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.model_fields)
"""
Will produce:
{'id': Field(name='id',
type=Optional[int],
required=False,
default=None),
'name': Field(name='name',
type=Optional[str],
required=False,
default=None),
'completed': Field(name='completed',
type=bool,
required=False,
default=False)}
"""
ormar-orm-ormar-a7e7fde/docs_src/models/docs004.py 0000664 0000000 0000000 00000001315 15211567312 0022142 0 ustar 00root root 0000000 0000000 import sqlalchemy
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///models_docs004.db"
ormar_base_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL), metadata=sqlalchemy.MetaData()
)
class Course(ormar.Model):
ormar_config = ormar.OrmarConfig(
tablename="courses",
database=DatabaseConnection(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.ormar_config.table.columns)
"""
Will produce:
ImmutableColumnCollection(courses.id, courses.name, courses.completed)
"""
ormar-orm-ormar-a7e7fde/docs_src/models/docs005.py 0000664 0000000 0000000 00000011045 15211567312 0022144 0 ustar 00root root 0000000 0000000 import pprint
import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
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)
pprint.pp({x: v.__dict__ for x, v in Course.ormar_config.model_fields.items()})
"""
Will produce:
{'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}}
"""
ormar-orm-ormar-a7e7fde/docs_src/models/docs006.py 0000664 0000000 0000000 00000001273 15211567312 0022147 0 ustar 00root root 0000000 0000000 import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.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")],
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
completed: bool = ormar.Boolean(default=False)
ormar-orm-ormar-a7e7fde/docs_src/models/docs007.py 0000664 0000000 0000000 00000001532 15211567312 0022146 0 ustar 00root root 0000000 0000000 import asyncio
import sqlalchemy
from examples import create_drop_database
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///models_docs007.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
ormar_base_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
class Course(ormar.Model):
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)
@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)
asyncio.run(run_query())
ormar-orm-ormar-a7e7fde/docs_src/models/docs008.py 0000664 0000000 0000000 00000001201 15211567312 0022140 0 ustar 00root root 0000000 0000000 import sqlalchemy
import ormar
from ormar import DatabaseConnection
DATABASE_URl = "sqlite+aiosqlite:///models_docs008.db"
database = DatabaseConnection(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)
ormar-orm-ormar-a7e7fde/docs_src/models/docs009.py 0000664 0000000 0000000 00000001736 15211567312 0022156 0 ustar 00root root 0000000 0000000 from typing import Optional
import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection(
"sqlite+aiosqlite:///models_docs009.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):
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)
artist: Optional[Artist] = ormar.ForeignKey(Artist, name="artist_id")
ormar-orm-ormar-a7e7fde/docs_src/models/docs010.py 0000664 0000000 0000000 00000002375 15211567312 0022146 0 ustar 00root root 0000000 0000000 import sqlalchemy
import ormar
from ormar import DatabaseConnection
DATABASE_URl = "sqlite+aiosqlite:///models_docs010.db"
database = DatabaseConnection(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):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
tablename="children_x_artists",
)
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")
children = ormar.ManyToMany(Child, through=ArtistChildren)
ormar-orm-ormar-a7e7fde/docs_src/models/docs012.py 0000664 0000000 0000000 00000000644 15211567312 0022145 0 ustar 00root root 0000000 0000000 import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
id = ormar.Integer(primary_key=True)
name = ormar.String(max_length=100)
completed = ormar.Boolean(default=False)
ormar-orm-ormar-a7e7fde/docs_src/models/docs013.py 0000664 0000000 0000000 00000001610 15211567312 0022140 0 ustar 00root root 0000000 0000000 from typing import Optional
import sqlalchemy
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///models_docs013.db"
ormar_base_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
class Artist(ormar.Model):
# 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)
last_name: str = ormar.String(max_length=100)
born_year: int = ormar.Integer(name="year")
class Album(ormar.Model):
ormar_config = ormar_base_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
artist: Optional[Artist] = ormar.ForeignKey(Artist)
ormar-orm-ormar-a7e7fde/docs_src/models/docs014.py 0000664 0000000 0000000 00000000773 15211567312 0022152 0 ustar 00root root 0000000 0000000 import pydantic
import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
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 = pydantic.Field(max_length=100)
ormar-orm-ormar-a7e7fde/docs_src/models/docs015.py 0000664 0000000 0000000 00000001015 15211567312 0022141 0 ustar 00root root 0000000 0000000 import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
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
def prefixed_name(self):
return "custom_prefix__" + self.name
ormar-orm-ormar-a7e7fde/docs_src/models/docs016.py 0000664 0000000 0000000 00000000771 15211567312 0022152 0 ustar 00root root 0000000 0000000 import pydantic
import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.Model):
ormar_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
model_config = pydantic.ConfigDict(frozen=True)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
completed: bool = ormar.Boolean(default=False)
ormar-orm-ormar-a7e7fde/docs_src/models/docs017.py 0000664 0000000 0000000 00000001302 15211567312 0022142 0 ustar 00root root 0000000 0000000 import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.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")],
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
completed: bool = ormar.Boolean(default=False)
ormar-orm-ormar-a7e7fde/docs_src/models/docs018.py 0000664 0000000 0000000 00000001443 15211567312 0022151 0 ustar 00root root 0000000 0000000 import datetime
import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Course(ormar.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=[
ormar.CheckColumns("start_time < end_time", name="date_check"),
],
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
start_date: datetime.date = ormar.Date()
end_date: datetime.date = ormar.Date()
ormar-orm-ormar-a7e7fde/docs_src/queries/ 0000775 0000000 0000000 00000000000 15211567312 0020606 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/queries/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0022705 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/queries/docs001.py 0000664 0000000 0000000 00000001334 15211567312 0022332 0 ustar 00root root 0000000 0000000 from typing import Optional
import sqlalchemy
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///queries_docs001.db"
ormar_base_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
class Album(ormar.Model):
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):
ormar_config = ormar_base_config.copy(tablename="track")
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
title: str = ormar.String(max_length=100)
position: int = ormar.Integer()
ormar-orm-ormar-a7e7fde/docs_src/queries/docs002.py 0000664 0000000 0000000 00000002354 15211567312 0022336 0 ustar 00root root 0000000 0000000 import asyncio
import sqlalchemy
from examples import create_drop_database
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///queries_docs002.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
ormar_base_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
class Book(ormar.Model):
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",
)
@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
asyncio.run(run_query())
ormar-orm-ormar-a7e7fde/docs_src/queries/docs003.py 0000664 0000000 0000000 00000002724 15211567312 0022340 0 ustar 00root root 0000000 0000000 import asyncio
import sqlalchemy
from examples import create_drop_database
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///queries_docs003.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
ormar_base_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
class Book(ormar.Model):
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",
)
@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() == 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() == 4
asyncio.run(run_query())
ormar-orm-ormar-a7e7fde/docs_src/queries/docs004.py 0000664 0000000 0000000 00000001774 15211567312 0022345 0 ustar 00root root 0000000 0000000 import asyncio
import sqlalchemy
from examples import create_drop_database
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///queries_docs004.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
ormar_base_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
class ToDo(ormar.Model):
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_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
asyncio.run(run_query())
ormar-orm-ormar-a7e7fde/docs_src/queries/docs005.py 0000664 0000000 0000000 00000002526 15211567312 0022342 0 ustar 00root root 0000000 0000000 import asyncio
import sqlalchemy
from examples import create_drop_database
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///queries_docs005.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
ormar_base_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
class Book(ormar.Model):
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",
)
@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
asyncio.run(run_query())
ormar-orm-ormar-a7e7fde/docs_src/queries/docs006.py 0000664 0000000 0000000 00000003375 15211567312 0022346 0 ustar 00root root 0000000 0000000 import asyncio
import sqlalchemy
from examples import create_drop_database
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///queries_docs006.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
ormar_base_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
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():
# 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())
ormar-orm-ormar-a7e7fde/docs_src/queries/docs007.py 0000664 0000000 0000000 00000002567 15211567312 0022351 0 ustar 00root root 0000000 0000000 import asyncio
import sqlalchemy
from examples import create_drop_database
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///queries_docs007.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
ormar_base_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
class Owner(ormar.Model):
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):
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)
@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)
asyncio.run(run_query())
ormar-orm-ormar-a7e7fde/docs_src/queries/docs008.py 0000664 0000000 0000000 00000007061 15211567312 0022344 0 ustar 00root root 0000000 0000000 import asyncio
import sqlalchemy
from examples import create_drop_database
from pydantic import ValidationError
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///queries_docs008.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
ormar_base_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
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():
# 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", "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 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 -
# 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())
ormar-orm-ormar-a7e7fde/docs_src/queries/docs009.py 0000664 0000000 0000000 00000004627 15211567312 0022352 0 ustar 00root root 0000000 0000000 import asyncio
import sqlalchemy
from examples import create_drop_database
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///queries_docs009.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
ormar_base_config = ormar.OrmarConfig(
database=database,
metadata=metadata,
)
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())
ormar-orm-ormar-a7e7fde/docs_src/relations/ 0000775 0000000 0000000 00000000000 15211567312 0021131 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/relations/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0023230 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/relations/docs001.py 0000664 0000000 0000000 00000002441 15211567312 0022655 0 ustar 00root root 0000000 0000000 from typing import Optional, Union
import sqlalchemy
import ormar
from ormar import DatabaseConnection
database = DatabaseConnection("sqlite+aiosqlite:///db.sqlite")
metadata = sqlalchemy.MetaData()
class Department(ormar.Model):
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):
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)
department: Optional[Union[Department, dict]] = ormar.ForeignKey(Department)
department = Department(name="Science")
# set up a relation with actual Model instance
course = Course(name="Math", completed=False, department=department)
# set up relation with only related model pk value
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.model_dump())
# explicitly set up None
course4 = Course(name="Math III", completed=False, department=None)
ormar-orm-ormar-a7e7fde/docs_src/relations/docs002.py 0000664 0000000 0000000 00000001762 15211567312 0022663 0 ustar 00root root 0000000 0000000 from typing import Optional
import sqlalchemy
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///relations_docs002.db"
ormar_base_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL), metadata=sqlalchemy.MetaData()
)
class Author(ormar.Model):
ormar_config = ormar_base_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=80)
last_name: str = ormar.String(max_length=80)
class Category(ormar.Model):
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):
ormar_config = ormar_base_config.copy(tablename="posts")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[list[Category]] = ormar.ManyToMany(Category)
author: Optional[Author] = ormar.ForeignKey(Author)
ormar-orm-ormar-a7e7fde/docs_src/relations/docs003.py 0000664 0000000 0000000 00000001273 15211567312 0022661 0 ustar 00root root 0000000 0000000 from typing import Optional, Union
import sqlalchemy
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///relations_docs003.db"
ormar_base_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL), metadata=sqlalchemy.MetaData()
)
class Department(ormar.Model):
ormar_config = ormar_base_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Course(ormar.Model):
ormar_config = ormar_base_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
department: Optional[Union[Department, dict]] = ormar.ForeignKey(Department)
ormar-orm-ormar-a7e7fde/docs_src/relations/docs004.py 0000664 0000000 0000000 00000001642 15211567312 0022662 0 ustar 00root root 0000000 0000000 import sqlalchemy
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///relations_docs004.db"
ormar_base_config = ormar.OrmarConfig(
database=DatabaseConnection(DATABASE_URL), metadata=sqlalchemy.MetaData()
)
class Category(ormar.Model):
ormar_config = ormar_base_config.copy(tablename="categories")
id = ormar.Integer(primary_key=True)
name = ormar.String(max_length=40)
class PostCategory(ormar.Model):
ormar_config = ormar_base_config.copy(tablename="posts_x_categories")
id: int = ormar.Integer(primary_key=True)
sort_order: int = ormar.Integer(nullable=True)
param_name: str = ormar.String(default="Name", max_length=200)
class Post(ormar.Model):
ormar_config = ormar_base_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories = ormar.ManyToMany(Category, through=PostCategory)
ormar-orm-ormar-a7e7fde/docs_src/select_columns/ 0000775 0000000 0000000 00000000000 15211567312 0022150 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/select_columns/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0024247 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/select_columns/docs001.py 0000664 0000000 0000000 00000003370 15211567312 0023676 0 ustar 00root root 0000000 0000000 import asyncio
import sqlalchemy
from examples import create_drop_database
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///select_columns_docs001.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
database=database,
metadata=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())
ormar-orm-ormar-a7e7fde/docs_src/select_columns/docs002_flatten.py 0000664 0000000 0000000 00000003376 15211567312 0025422 0 ustar 00root root 0000000 0000000 import asyncio
import sqlalchemy
from examples import create_drop_database
import ormar
from ormar import DatabaseConnection
DATABASE_URL = "sqlite+aiosqlite:///select_columns_docs002_flatten.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
base_ormar_config = ormar.OrmarConfig(
database=database,
metadata=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)
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)
@create_drop_database(base_config=base_ormar_config)
async def sample_data():
toyota = await Company.objects.create(name="Toyota")
await Car.objects.create(manufacturer=toyota, name="Corolla")
# Queryset-level flatten: relation renders as its pk value on dump.
# Missing relations are auto-loaded via select_related / prefetch_related.
cars = await Car.objects.flatten_fields("manufacturer").all()
assert cars[0].model_dump() == {
"id": cars[0].id,
"name": "Corolla",
"manufacturer": toyota.id,
}
# Alternatively: nested dict form, or a Python FieldAccessor chain.
cars = await Car.objects.flatten_fields({"manufacturer": ...}).all()
cars = await Car.objects.flatten_fields(Car.manufacturer).all()
# Ad-hoc flattening directly on model_dump(); accepts the same forms.
cars = await Car.objects.select_related("manufacturer").all()
cars[0].model_dump(flatten_fields="manufacturer")
cars[0].model_dump(flatten_all=True)
asyncio.run(sample_data())
ormar-orm-ormar-a7e7fde/docs_src/signals/ 0000775 0000000 0000000 00000000000 15211567312 0020571 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/signals/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0022670 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/docs_src/signals/docs002.py 0000664 0000000 0000000 00000002523 15211567312 0022317 0 ustar 00root root 0000000 0000000 import asyncio
import sqlalchemy
from examples import create_drop_database
import ormar
from ormar import DatabaseConnection, pre_update
DATABASE_URL = "sqlite+aiosqlite:///signals_docs002.db"
database = DatabaseConnection(DATABASE_URL)
metadata = sqlalchemy.MetaData()
ormar_base_config = ormar.OrmarConfig(
database=database,
metadata=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):
if instance.play_count > 50 and not instance.is_best_seller:
instance.is_best_seller = True
@create_drop_database(base_config=ormar_base_config)
async def run_query():
# here album.play_count and 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 = 60
await album.update()
assert album.is_best_seller
asyncio.run(run_query())
ormar-orm-ormar-a7e7fde/docs_src/test_all_docs.py 0000664 0000000 0000000 00000001006 15211567312 0022317 0 ustar 00root root 0000000 0000000 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
ormar-orm-ormar-a7e7fde/examples/ 0000775 0000000 0000000 00000000000 15211567312 0017150 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/examples/__init__.py 0000664 0000000 0000000 00000000114 15211567312 0021255 0 ustar 00root root 0000000 0000000 from .utils import create_drop_database
__all__ = ["create_drop_database"]
ormar-orm-ormar-a7e7fde/examples/fastapi_quick_start.py 0000664 0000000 0000000 00000004221 15211567312 0023561 0 ustar 00root root 0000000 0000000 from contextlib import asynccontextmanager
from typing import Optional
import sqlalchemy
import uvicorn
from fastapi import FastAPI
import ormar
DATABASE_URL = "sqlite+aiosqlite:///test.db"
ormar_base_config = ormar.OrmarConfig(
database=ormar.DatabaseConnection(DATABASE_URL), metadata=sqlalchemy.MetaData()
)
@asynccontextmanager
async def lifespan(app: FastAPI):
database_ = app.state.database
if not database_.is_connected:
await database_.connect()
yield
database_ = app.state.database
if database_.is_connected:
await database_.disconnect()
app = FastAPI(lifespan=lifespan)
metadata = sqlalchemy.MetaData()
database = ormar.DatabaseConnection(DATABASE_URL)
app.state.database = database
class Category(ormar.Model):
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):
ormar_config = ormar_base_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)
@app.get("/items/", response_model=list[Item])
async def get_items():
items = await Item.objects.select_related("category").all()
return items
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
await item.save()
return item
@app.post("/categories/", response_model=Category)
async def create_category(category: Category):
await category.save()
return 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.model_dump())
@app.delete("/items/{item_id}")
async def delete_item(item_id: int, item: Item = None):
if item:
return {"deleted_rows": await item.delete()}
item_db = await Item.objects.get(pk=item_id)
return {"deleted_rows": await item_db.delete()}
if __name__ == "__main__":
# to play with API run the script and visit http://127.0.0.1:8000/docs
uvicorn.run(app, host="127.0.0.1", port=8000)
ormar-orm-ormar-a7e7fde/examples/script_from_readme.py 0000664 0000000 0000000 00000035461 15211567312 0023377 0 ustar 00root root 0000000 0000000 from typing import Optional
import pydantic
import sqlalchemy
import ormar
DATABASE_URL = "sqlite+aiosqlite:///db.sqlite"
# 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
base_ormar_config = ormar.OrmarConfig(
database=ormar.DatabaseConnection(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
)
# Note that all type hints are optional
# below is a perfectly valid model declaration
# class Author(ormar.Model):
# 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):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
# note - normally import should be at the beginning of the file
import asyncio
async def setup_database():
"""Create all tables in the database."""
if not base_ormar_config.database.is_connected:
await base_ormar_config.database.connect()
async with base_ormar_config.database.engine.begin() as conn:
await conn.run_sync(base_ormar_config.metadata.drop_all)
await conn.run_sync(base_ormar_config.metadata.create_all)
if base_ormar_config.database.is_connected:
await base_ormar_config.database.disconnect()
# create the database
# note that in production you should use migrations
# note that this is not required if you connect to existing database
# just to be sure we clear the db before
asyncio.run(setup_database())
# all functions below are divided into functionality categories
# note how all functions are defined with async - hence can use await AND needs to
# be awaited on their own
async def create():
# Create some records to work with through QuerySet.create method.
# Note that queryset is exposed on each Model's class as objects
tolkien = await Author.objects.create(name="J.R.R. Tolkien")
await Book.objects.create(author=tolkien, title="The Hobbit", year=1937)
await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955)
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
# alternative creation of object divided into 2 steps
sapkowski = Author(name="Andrzej Sapkowski")
# do some stuff
await sapkowski.save()
# or save() after initialization
await Book(author=sapkowski, title="The Witcher", year=1990).save()
await Book(author=sapkowski, title="The Tower of Fools", year=2002).save()
# to read more about inserting data into the database
# visit: https://collerek.github.io/ormar/queries/create/
async def read():
# Fetch an instance, without loading a foreign key relationship on it.
# Django style
book = await Book.objects.get(title="The Hobbit")
# or python style
book = await Book.objects.get(Book.title == "The Hobbit")
book2 = await Book.objects.first()
# first() fetch the instance with lower primary key value
assert book == book2
# you can access all fields on loaded model
assert book.title == "The Hobbit"
assert book.year == 1937
# when no condition is passed to get()
# it behaves as last() based on primary key column
book3 = await Book.objects.get()
assert book3.title == "The Tower of Fools"
# When you have a relation, ormar always defines a related model for you
# even when all you loaded is a foreign key value like in this example
assert isinstance(book.author, Author)
# primary key is populated from foreign key stored in books table
assert book.author.pk == 1
# since the related model was not loaded all other fields are None
assert book.author.name is None
# Load the relationship from the database when you already have the related model
# alternatively see joins section below
await book.author.load()
assert book.author.name == "J.R.R. Tolkien"
# get all rows for given model
authors = await Author.objects.all()
assert len(authors) == 2
# to read more about reading data from the database
# visit: https://collerek.github.io/ormar/queries/read/
async def update():
# read existing row from db
tolkien = await Author.objects.get(name="J.R.R. Tolkien")
assert tolkien.name == "J.R.R. Tolkien"
tolkien_id = tolkien.id
# change the selected property
tolkien.name = "John Ronald Reuel Tolkien"
# call update on a model instance
await tolkien.update()
# confirm that object was updated
tolkien = await Author.objects.get(name="John Ronald Reuel Tolkien")
assert tolkien.name == "John Ronald Reuel Tolkien"
assert tolkien.id == tolkien_id
# alternatively update data without loading
await Author.objects.filter(name__contains="Tolkien").update(name="J.R.R. Tolkien")
# to read more about updating data in the database
# visit: https://collerek.github.io/ormar/queries/update/
async def delete():
silmarillion = await Book.objects.get(year=1977)
# call delete() on instance
await silmarillion.delete()
# alternatively delete without loading
await Book.objects.delete(title="The Tower of Fools")
# note that when there is no record ormar raises NoMatch exception
try:
await Book.objects.get(year=1977)
except ormar.NoMatch:
print("No book from 1977!")
# to read more about deleting data from the database
# visit: https://collerek.github.io/ormar/queries/delete/
# note that despite the fact that record no longer exists in database
# the object above is still accessible and you can use it (and i.e. save()) again.
tolkien = silmarillion.author
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
async def joins():
# Tho join two models use select_related
# Django style
book = await Book.objects.select_related("author").get(title="The Hobbit")
# Python style
book = await Book.objects.select_related(Book.author).get(
Book.title == "The Hobbit"
)
# now the author is already prefetched
assert book.author.name == "J.R.R. Tolkien"
# By default you also get a second side of the relation
# constructed as lowercase source model name +'s' (books in this case)
# you can also provide custom name with parameter related_name
# Django style
author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien")
# Python style
author = await Author.objects.select_related(Author.books).all(
Author.name == "J.R.R. Tolkien"
)
assert len(author[0].books) == 3
# for reverse and many to many relations you can also prefetch_related
# that executes a separate query for each of related models
# Django style
author = await Author.objects.prefetch_related("books").get(name="J.R.R. Tolkien")
# Python style
author = await Author.objects.prefetch_related(Author.books).get(
Author.name == "J.R.R. Tolkien"
)
assert len(author.books) == 3
# to read more about relations
# visit: https://collerek.github.io/ormar/relations/
# to read more about joins and subqueries
# visit: https://collerek.github.io/ormar/queries/joins-and-subqueries/
async def filter_and_sort():
# to filter the query you can use filter() or pass key-value pars to
# get(), all() etc.
# to use special methods or access related model fields use double
# underscore like to filter by the name of the author use author__name
# Django style
books = await Book.objects.all(author__name="J.R.R. Tolkien")
# python style
books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien")
assert len(books) == 3
# filter can accept special methods also separated with double underscore
# to issue sql query ` where authors.name like "%tolkien%"` that is not
# case sensitive (hence small t in Tolkien)
# Django style
books = await Book.objects.filter(author__name__icontains="tolkien").all()
# python style
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).all()
assert len(books) == 3
# to sort use order_by() function of queryset
# to sort decreasing use hyphen before the field name
# same as with filter you can use double underscores to access related fields
# Django style
books = (
await Book.objects.filter(author__name__icontains="tolkien")
.order_by("-year")
.all()
)
# python style
books = (
await Book.objects.filter(Book.author.name.icontains("tolkien"))
.order_by(Book.year.desc())
.all()
)
assert len(books) == 3
assert books[0].title == "The Silmarillion"
assert books[2].title == "The Hobbit"
# to read more about filtering and ordering
# visit: https://collerek.github.io/ormar/queries/filter-and-sort/
async def subset_of_columns():
# to exclude some columns from loading when querying the database
# you can use fields() method
hobbit = await Book.objects.fields(["title"]).get(title="The Hobbit")
# note that fields not included in fields are empty (set to None)
assert hobbit.year is None
assert hobbit.author is None
# selected field is there
assert hobbit.title == "The Hobbit"
# alternatively you can provide columns you want to exclude
hobbit = await Book.objects.exclude_fields(["year"]).get(title="The Hobbit")
# year is still not set
assert hobbit.year is None
# but author is back
assert hobbit.author is not None
# also you cannot exclude primary key column - it's always there
# even if you EXPLICITLY exclude it it will be there
# note that each model have a shortcut for primary_key column which is pk
# and you can filter/access/set the values by this alias like below
assert hobbit.pk is not None
# note that you cannot exclude fields that are not nullable
# (required) in model definition
try:
await Book.objects.exclude_fields(["title"]).get(title="The Hobbit")
except pydantic.ValidationError:
print("Cannot exclude non nullable field title")
# to read more about selecting subset of columns
# visit: https://collerek.github.io/ormar/queries/select-columns/
async def pagination():
# to limit number of returned rows use limit()
books = await Book.objects.limit(1).all()
assert len(books) == 1
assert books[0].title == "The Hobbit"
# to offset number of returned rows use offset()
books = await Book.objects.limit(1).offset(1).all()
assert len(books) == 1
assert books[0].title == "The Lord of the Rings"
# alternatively use paginate that combines both
books = await Book.objects.paginate(page=2, page_size=2).all()
assert len(books) == 2
# note that we removed one book of Sapkowski in delete()
# and recreated The Silmarillion - by default when no order_by is set
# ordering sorts by primary_key column
assert books[0].title == "The Witcher"
assert books[1].title == "The Silmarillion"
# to read more about pagination and number of rows
# visit: https://collerek.github.io/ormar/queries/pagination-and-rows-number/
async def aggregations():
# count:
assert 2 == await Author.objects.count()
# exists
assert await Book.objects.filter(title="The Hobbit").exists()
# maximum
assert 1990 == await Book.objects.max(columns=["year"])
# minimum
assert 1937 == await Book.objects.min(columns=["year"])
# average
assert 1964.75 == await Book.objects.avg(columns=["year"])
# sum
assert 7859 == await Book.objects.sum(columns=["year"])
# to read more about aggregated functions
# visit: https://collerek.github.io/ormar/queries/aggregations/
async def raw_data():
# extract raw data in a form of dicts or tuples
# note that this skips the validation(!) as models are
# not created from parsed data
# get list of objects as dicts
assert await Book.objects.values() == [
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937},
{"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955},
{"id": 4, "author": 2, "title": "The Witcher", "year": 1990},
{"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977},
]
# get list of objects as tuples
assert await Book.objects.values_list() == [
(1, 1, "The Hobbit", 1937),
(2, 1, "The Lord of the Rings", 1955),
(4, 2, "The Witcher", 1990),
(5, 1, "The Silmarillion", 1977),
]
# filter data - note how you always get a list
assert await Book.objects.filter(title="The Hobbit").values() == [
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}
]
# select only wanted fields
assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [
{"id": 1, "title": "The Hobbit"}
]
# if you select only one column you could flatten it with values_list
assert await Book.objects.values_list("title", flatten=True) == [
"The Hobbit",
"The Lord of the Rings",
"The Witcher",
"The Silmarillion",
]
# to read more about extracting raw values
# visit: https://collerek.github.io/ormar/queries/aggregations/
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 base_ormar_config.database:
await function()
# note that if you use framework like `fastapi` you shouldn't connect
# in your endpoints but have a global connection pool
# check https://collerek.github.io/ormar/fastapi/ and section with db connection
async def cleanup_database():
"""Drop all tables from the database."""
if not base_ormar_config.database.is_connected:
await base_ormar_config.database.connect()
async with base_ormar_config.database.engine.begin() as conn:
await conn.run_sync(base_ormar_config.metadata.drop_all)
if base_ormar_config.database.is_connected:
await base_ormar_config.database.disconnect()
# gather and execute all functions
# note that normally you use gather() function to run several functions
# concurrently but we actually modify the data and we rely on the order of functions
for func in [
create,
read,
update,
delete,
joins,
filter_and_sort,
subset_of_columns,
pagination,
aggregations,
raw_data,
]:
print(f"Executing: {func.__name__}")
asyncio.run(with_connect(func))
# drop the database tables
asyncio.run(cleanup_database())
ormar-orm-ormar-a7e7fde/examples/utils.py 0000664 0000000 0000000 00000002247 15211567312 0020667 0 ustar 00root root 0000000 0000000 import functools
import ormar
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):
# Connect database if not already connected
if not base_config.database.is_connected:
await base_config.database.connect()
# Drop and create tables
async with base_config.database.engine.begin() as conn:
await conn.run_sync(base_config.metadata.drop_all)
await conn.run_sync(base_config.metadata.create_all)
try:
await func(*args)
finally:
# Drop tables and cleanup
async with base_config.database.engine.begin() as conn:
await conn.run_sync(base_config.metadata.drop_all)
# Disconnect and dispose engine
if base_config.database.is_connected:
await base_config.database.disconnect()
return wrapped
return wrapper
ormar-orm-ormar-a7e7fde/mkdocs.yml 0000664 0000000 0000000 00000005750 15211567312 0017344 0 ustar 00root root 0000000 0000000 site_name: ormar
site_url: 'https://ormar-orm.github.io/ormar/'
site_description: A simple async ORM with fastapi in mind and pydantic validation.
nav:
- Overview: index.md
- Installation: install.md
- Models:
- Definition: models/index.md
- Inheritance: models/inheritance.md
- Methods: models/methods.md
- Migrations: models/migrations.md
- Internals: models/internals.md
- Fields:
- Common parameters: fields/common-parameters.md
- Fields types: fields/field-types.md
- Pydantic only fields: fields/pydantic-fields.md
- Fields encryption: fields/encryption.md
- Relations:
- 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
- queries/create.md
- queries/read.md
- queries/update.md
- queries/delete.md
- queries/joins-and-subqueries.md
- queries/filter-and-sort.md
- queries/select-columns.md
- queries/pagination-and-rows-number.md
- queries/aggregations.md
- Return raw data: queries/raw-data.md
- Signals: signals.md
- Transactions: transactions.md
- Use with Fastapi:
- Quick Start: fastapi/index.md
- 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
- Api (BETA): api/
repo_name: ormar-orm/ormar
repo_url: https://github.com/ormar-orm/ormar
theme:
name: material
highlightjs: true
hljs_languages:
- python
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: indigo
accent: indigo
toggle:
icon: material/lightbulb
name: Switch to light mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: indigo
accent: indigo
toggle:
icon: material/lightbulb-outline
name: Switch to dark mode
analytics:
gtag: G-ZJWZYM5DNM
markdown_extensions:
- admonition
- pymdownx.superfences
- pymdownx.snippets:
base_path: docs
- pymdownx.inlinehilite
- pymdownx.highlight:
linenums: true
auto_title: true
plugins:
- mike:
alias_type: symlink
canonical_version: latest
version_selector: true
- search
- gen-files:
scripts:
- docs/gen_ref_pages.py
- literate-nav:
nav_file: SUMMARY.md
- section-index
- mkdocstrings:
watch:
- ormar
handlers:
python:
selection:
docstring_style: sphinx
rendering:
show_submodules: no
extra:
analytics:
provider: google
property: UA-72514911-3
version:
provider: mike
extra_javascript:
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js
- javascripts/config.js
extra_css:
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/default.min.css
ormar-orm-ormar-a7e7fde/ormar/ 0000775 0000000 0000000 00000000000 15211567312 0016452 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/__init__.py 0000664 0000000 0000000 00000006234 15211567312 0020570 0 ustar 00root root 0000000 0000000 """
The `ormar` package is an async mini ORM for Python, with support for **Postgres,
MySQL**, and **SQLite**.
The main benefit of using `ormar` are:
* getting an **async ORM that can be used with async frameworks**
(fastapi, starlette etc.)
* getting just **one model to maintain** - you don't have to maintain pydantic
and other orm model (sqlalchemy, peewee, gino etc.)
The goal was to create a simple ORM that can be **used directly
(as request and response models)
with `fastapi`** that bases it's data validation on pydantic.
Ormar - apart form obvious ORM in name - get it's name from ormar in swedish which means
snakes, and ormar(e) in italian which means cabinet.
And what's a better name for python ORM than snakes cabinet :)
"""
from ormar.protocols import QuerySetProtocol, RelationProtocol # noqa: I001
from importlib.metadata import version
from ormar.decorators import ( # noqa: I100
post_bulk_update,
post_delete,
post_relation_add,
post_relation_remove,
post_save,
post_update,
pre_delete,
pre_relation_add,
pre_relation_remove,
pre_save,
pre_update,
)
from ormar.exceptions import ( # noqa: I100
ModelDefinitionError,
MultipleMatches,
NoMatch,
)
from ormar.fields import (
DECODERS_MAP,
ENCODERS_MAP,
JSON,
SQL_ENCODERS_MAP,
UUID,
BaseField,
BigInteger,
Boolean,
CheckColumns,
Date,
DateTime,
Decimal,
EncryptBackends,
Enum,
Float,
ForeignKey,
ForeignKeyField,
IndexColumns,
Integer,
LargeBinary,
ManyToMany,
ManyToManyField,
ReferentialAction,
SmallInteger,
String,
Text,
Time,
UniqueColumns,
)
# noqa: I100
from ormar.databases.connection import DatabaseConnection
from ormar.models import ExcludableItems, Extra, Model, OrmarConfig
from ormar.queryset import NullsOrdering, OrderAction, QuerySet, and_, or_
from ormar.relations import RelationType
from ormar.signals import Signal
class UndefinedType: # pragma no cover
def __repr__(self) -> str:
return "OrmarUndefined"
Undefined = UndefinedType()
__version__ = version("ormar")
__all__ = [
"Integer",
"BigInteger",
"SmallInteger",
"Boolean",
"Time",
"Text",
"String",
"JSON",
"DateTime",
"Date",
"Decimal",
"Enum",
"Float",
"ManyToMany",
"Model",
"ModelDefinitionError",
"MultipleMatches",
"NoMatch",
"ForeignKey",
"QuerySet",
"RelationType",
"Undefined",
"UUID",
"UniqueColumns",
"IndexColumns",
"CheckColumns",
"ReferentialAction",
"QuerySetProtocol",
"RelationProtocol",
"post_bulk_update",
"post_delete",
"post_save",
"post_update",
"post_relation_add",
"post_relation_remove",
"pre_delete",
"pre_save",
"pre_update",
"pre_relation_remove",
"pre_relation_add",
"Signal",
"BaseField",
"ManyToManyField",
"ForeignKeyField",
"OrderAction",
"NullsOrdering",
"ExcludableItems",
"and_",
"or_",
"EncryptBackends",
"ENCODERS_MAP",
"SQL_ENCODERS_MAP",
"DECODERS_MAP",
"LargeBinary",
"Extra",
"OrmarConfig",
"DatabaseConnection",
]
ormar-orm-ormar-a7e7fde/ormar/databases/ 0000775 0000000 0000000 00000000000 15211567312 0020401 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/databases/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0022500 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/databases/connection.py 0000664 0000000 0000000 00000015433 15211567312 0023120 0 ustar 00root root 0000000 0000000 """
DatabaseConnection module - provides async database connection management.
"""
from contextlib import asynccontextmanager
from contextvars import ContextVar
from typing import Any, AsyncIterator, Optional
from sqlalchemy import event
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
from ormar.databases.query_executor import QueryExecutor
from ormar.databases.transaction import Transaction
_transaction_connection: ContextVar[Optional[AsyncConnection]] = ContextVar(
"_transaction_connection", default=None
)
class DatabaseConnection:
"""
Wrapper around SQLAlchemy AsyncEngine that provides a databases-compatible API.
"""
def __init__(self, url: str, **options: Any) -> None:
"""
Initialize database connection.
:param url: Database URL with an async driver (e.g., postgresql+asyncpg://)
:param options: Additional engine options
"""
self._force_rollback = options.pop("force_rollback", False)
self._url = url
# Set reasonable pool defaults if not provided
if "pool_size" not in options:
options["pool_size"] = 5
if "max_overflow" not in options:
options["max_overflow"] = 10
self._options = options
self._engine: Optional[AsyncEngine] = None
self._autocommit_engine: Optional[AsyncEngine] = None
self._global_transaction: Optional[Transaction] = None
async def connect(self) -> None:
"""Connect to the database by creating the async engine."""
if self._engine is None:
self._engine = create_async_engine(self._url, **self._options)
# View of the same engine/pool in AUTOCOMMIT mode. Standalone
# queries use this to avoid a BEGIN/COMMIT round-trip per call,
# matching legacy `databases`-library semantics. Explicit
# Transactions keep using ``self._engine`` so BEGIN / SAVEPOINT
# still work.
self._autocommit_engine = self._engine.execution_options(
isolation_level="AUTOCOMMIT"
)
# Set up SQLite foreign keys pragma if using SQLite
if self._engine.dialect.name == "sqlite": # pragma: nocover
@event.listens_for(self._engine.sync_engine, "connect")
def set_sqlite_pragma(dbapi_conn: Any, connection_record: Any) -> None:
cursor = dbapi_conn.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
if self._force_rollback:
assert self._global_transaction is None
self._global_transaction = Transaction(
self, force_rollback=self._force_rollback
)
await self._global_transaction.__aenter__()
async def disconnect(self) -> None:
"""
Disconnect from the database and dispose of the engine.
In case of force_rollback, also roll back the global transaction.
"""
if self._engine is not None:
if self._force_rollback:
assert self._global_transaction is not None
await self._global_transaction.__aexit__()
self._global_transaction = None
await self._engine.dispose()
self._engine = None
self._autocommit_engine = None
@property
def is_connected(self) -> bool:
"""Check if the engine is connected."""
return self._engine is not None
@property
def engine(self) -> AsyncEngine:
"""Get the async engine."""
assert self._engine is not None, "DatabaseConnection not connected"
return self._engine
@property
def dialect(self) -> Any:
"""Get the database dialect."""
return self.engine.dialect
@property
def url(self) -> str:
"""Get the database URL."""
return self._url
@asynccontextmanager
async def connection(self) -> AsyncIterator[AsyncConnection]:
"""
Get a connection from the pool.
If inside a transaction, returns the transaction connection.
"""
trans_conn = _transaction_connection.get()
if trans_conn is not None:
yield trans_conn
else:
async with self.engine.connect() as conn:
yield conn
def transaction(self, force_rollback: bool = False) -> Transaction:
"""
Create a transaction context manager.
:param force_rollback: If True, always rollback (used for testing)
"""
return Transaction(self, force_rollback=force_rollback)
@asynccontextmanager
async def get_query_executor(
self, *, transactional: bool = False
) -> AsyncIterator[QueryExecutor]:
"""
Get connection, reusing transaction connection if in transaction.
:param transactional: If True, open an explicit transaction for the
scope of the executor. Use for server-side cursors (``iterate``)
and multi-row ``execute_many`` batches, which need a real
transaction either because AUTOCOMMIT would commit each row
separately or because the driver (asyncpg) requires a
transaction for streaming.
:type transactional: bool
:return: QueryExecutor wrapping a connection
:rtype: QueryExecutor
"""
trans_conn = self.get_transaction_connection()
if trans_conn is not None:
# Inside a transaction - reuse the transaction's connection
yield QueryExecutor(trans_conn)
elif transactional:
async with self.transaction():
conn = self.get_transaction_connection()
assert conn is not None
yield QueryExecutor(conn)
else:
# Outside transaction: use AUTOCOMMIT view so each statement
# commits at the driver level with no extra BEGIN/COMMIT
# round-trip.
assert self._autocommit_engine is not None
async with self._autocommit_engine.connect() as conn:
yield QueryExecutor(conn)
def get_transaction_connection(self) -> Optional[AsyncConnection]:
"""Get the current transaction connection if in a transaction."""
return _transaction_connection.get()
def set_transaction_connection(self, conn: Optional[AsyncConnection]) -> None:
"""Set the current transaction connection."""
_transaction_connection.set(conn)
async def __aenter__(self) -> "DatabaseConnection":
"""Async context manager entry - connect to a database."""
await self.connect()
return self
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
"""Async context manager exit - disconnect from database."""
await self.disconnect()
ormar-orm-ormar-a7e7fde/ormar/databases/query_executor.py 0000664 0000000 0000000 00000007716 15211567312 0024051 0 ustar 00root root 0000000 0000000 """
QueryExecutor module - executes database queries using SQLAlchemy async API.
"""
from typing import Any, AsyncIterator, List, Mapping, Optional, Sequence, Union
from sqlalchemy import RowMapping, text
from sqlalchemy.engine import CursorResult
from sqlalchemy.ext.asyncio import AsyncConnection
from sqlalchemy.sql import Executable
class QueryExecutor:
"""
Executes database queries using SQLAlchemy async API.
Provides a databases-compatible interface.
"""
def __init__(self, connection: AsyncConnection) -> None:
"""
Initialize query executor.
:param connection: SQLAlchemy async connection
"""
self._connection = connection
async def fetch_all(self, query: Executable) -> List[Any]:
"""
Execute a query and fetch all rows.
:param query: SQLAlchemy query expression
:return: List of Row objects
"""
result: CursorResult[Any] = await self._connection.execute(query)
return list(result.mappings().all())
async def fetch_one(self, query: Executable) -> Optional[RowMapping]:
"""
Execute a query and fetch one row.
:param query: SQLAlchemy query expression
:return: Single Row object or None
"""
result: CursorResult[Any] = await self._connection.execute(query)
row = result.mappings().first()
return row
async def fetch_val(self, query: Executable, column: int = 0) -> Optional[Any]:
"""
Execute a query and fetch a single scalar value.
:param query: SQLAlchemy query expression
:param column: Column index to fetch (default 0)
:return: Scalar value or None
"""
result: CursorResult[Any] = await self._connection.execute(query)
return result.scalar()
async def execute(self, query: Executable) -> Any:
"""
Execute a query (INSERT, UPDATE, DELETE).
:param query: SQLAlchemy query expression
:return: For INSERT, the inserted primary key or ``None`` if the backend
cannot return one (e.g. Oracle MySQL inserting into a
non-AUTO_INCREMENT pk with a server default — no RETURNING support).
For UPDATE/DELETE, the row count.
"""
result: CursorResult[Any] = await self._connection.execute(query)
# For INSERT queries, try to get the inserted primary key via the
# dialect's best-available mechanism (RETURNING on PostgreSQL / SQLite
# 3.35+ / MariaDB 10.5+, LAST_INSERT_ID() on MySQL AUTO_INCREMENT).
# Do NOT fall back to rowcount here: rowcount is not a pk, and
# returning it would silently corrupt `Model.pk` in `save()`.
if result.context and result.context.isinsert:
if result.inserted_primary_key:
pk_value = result.inserted_primary_key[0]
if pk_value is not None:
return pk_value
if hasattr(result, "lastrowid") and result.lastrowid: # pragma: no cover
return result.lastrowid
return None # pragma: no cover
return result.rowcount if result.rowcount is not None else 0
async def execute_many(
self, query: Union[Executable, str], values: Sequence[Mapping[str, Any]]
) -> None:
"""
Execute a query multiple times with different parameter sets.
:param query: SQLAlchemy query expression or SQL string
:param values: Sequence of parameter mappings
"""
exec_query = text(query) if isinstance(query, str) else query
await self._connection.execute(exec_query, values)
async def iterate(self, query: Executable) -> AsyncIterator[Any]:
"""
Execute a query and iterate over results.
:param query: SQLAlchemy query expression
:return: Async iterator of Row objects
"""
async with self._connection.stream(query) as result:
async for row in result.mappings():
yield row
ormar-orm-ormar-a7e7fde/ormar/databases/transaction.py 0000664 0000000 0000000 00000006355 15211567312 0023311 0 ustar 00root root 0000000 0000000 """
Transaction module - provides transaction management with savepoint support.
"""
from contextvars import ContextVar
from types import TracebackType
from typing import TYPE_CHECKING, Optional, Type
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncTransaction
if TYPE_CHECKING: # pragma: no cover
from ormar.databases.connection import DatabaseConnection
_transaction_depth: ContextVar[int] = ContextVar("_transaction_depth", default=0)
class Transaction:
"""
Transaction context manager with support for nested transactions via savepoints.
"""
def __init__(
self,
database: "DatabaseConnection",
force_rollback: bool = False,
) -> None:
"""
Initialize transaction.
:param database: DatabaseConnection instance
:param force_rollback: If True, always rollback (used for testing)
"""
self._database = database
self._force_rollback = force_rollback
self._connection: Optional[AsyncConnection] = None
self._transaction: Optional[AsyncTransaction] = None
self._depth: int = 0
async def __aenter__(self) -> "Transaction":
"""Enter transaction context."""
self._depth = _transaction_depth.get()
# If this is the outermost transaction, get a new connection.
# This uses the main engine (not the AUTOCOMMIT view used by
# standalone queries) so ``connection.begin()`` and the nested
# ``begin_nested()`` savepoints below both work.
if self._depth == 0:
self._connection = await self._database.engine.connect().__aenter__()
self._database.set_transaction_connection(self._connection)
self._transaction = await self._connection.begin()
# SQLite requires an explicit BEGIN before SAVEPOINTs to prevent
# RELEASE SAVEPOINT from auto-committing when no outer transaction exists.
# Issue after conn.begin() to avoid conflicting with SQLAlchemy's autobegin.
if self._database.engine.dialect.name == "sqlite": # pragma: nocover
await self._connection.exec_driver_sql("BEGIN")
else:
# Nested transaction - use savepoint
self._connection = self._database.get_transaction_connection()
assert self._connection is not None
self._transaction = await self._connection.begin_nested()
_transaction_depth.set(self._depth + 1)
return self
async def __aexit__(
self,
exc_type: Optional[Type[BaseException]] = None,
exc_value: Optional[BaseException] = None,
traceback: Optional[TracebackType] = None,
) -> None:
"""Exit transaction context."""
try:
_transaction_depth.set(self._depth)
# Handle transaction completion
if self._transaction is not None:
if exc_type is not None or self._force_rollback:
await self._transaction.rollback()
else:
await self._transaction.commit()
finally:
if self._depth == 0:
self._database.set_transaction_connection(None)
if self._connection is not None:
await self._connection.close()
ormar-orm-ormar-a7e7fde/ormar/decorators/ 0000775 0000000 0000000 00000000000 15211567312 0020617 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/decorators/__init__.py 0000664 0000000 0000000 00000001213 15211567312 0022725 0 ustar 00root root 0000000 0000000 """
Module with all decorators that are exposed for users.
Currently only:
* predefined signals decorators (pre/post + save/update/delete)
"""
from ormar.decorators.signals import (
post_bulk_update,
post_delete,
post_relation_add,
post_relation_remove,
post_save,
post_update,
pre_delete,
pre_relation_add,
pre_relation_remove,
pre_save,
pre_update,
)
__all__ = [
"post_bulk_update",
"post_delete",
"post_save",
"post_update",
"pre_delete",
"pre_save",
"pre_update",
"post_relation_remove",
"post_relation_add",
"pre_relation_remove",
"pre_relation_add",
]
ormar-orm-ormar-a7e7fde/ormar/decorators/signals.py 0000664 0000000 0000000 00000014426 15211567312 0022640 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Callable, Union
if TYPE_CHECKING: # pragma: no cover
from ormar import Model
def receiver(
signal: str, senders: Union[type["Model"], list[type["Model"]]]
) -> Callable:
"""
Connect given function to all senders for given signal name.
:param signal: name of the signal to register to
:type signal: str
:param senders: one or a list of "Model" classes
that should have the signal receiver registered
:type senders: Union[type["Model"], list[type["Model"]]]
:return: returns the original function untouched
:rtype: Callable
"""
def _decorator(func: Callable) -> Callable:
"""
Internal decorator that does all the registering.
:param func: function to register as receiver
:type func: Callable
:return: untouched function already registered for given signal
:rtype: Callable
"""
if not isinstance(senders, list):
_senders = [senders]
else:
_senders = senders
for sender in _senders:
signals = getattr(sender.ormar_config.signals, signal)
signals.connect(func)
return func
return _decorator
def post_save(senders: Union[type["Model"], list[type["Model"]]]) -> Callable:
"""
Connect given function to all senders for post_save signal.
:param senders: one or a list of "Model" classes
that should have the signal receiver registered
:type senders: Union[type["Model"], list[type["Model"]]]
:return: returns the original function untouched
:rtype: Callable
"""
return receiver(signal="post_save", senders=senders)
def post_update(senders: Union[type["Model"], list[type["Model"]]]) -> Callable:
"""
Connect given function to all senders for post_update signal.
:param senders: one or a list of "Model" classes
that should have the signal receiver registered
:type senders: Union[type["Model"], list[type["Model"]]]
:return: returns the original function untouched
:rtype: Callable
"""
return receiver(signal="post_update", senders=senders)
def post_delete(senders: Union[type["Model"], list[type["Model"]]]) -> Callable:
"""
Connect given function to all senders for post_delete signal.
:param senders: one or a list of "Model" classes
that should have the signal receiver registered
:type senders: Union[type["Model"], list[type["Model"]]]
:return: returns the original function untouched
:rtype: Callable
"""
return receiver(signal="post_delete", senders=senders)
def pre_save(senders: Union[type["Model"], list[type["Model"]]]) -> Callable:
"""
Connect given function to all senders for pre_save signal.
:param senders: one or a list of "Model" classes
that should have the signal receiver registered
:type senders: Union[type["Model"], list[type["Model"]]]
:return: returns the original function untouched
:rtype: Callable
"""
return receiver(signal="pre_save", senders=senders)
def pre_update(senders: Union[type["Model"], list[type["Model"]]]) -> Callable:
"""
Connect given function to all senders for pre_update signal.
:param senders: one or a list of "Model" classes
that should have the signal receiver registered
:type senders: Union[type["Model"], list[type["Model"]]]
:return: returns the original function untouched
:rtype: Callable
"""
return receiver(signal="pre_update", senders=senders)
def pre_delete(senders: Union[type["Model"], list[type["Model"]]]) -> Callable:
"""
Connect given function to all senders for pre_delete signal.
:param senders: one or a list of "Model" classes
that should have the signal receiver registered
:type senders: Union[type["Model"], list[type["Model"]]]
:return: returns the original function untouched
:rtype: Callable
"""
return receiver(signal="pre_delete", senders=senders)
def pre_relation_add(senders: Union[type["Model"], list[type["Model"]]]) -> Callable:
"""
Connect given function to all senders for pre_relation_add signal.
:param senders: one or a list of "Model" classes
that should have the signal receiver registered
:type senders: Union[type["Model"], list[type["Model"]]]
:return: returns the original function untouched
:rtype: Callable
"""
return receiver(signal="pre_relation_add", senders=senders)
def post_relation_add(senders: Union[type["Model"], list[type["Model"]]]) -> Callable:
"""
Connect given function to all senders for post_relation_add signal.
:param senders: one or a list of "Model" classes
that should have the signal receiver registered
:type senders: Union[type["Model"], list[type["Model"]]]
:return: returns the original function untouched
:rtype: Callable
"""
return receiver(signal="post_relation_add", senders=senders)
def pre_relation_remove(senders: Union[type["Model"], list[type["Model"]]]) -> Callable:
"""
Connect given function to all senders for pre_relation_remove signal.
:param senders: one or a list of "Model" classes
that should have the signal receiver registered
:type senders: Union[type["Model"], list[type["Model"]]]
:return: returns the original function untouched
:rtype: Callable
"""
return receiver(signal="pre_relation_remove", senders=senders)
def post_relation_remove(
senders: Union[type["Model"], list[type["Model"]]],
) -> Callable:
"""
Connect given function to all senders for post_relation_remove signal.
:param senders: one or a list of "Model" classes
that should have the signal receiver registered
:type senders: Union[type["Model"], list[type["Model"]]]
:return: returns the original function untouched
:rtype: Callable
"""
return receiver(signal="post_relation_remove", senders=senders)
def post_bulk_update(senders: Union[type["Model"], list[type["Model"]]]) -> Callable:
"""
Connect given function to all senders for post_bulk_update signal.
:param senders: one or a list of "Model" classes
that should have the signal receiver registered
:type senders: Union[type["Model"], list[type["Model"]]]
:return: returns the original function untouched
:rtype: Callable
"""
return receiver(signal="post_bulk_update", senders=senders)
ormar-orm-ormar-a7e7fde/ormar/exceptions.py 0000664 0000000 0000000 00000003422 15211567312 0021206 0 ustar 00root root 0000000 0000000 """
Gathers all exceptions thrown by ormar.
"""
class AsyncOrmException(Exception):
"""
Base ormar Exception
"""
pass
class ModelDefinitionError(AsyncOrmException):
"""
Raised for errors related to the model definition itself:
* defining a Field without required parameters
* defining a model with more than one primary_key
* defining a model without primary_key
"""
pass
class ModelError(AsyncOrmException):
"""
Raised for initialization of model with non-existing field keyword.
"""
pass
class NoMatch(AsyncOrmException):
"""
Raised for database queries that has no matching result (empty result).
"""
pass
class MultipleMatches(AsyncOrmException):
"""
Raised for database queries that should return one row (i.e. get, first etc.)
but has multiple matching results in response.
"""
pass
class QueryDefinitionError(AsyncOrmException):
"""
Raised for errors in query definition:
* using contains or icontains filter with instance of the Model
* using Queryset.update() without filter and setting each flag to True
* using Queryset.delete() without filter and setting each flag to True
"""
pass
class RelationshipInstanceError(AsyncOrmException):
pass
class ModelPersistenceError(AsyncOrmException):
"""
Raised for update of models without primary_key set (cannot retrieve from db)
or for saving a model with relation to unsaved model (cannot extract fk value).
"""
pass
class SignalDefinitionError(AsyncOrmException):
"""
Raised when non callable receiver is passed as signal callback.
"""
pass
class ModelListEmptyError(AsyncOrmException):
"""
Raised for objects is empty when bulk_update
"""
pass
ormar-orm-ormar-a7e7fde/ormar/fields/ 0000775 0000000 0000000 00000000000 15211567312 0017720 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/fields/__init__.py 0000664 0000000 0000000 00000003011 15211567312 0022024 0 ustar 00root root 0000000 0000000 """
Module with classes and constructors for ormar Fields.
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 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,
DateTime,
Decimal,
Enum,
Float,
Integer,
LargeBinary,
SmallInteger,
String,
Text,
Time,
)
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
__all__ = [
"Decimal",
"BigInteger",
"SmallInteger",
"Boolean",
"Date",
"DateTime",
"String",
"JSON",
"Integer",
"Text",
"Float",
"Time",
"UUID",
"Enum",
"ForeignKey",
"ManyToMany",
"ManyToManyField",
"BaseField",
"ForeignKeyField",
"ThroughField",
"Through",
"EncryptBackends",
"EncryptBackend",
"DECODERS_MAP",
"ENCODERS_MAP",
"SQL_ENCODERS_MAP",
"LargeBinary",
"UniqueColumns",
"IndexColumns",
"CheckColumns",
"ReferentialAction",
]
ormar-orm-ormar-a7e7fde/ormar/fields/base.py 0000664 0000000 0000000 00000037525 15211567312 0021220 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Any, Literal, Optional, Union, overload
import sqlalchemy
from pydantic.fields import FieldInfo, _Unset
import ormar # noqa I101
from ormar import ModelDefinitionError
from ormar.fields.sqlalchemy_encrypted import (
EncryptBackend,
EncryptBackends,
EncryptedString,
)
if TYPE_CHECKING: # pragma no cover
from ormar.models import Model, NewBaseModel
class BaseField(FieldInfo): # type: ignore[misc]
"""
BaseField serves as a parent class for all basic Fields in ormar.
It keeps all common parameters available for all fields as well as
set of useful functions.
All values are kept as class variables, ormar Fields are never instantiated.
Subclasses pydantic.FieldInfo to keep the fields related
to pydantic field types like ConstrainedStr
"""
def __init__(self, **kwargs: Any) -> None:
self.__type__: type = kwargs.pop("__type__", None)
self.__pydantic_type__: type = kwargs.pop("__pydantic_type__", None)
self.__sample__: type = kwargs.pop("__sample__", None)
self.related_name = kwargs.pop("related_name", None)
self.column_type: sqlalchemy.Column = kwargs.pop("column_type", None)
self.constraints: list = kwargs.pop("constraints", list())
self.name: str = kwargs.pop("name", None)
self.db_alias: str = kwargs.pop("alias", None)
self.primary_key: bool = kwargs.pop("primary_key", False)
self.autoincrement: bool = kwargs.pop("autoincrement", False)
self.nullable: bool = kwargs.pop("nullable", False)
self.sql_nullable: bool = kwargs.pop("sql_nullable", False)
self.index: bool = kwargs.pop("index", False)
self.unique: bool = kwargs.pop("unique", False)
self.virtual: bool = kwargs.pop(
"virtual", None
) # ManyToManyFields and reverse ForeignKeyFields
self.is_multi: bool = kwargs.pop("is_multi", None) # ManyToManyField
self.is_relation: bool = kwargs.pop(
"is_relation", None
) # ForeignKeyField + subclasses
self.is_through: bool = kwargs.pop("is_through", False) # ThroughFields
self.through_relation_name = kwargs.pop("through_relation_name", None)
self.through_reverse_relation_name = kwargs.pop(
"through_reverse_relation_name", None
)
self.through_foreign_key_name: Optional[str] = kwargs.pop(
"through_foreign_key_name", None
)
self.through_reverse_foreign_key_name: Optional[str] = kwargs.pop(
"through_reverse_foreign_key_name", None
)
self.through_relation_nullable: bool = kwargs.pop(
"through_relation_nullable", True
)
self.through_reverse_relation_nullable: bool = kwargs.pop(
"through_reverse_relation_nullable", True
)
self.skip_reverse: bool = kwargs.pop("skip_reverse", False)
self.skip_field: bool = kwargs.pop("skip_field", False)
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(
"self_reference_primary", None
)
self.orders_by: Optional[list[str]] = kwargs.pop("orders_by", None)
self.related_orders_by: Optional[list[str]] = kwargs.pop(
"related_orders_by", None
)
self.encrypt_secret: str = kwargs.pop("encrypt_secret", None)
self.encrypt_backend: EncryptBackends = kwargs.pop(
"encrypt_backend", EncryptBackends.NONE
)
self.encrypt_custom_backend: Optional[type[EncryptBackend]] = kwargs.pop(
"encrypt_custom_backend", None
)
self.ormar_default: Any = kwargs.pop("default", None)
self.server_default: Any = kwargs.pop("server_default", None)
self.on_update: Any = kwargs.pop("on_update", None)
self.comment: str = kwargs.pop("comment", None)
self.represent_as_base64_str: bool = kwargs.pop(
"represent_as_base64_str", False
)
for name, value in kwargs.items():
setattr(self, name, value)
kwargs.update(self.get_pydantic_default())
super().__init__(**kwargs)
def is_valid_uni_relation(self) -> bool:
"""
Checks if field is a relation definition but only for ForeignKey relation,
so excludes ManyToMany fields, as well as virtual ForeignKey
(second side of FK relation).
Is used to define if a field is a db ForeignKey column that
should be saved/populated when dealing with internal/own
Model columns only.
:return: result of the check
:rtype: bool
"""
return not self.is_multi and not self.virtual
def get_alias(self) -> str:
"""
Used to translate Model column names to database column names during db queries.
:return: returns custom database column name if defined by user,
otherwise field name in ormar/pydantic
:rtype: str
"""
return self.db_alias if self.db_alias else self.name
def get_pydantic_default(self) -> dict:
"""
Generates base pydantic.FieldInfo with only default and optionally
required to fix pydantic Json field being set to required=False.
Used in an ormar Model Metaclass.
:return: instance of base pydantic.FieldInfo
:rtype: pydantic.FieldInfo
"""
base = self.default_value()
if base is None:
base = dict(default=None) if self.nullable else dict(default=_Unset)
return base
def default_value(self, use_server: bool = False) -> Optional[dict]:
"""
Returns a FieldInfo instance with populated default
(static) or default_factory (function).
If the field is a autoincrement primary key the default is None.
Otherwise field have to has either default, or default_factory populated.
If all default conditions fail None is returned.
Used in converting to pydantic FieldInfo.
:param use_server: flag marking if server_default should be
treated as default value, default False
:type use_server: bool
:return: returns a call to pydantic.Field
which is returning a FieldInfo instance
:rtype: Optional[pydantic.FieldInfo]
"""
if self.is_auto_primary_key():
return dict(default=None)
if self.has_default(use_server=use_server):
default = (
self.ormar_default
if self.ormar_default is not None
else self.server_default
)
if callable(default):
return dict(default_factory=default)
return dict(default=default)
return None
@overload
def get_default(
self,
*,
call_default_factory: Literal[True],
validated_data: Union[dict[str, Any], None] = None,
) -> Any: ...
@overload
def get_default(self, *, call_default_factory: Literal[False] = ...) -> Any: ...
def get_default(
self,
*,
call_default_factory: bool = True,
validated_data: Union[dict[str, Any], None] = None,
use_server: bool = False,
) -> Any: # noqa CCR001
"""
Return default value for a field.
If the field is Callable the function is called and actual result is returned.
Used to populate default_values for pydantic Model in ormar Model Metaclass.
:param use_server: flag marking if server_default should be
treated as default value, default False
:type use_server: bool
:return: default value for the field if set, otherwise implicit None
:rtype: Any
"""
if self.has_default():
default = (
self.ormar_default
if self.ormar_default is not None
else self._get_default_server_value(use_server=use_server)
)
return self._get_default_callable_value(
default=default,
call_default_factory=call_default_factory,
)
def _get_default_server_value(self, use_server: bool) -> Any: # pragma: no cover
"""
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:
"""
Checks if the field has default value set.
:param use_server: flag marking if server_default should be
treated as default value, default False
:type use_server: bool
:return: result of the check if default value is set
:rtype: bool
"""
return self.ormar_default is not None or (
self.server_default is not None and use_server
)
def has_on_update(self) -> bool:
"""
Checks if the field has an on_update value or callable configured.
:return: result of the check if on_update value is set
:rtype: bool
"""
return self.on_update is not None
def get_on_update(self) -> Any:
"""
Resolves the on_update value, calling it if it is a callable.
:return: resolved on_update value
:rtype: Any
"""
return self.on_update() if callable(self.on_update) else self.on_update
def is_auto_primary_key(self) -> bool:
"""
Checks if field is first a primary key and if it,
it's than check if it's set to autoincrement.
Autoincrement primary_key is nullable/optional.
:return: result of the check for primary key and autoincrement
:rtype: bool
"""
if self.primary_key:
return self.autoincrement
return False
def construct_constraints(self) -> list:
"""
Converts list of ormar constraints into sqlalchemy ForeignKeys.
Has to be done dynamically as sqlalchemy binds ForeignKey to the table.
And we need a new ForeignKey for subclasses of current model
:return: list of sqlalchemy foreign keys - by default one.
:rtype: list[sqlalchemy.schema.ForeignKey]
"""
constraints = []
for constraint in self.constraints:
owner_table = self.owner.ormar_config.tablename
target_table = self.to.ormar_config.tablename
target_pk = self.to.get_column_alias(self.to.ormar_config.pkname)
default_name = f"fk_{owner_table}_{target_table}_{target_pk}_{self.name}"
constraints.append(
sqlalchemy.ForeignKey(
constraint.reference,
ondelete=constraint.ondelete,
onupdate=constraint.onupdate,
name=constraint.name or default_name,
)
)
return constraints
def get_column(self, name: str) -> sqlalchemy.Column:
"""
Returns definition of sqlalchemy.Column used in creation of sqlalchemy.Table.
Populates name, column type constraints, as well as a number of parameters like
primary_key, index, unique, nullable, default and server_default.
:param name: name of the db column - used if alias is not set
:type name: str
:return: actual definition of the database column as sqlalchemy requires.
:rtype: sqlalchemy.Column
"""
if self.encrypt_backend == EncryptBackends.NONE:
column: sqlalchemy.Column = sqlalchemy.Column(
self.db_alias or name,
self.column_type,
*self.construct_constraints(),
primary_key=self.primary_key,
autoincrement=self.autoincrement,
nullable=self.sql_nullable,
index=self.index,
unique=self.unique,
default=self.ormar_default,
server_default=self.server_default,
comment=self.comment,
)
else:
column = self._get_encrypted_column(name=name)
return column
def _get_encrypted_column(self, name: str) -> sqlalchemy.Column:
"""
Returns EncryptedString column type instead of actual column.
:param name: column name
:type name: str
:return: newly defined column
:rtype: sqlalchemy.Column
"""
if self.primary_key or self.is_relation:
raise ModelDefinitionError(
"Primary key field and relations fields cannot be encrypted!"
)
column: sqlalchemy.Column = sqlalchemy.Column(
self.db_alias or name,
EncryptedString(
_field_type=self,
encrypt_secret=self.encrypt_secret,
encrypt_backend=self.encrypt_backend,
encrypt_custom_backend=self.encrypt_custom_backend,
),
nullable=self.nullable,
index=self.index,
unique=self.unique,
default=self.ormar_default,
server_default=self.server_default,
)
return column
def expand_relationship(
self,
value: Any,
child: Union["Model", "NewBaseModel"],
to_register: bool = True,
) -> Any:
"""
Function overwritten for relations, in basic field the value is returned as is.
For relations the child model is first constructed (if needed),
registered in relation and returned.
For relation fields the value can be a pk value (Any type of field),
dict (from Model) or actual instance/list of a "Model".
:param value: a Model field value, returned untouched for non relation fields.
:type value: Any
:param child: a child Model to register
:type child: Union["Model", "NewBaseModel"]
:param to_register: flag if the relation should be set in RelationshipManager
:type to_register: bool
:return: returns untouched value for normal fields, expands only for relations
:rtype: Any
"""
return value # pragma: no cover
def set_self_reference_flag(self) -> None:
"""
Sets `self_reference` to True if field to and owner are same model.
:return: None
:rtype: None
"""
if self.owner is not None and (
self.owner == self.to or self.owner.ormar_config == self.to.ormar_config
):
self.self_reference = True
self.self_reference_primary = self.name
def has_unresolved_forward_refs(self) -> bool:
"""
Verifies if the filed has any ForwardRefs that require updating before the
model can be used.
:return: result of the check
:rtype: bool
"""
return False
def evaluate_forward_ref(self, globalns: Any, localns: Any) -> None:
"""
Evaluates the ForwardRef to actual Field based on global and local namespaces
:param globalns: global namespace
:type globalns: Any
:param localns: local namespace
:type localns: Any
:return: None
:rtype: None
"""
def get_related_name(self) -> str:
"""
Returns name to use for reverse relation.
It's either set as `related_name` or by default it's owner model. get_name + 's'
:return: name of the related_name or default related name.
:rtype: str
"""
return "" # pragma: no cover
ormar-orm-ormar-a7e7fde/ormar/fields/constraints.py 0000664 0000000 0000000 00000001504 15211567312 0022641 0 ustar 00root root 0000000 0000000 from typing import Any, Optional
from sqlalchemy import CheckConstraint, Index, UniqueConstraint
class UniqueColumns(UniqueConstraint):
"""
Subclass of sqlalchemy.UniqueConstraint.
Used to avoid importing anything from sqlalchemy by user.
"""
class IndexColumns(Index):
def __init__(self, *args: Any, name: Optional[str] = None, **kw: Any) -> None:
if not name:
name = "TEMPORARY_NAME"
super().__init__(name, *args, **kw)
"""
Subclass of sqlalchemy.Index.
Used to avoid importing anything from sqlalchemy by user.
"""
class CheckColumns(CheckConstraint):
"""
Subclass of sqlalchemy.CheckConstraint.
Used to avoid importing anything from sqlalchemy by user.
Note that some databases do not actively support check constraints such as MySQL.
"""
ormar-orm-ormar-a7e7fde/ormar/fields/foreign_key.py 0000664 0000000 0000000 00000057512 15211567312 0022605 0 ustar 00root root 0000000 0000000 import string
import sys
import uuid
from dataclasses import dataclass
from functools import cached_property
from random import choices
from typing import TYPE_CHECKING, Any, ForwardRef, Optional, Union, cast, overload
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
if TYPE_CHECKING: # pragma no cover
from ormar.fields import ManyToManyField
from ormar.models import Model, NewBaseModel, T
def create_dummy_instance(fk: type["T"], pk: Any = None) -> "T":
"""
Ormar never returns you a raw data.
So if you have a related field that has a value populated
it will construct you a Model instance out of it.
Creates a "fake" instance of passed Model from pk value.
The instantiated Model has only pk value filled.
To achieve this __pk_only__ flag has to be passed as it skips the validation.
If the nested related Models are required they are set with -1 as pk value.
:param fk: class of the related Model to which instance should be constructed
:type fk: Model class
:param pk: value of the primary_key column
:type pk: Any
:return: Model instance populated with only pk
:rtype: Model
"""
init_dict = {
**{fk.ormar_config.pkname: pk or -1},
**{
k: create_dummy_instance(v.to)
for k, v in fk.ormar_config.model_fields.items()
if v.is_relation and not v.nullable and not v.virtual
},
}
return cast("T", fk._internal_construct(_pk_only=True, _excluded=None, **init_dict))
def create_dummy_model(
base_model: type["T"],
pk_field: Union[BaseField, "ForeignKeyField", "ManyToManyField"],
) -> type["BaseModel"]:
"""
Used to construct a dummy pydantic model for type hints and pydantic validation.
Populates only pk field and set it to desired type.
:param base_model: class of target dummy model
:type base_model: Model class
:param pk_field: ormar Field to be set on pydantic Model
:type pk_field: Union[BaseField, "ForeignKeyField", "ManyToManyField"]
:return: constructed dummy model
:rtype: pydantic.BaseModel
"""
alias = (
"".join(choices(string.ascii_uppercase, k=6)) # + uuid.uuid4().hex[:4]
).lower()
fields = {f"{pk_field.name}": (pk_field.__type__, None)}
dummy_model = create_model( # type: ignore
f"PkOnly{base_model.get_name(lower=False)}{alias}",
__module__=base_model.__module__,
**fields, # type: ignore
)
return dummy_model
def populate_fk_params_based_on_to_model(
to: type["T"],
nullable: bool,
onupdate: Optional[str] = None,
ondelete: Optional[str] = None,
foreign_key_name: 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.
:param to: target related ormar Model
:type to: Model class
:param nullable: marks field as optional/ required
:type nullable: bool
:param onupdate: parameter passed to sqlalchemy.ForeignKey.
How to treat child rows on update of parent (the one where FK is defined) model.
:type onupdate: str
:param ondelete: parameter passed to sqlalchemy.ForeignKey.
How to treat child rows on delete of parent (the one where FK is defined) model.
:type ondelete: str
:param foreign_key_name: optional override for the foreign key constraint name
emitted in migrations. Defaults to ``None``, which lets ormar generate a name.
:type foreign_key_name: Optional[str]
:return: tuple with target pydantic type, list of fk constraints and target col type
:rtype: tuple[Any, list, Any]
"""
from ormar.models.helpers.sqlalchemy import qualified_fk_reference
fk_string = qualified_fk_reference(to, 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]
if not nullable
else Optional[Union[to_field.__type__, to, pk_only_model]]
)
constraints = [
ForeignKeyConstraint(
reference=fk_string,
ondelete=ondelete,
onupdate=onupdate,
name=foreign_key_name,
)
]
column_type = to_field.column_type
return __type__, constraints, column_type, pk_only_model
def validate_not_allowed_fields(kwargs: dict) -> None:
"""
Verifies if not allowed parameters are set on relation models.
Usually they are omitted later anyway but this way it's explicitly
notify the user that it's not allowed/ supported.
:raises ModelDefinitionError: if any forbidden field is set
:param kwargs: dict of kwargs to verify passed to relation field
:type kwargs: dict
"""
default = kwargs.pop("default", None)
encrypt_secret = kwargs.pop("encrypt_secret", None)
encrypt_backend = kwargs.pop("encrypt_backend", None)
encrypt_custom_backend = kwargs.pop("encrypt_custom_backend", None)
overwrite_pydantic_type = kwargs.pop("overwrite_pydantic_type", None)
not_supported = [
default,
encrypt_secret,
encrypt_backend,
encrypt_custom_backend,
overwrite_pydantic_type,
]
if any(x is not None for x in not_supported):
raise ModelDefinitionError(
f"Argument {next((x for x in not_supported if x is not None))} "
f"is not supported "
"on relation fields!"
)
def validate_referential_action(
action: Optional[Union[ReferentialAction, str]],
) -> Optional[str]:
"""
Validation `onupdate` and `ondelete` action cast to a string value
:raises ModelDefinitionError: if action is a not valid name string value
:param action: referential action attribute or name string
:type action: Optional[Union[ReferentialAction, str]]
:rtype: Optional[str]
"""
if action is not None and not isinstance(action, ReferentialAction):
try:
action = ReferentialAction(action.upper())
except (ValueError, AttributeError):
raise ModelDefinitionError(f"{action} ReferentialAction not supported.")
return action.value if action is not None else None
@dataclass
class ForeignKeyConstraint:
"""
Internal container to store ForeignKey definitions used later
to produce sqlalchemy.ForeignKeys
"""
reference: Union[str, sqlalchemy.Column]
name: Optional[str]
ondelete: Optional[str]
onupdate: Optional[str]
@overload
def ForeignKey(to: type["T"], **kwargs: Any) -> "T": # pragma: no cover
...
@overload
def ForeignKey(to: ForwardRef, **kwargs: Any) -> "Model": # pragma: no cover
...
def ForeignKey( # type: ignore # noqa CFQ002
to: Union[type["T"], "ForwardRef"],
*,
name: Optional[str] = None,
unique: bool = False,
nullable: bool = True,
related_name: Optional[str] = None,
virtual: bool = False,
onupdate: Union[ReferentialAction, str, None] = None,
ondelete: Union[ReferentialAction, str, None] = None,
foreign_key_name: Optional[str] = None,
**kwargs: Any,
) -> "T":
"""
Despite a name it's a function that returns constructed ForeignKeyField.
This function is actually used in model declaration (as ormar.ForeignKey(ToModel)).
Accepts number of relation setting parameters as well as all BaseField ones.
:param to: target related ormar Model
:type to: Model class
:param name: name of the database field - later called alias
:type name: str
:param unique: parameter passed to sqlalchemy.ForeignKey, unique flag
:type unique: bool
:param nullable: marks field as optional/ required
:type nullable: bool
:param related_name: name of reversed FK relation populated for you on to model
:type related_name: str
:param virtual: marks if relation is virtual.
It is for reversed FK and auto generated FK on through model in Many2Many relations.
:type virtual: bool
:param onupdate: parameter passed to sqlalchemy.ForeignKey.
How to treat child rows on update of parent (the one where FK is defined) model.
:type onupdate: Union[ReferentialAction, str]
:param ondelete: parameter passed to sqlalchemy.ForeignKey.
How to treat child rows on delete of parent (the one where FK is defined) model.
:type ondelete: Union[ReferentialAction, str]
:param foreign_key_name: optional override for the foreign key constraint name
generated in migrations. Useful when the auto-generated name exceeds a database
specific identifier length limit (for example MySQL 64 chars).
:type foreign_key_name: Optional[str]
:param kwargs: all other args to be populated by BaseField
:type kwargs: Any
:return: ormar ForeignKeyField with relation to selected model
:rtype: ForeignKeyField
"""
onupdate = validate_referential_action(action=onupdate)
ondelete = validate_referential_action(action=ondelete)
owner = kwargs.pop("owner", None)
self_reference = kwargs.pop("self_reference", False)
orders_by = kwargs.pop("orders_by", None)
related_orders_by = kwargs.pop("related_orders_by", None)
skip_reverse = kwargs.pop("skip_reverse", False)
skip_field = kwargs.pop("skip_field", False)
sql_nullable = kwargs.pop("sql_nullable", None)
sql_nullable = nullable if sql_nullable is None else sql_nullable
index = kwargs.pop("index", False)
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,
pk_only_model,
) = populate_fk_params_based_on_to_model(
to=to, # type: ignore
nullable=nullable,
ondelete=ondelete,
onupdate=onupdate,
foreign_key_name=foreign_key_name,
)
namespace = dict(
__type__=__type__,
to=to,
to_pk_only=pk_only_model,
through=None,
alias=name,
name=kwargs.pop("real_name", None),
nullable=nullable,
sql_nullable=sql_nullable,
constraints=constraints,
unique=unique,
column_type=column_type,
related_name=related_name,
virtual=virtual,
primary_key=False,
index=index,
default=None,
server_default=None,
onupdate=onupdate,
ondelete=ondelete,
foreign_key_name=foreign_key_name,
owner=owner,
self_reference=self_reference,
is_relation=True,
orders_by=orders_by,
related_orders_by=related_orders_by,
skip_reverse=skip_reverse,
skip_field=skip_field,
**kwargs,
)
Field = type("ForeignKey", (ForeignKeyField, BaseField), {})
return Field(**namespace)
class ForeignKeyField(BaseField): # type: ignore[misc]
"""
Actual class returned from ForeignKey function call and stored in model_fields.
"""
def __init__(self, **kwargs: Any) -> None:
if TYPE_CHECKING: # pragma: no cover
self.__type__: type
self.to: type["Model"]
self.ondelete: str = kwargs.pop("ondelete", None)
self.onupdate: str = kwargs.pop("onupdate", None)
self.foreign_key_name: Optional[str] = kwargs.pop("foreign_key_name", None)
super().__init__(**kwargs)
def get_source_related_name(self) -> str:
"""
Returns name to use for source relation name.
For FK it's the same, differs for m2m fields.
It's either set as `related_name` or by default it's owner model. get_name + 's'
:return: name of the related_name or default related name.
:rtype: str
"""
return self.get_related_name()
def get_related_name(self) -> str:
"""
Returns name to use for reverse relation.
It's either set as `related_name` or by default it's owner model. get_name + 's'
:return: name of the related_name or default related name.
:rtype: str
"""
return self.related_name or self.owner.get_name() + "s"
def default_target_field_name(self) -> str:
"""
Returns default target model name on through model.
:return: name of the field
:rtype: str
"""
prefix = "from_" if self.self_reference else ""
return self.through_reverse_relation_name or f"{prefix}{self.to.get_name()}"
def default_source_field_name(self) -> str:
"""
Returns default target model name on through model.
:return: name of the field
:rtype: str
"""
prefix = "to_" if self.self_reference else ""
return self.through_relation_name or f"{prefix}{self.owner.get_name()}"
def get_filter_clause_target(self) -> type["Model"]:
return self.to
def get_model_relation_fields(self, use_alias: bool = False) -> str:
"""
Extract names of the database columns or model fields that are connected
with given relation based on use_alias switch and which side of the relation
the current field is - reverse or normal.
:param use_alias: use db names aliases or model fields
:type use_alias: bool
:return: name or names of the related columns/ fields
:rtype: Union[str, list[str]]
"""
if use_alias:
return self._get_model_relation_fields_alias()
return self._get_model_relation_fields_name()
def _get_model_relation_fields_name(self) -> str:
if self.virtual:
return self.owner.ormar_config.pkname
return self.name
def _get_model_relation_fields_alias(self) -> str:
if self.virtual:
return self.owner.ormar_config.model_fields[
self.owner.ormar_config.pkname
].get_alias()
return self.get_alias()
def get_related_field_alias(self) -> str:
"""
Extract names of the related database columns or that are connected
with given relation based to use as a target in filter clause.
:return: name or names of the related columns/ fields
:rtype: Union[str, dict[str, str]]
"""
if self.virtual:
field_name = self.get_related_name()
field = self.to.ormar_config.model_fields[field_name]
return field.get_alias()
target_field = self.to.get_column_alias(self.to.ormar_config.pkname)
return target_field
def get_related_field_name(self) -> Union[str, list[str]]:
"""
Returns name of the relation field that should be used in prefetch query.
This field is later used to register relation in prefetch query,
populate relations dict, and populate nested model in prefetch query.
:return: name(s) of the field
:rtype: Union[str, list[str]]
"""
if self.virtual:
return self.get_related_name()
return self.to.ormar_config.pkname
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, recursive_guard=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
:param globalns: global namespace
:type globalns: Any
:param localns: local namespace
:type localns: Any
:return: None
:rtype: None
"""
if self.to.__class__ == ForwardRef:
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,
ondelete=self.ondelete,
onupdate=self.onupdate,
foreign_key_name=self.foreign_key_name,
)
def _extract_model_from_sequence(
self, value: list, child: "Model", to_register: bool
) -> list["Model"]:
"""
Takes a list of Models and registers them on parent.
Registration is mutual, so children have also reference to parent.
Used in reverse FK relations.
:param value: list of Model
:type value: list
:param child: child/ related Model
:type child: Model
:param to_register: flag if the relation should be set in RelationshipManager
:type to_register: bool
:return: list (if needed) registered Models
:rtype: list["Model"]
"""
return [
self.expand_relationship( # type: ignore
value=val, child=child, to_register=to_register
)
for val in value
]
def _construct_model_from_dict(
self, value: dict, child: "Model", to_register: bool
) -> "Model":
"""
Takes a dictionary, creates a instance and registers it for parent.
If dictionary contains only one field and it's a pk it is a __pk_only__ model.
Registration is mutual, so children have also reference to parent.
Used in normal FK for dictionaries.
:param value: dictionary of a Model
:type value: dict
:param child: child/ related Model
:type child: Model
:param to_register: flag if the relation should be set in RelationshipManager
:type to_register: bool
:return: (if needed) registered Model
:rtype: Model
"""
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
):
pk_only_model = self.to_pk_only(**value)
model = cast(
"Model",
self.to._internal_construct(_pk_only=True, _excluded=None, **value),
)
else:
model = self.to(**value)
if to_register:
self.register_relation(model=model, child=child)
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
) -> "Model":
"""
Takes a pk value, creates a dummy instance and registers it for parent.
Registration is mutual, so children have also reference to parent.
Used in normal FK for dictionaries.
:param value: value of a related pk / fk column
:type value: Any
:param child: child/ related Model
:type child: Model
:param to_register: flag if the relation should be set in RelationshipManager
:type to_register: bool
:return: (if needed) registered Model
:rtype: Model
"""
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()):
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)
return model
def register_relation(self, model: "Model", child: "Model") -> None:
"""
Registers relation between parent and child in relation manager.
Relation manager is kep on each model (different instance).
Used in Metaclass and sometimes some relations are missing
(i.e. cloned Models in fastapi might miss one).
:param model: parent model (with relation definition)
:type model: Model class
:param child: child model
:type child: Model class
"""
model._orm.add(parent=model, child=child, field=self)
def has_unresolved_forward_refs(self) -> bool:
"""
Verifies if the filed has any ForwardRefs that require updating before the
model can be used.
:return: result of the check
:rtype: bool
"""
return self.to.__class__ == ForwardRef
@cached_property
def _constructor_dispatch(self) -> dict[str, Any]:
"""
Per-field map from input class name to the constructor handling that
shape. Built once at first access — by the time
``expand_relationship`` runs, ``_verify_model_can_be_initialized``
has already gated on ``requires_ref_update``, so the bound methods
captured here are stable.
:return: dispatch table for the slow path of ``expand_relationship``
:rtype: dict[str, Any]
"""
return {
"dict": self._construct_model_from_dict,
"list": self._extract_model_from_sequence,
}
def expand_relationship(
self,
value: Any,
child: Union["Model", "NewBaseModel"],
to_register: bool = True,
) -> Optional[Union["Model", list["Model"]]]:
"""
For relations the child model is first constructed (if needed),
registered in relation and returned.
For relation fields the value can be a pk value (Any type of field),
dict (from Model) or actual instance/list of a "Model".
Selects the appropriate constructor based on a passed value.
:param value: a Model field value, returned untouched for non relation fields.
:type value: Any
:param child: a child Model to register
:type child: Union["Model", "NewBaseModel"]
:param to_register: flag if the relation should be set in RelationshipManager
:type to_register: bool
:return: returns a Model or a list of Models
:rtype: Optional[Union["Model", list["Model"]]]
"""
if value is None:
return None if not self.virtual else []
# Fast path: ``value`` is already a Model of ``self.to``. Dominant
# case in row materialization and in user kwargs that pass
# constructed Models directly. Skips the dispatch table and the
# ``_register_existing_model`` indirection entirely.
if value.__class__ is self.to:
if to_register:
self.register_relation(model=value, child=cast("Model", child))
return value
return self._constructor_dispatch.get(
value.__class__.__name__, self._construct_model_from_pk
)(value, child, to_register)
def get_relation_name(self) -> str: # pragma: no cover
"""
Returns name of the relation, which can be a own name or through model
names for m2m models
:return: result of the check
:rtype: bool
"""
return self.name
def get_source_model(self) -> type["Model"]: # pragma: no cover
"""
Returns model from which the relation comes -> either owner or through model
:return: source model
:rtype: type["Model"]
"""
return self.owner
ormar-orm-ormar-a7e7fde/ormar/fields/many_to_many.py 0000664 0000000 0000000 00000030125 15211567312 0022765 0 ustar 00root root 0000000 0000000 from typing import (
TYPE_CHECKING,
Any,
ForwardRef,
Optional,
Union,
cast,
overload,
)
import ormar # noqa: I100
from ormar import ModelDefinitionError
from ormar.fields import BaseField
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
REF_PREFIX = "#/components/schemas/"
def forbid_through_relations(through: type["Model"]) -> None:
"""
Verifies if the through model does not have relations.
:param through: through Model to be checked
:type through: type['Model]
"""
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 "
f"{through.get_name(lower=False)}"
)
def populate_m2m_params_based_on_to_model(
to: type["Model"], nullable: bool
) -> 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.
:param to: target related ormar Model
:type to: Model class
:param nullable: marks field as optional/ required
:type nullable: bool
:return: tuple[list, Any]
:rtype: tuple with target pydantic type and target col type
"""
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__ = (
base_type # type: ignore
if not nullable
else Optional[base_type] # type: ignore
)
column_type = to_field.column_type
return __type__, column_type, pk_only_model
@overload
def ManyToMany(to: type["T"], **kwargs: Any) -> "RelationProxy[T]": # pragma: no cover
...
@overload
def ManyToMany(to: ForwardRef, **kwargs: Any) -> "RelationProxy": # pragma: no cover
...
def ManyToMany( # type: ignore
to: Union[type["T"], "ForwardRef"],
through: Optional[Union[type["T"], "ForwardRef"]] = None,
*,
name: Optional[str] = None,
unique: bool = False,
virtual: bool = False,
**kwargs: Any,
) -> "RelationProxy[T]":
"""
Despite a name it's a function that returns constructed ManyToManyField.
This function is actually used in model declaration
(as ormar.ManyToMany(ToModel, through=ThroughModel)).
Accepts number of relation setting parameters as well as all BaseField ones.
:param to: target related ormar Model
:type to: Model class
:param through: through model for m2m relation
:type through: Model class
:param name: name of the database field - later called alias
:type name: str
:param unique: parameter passed to sqlalchemy.ForeignKey, unique flag
:type unique: bool
:param virtual: marks if relation is virtual.
It is for reversed FK and auto generated FK on through model in Many2Many relations.
:type virtual: bool
:param kwargs: all other args to be populated by BaseField
:type kwargs: Any
:return: ormar ManyToManyField with m2m relation to selected model
:rtype: ManyToManyField
"""
related_name = kwargs.pop("related_name", None)
nullable = kwargs.pop("nullable", True)
owner = kwargs.pop("owner", None)
self_reference = kwargs.pop("self_reference", False)
orders_by = kwargs.pop("orders_by", None)
related_orders_by = kwargs.pop("related_orders_by", None)
skip_reverse = kwargs.pop("skip_reverse", False)
skip_field = kwargs.pop("skip_field", False)
through_relation_name = kwargs.pop("through_relation_name", None)
through_reverse_relation_name = kwargs.pop("through_reverse_relation_name", None)
through_foreign_key_name = kwargs.pop("through_foreign_key_name", None)
through_reverse_foreign_key_name = kwargs.pop(
"through_reverse_foreign_key_name", None
)
through_relation_nullable = kwargs.pop("through_relation_nullable", True)
through_reverse_relation_nullable = kwargs.pop(
"through_reverse_relation_nullable", True
)
if through is not None and through.__class__ != ForwardRef:
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
if not nullable
else Optional[Union[to, list[to]]] # type: ignore
)
column_type = None
else:
__type__, column_type, pk_only_model = populate_m2m_params_based_on_to_model(
to=to, # type: ignore
nullable=nullable,
)
namespace = dict(
__type__=__type__,
to=to,
to_pk_only=pk_only_model,
through=through,
alias=name,
name=name,
nullable=nullable,
unique=unique,
column_type=column_type,
related_name=related_name,
virtual=virtual,
primary_key=False,
index=False,
default=None,
server_default=None,
owner=owner,
self_reference=self_reference,
is_relation=True,
is_multi=True,
orders_by=orders_by,
related_orders_by=related_orders_by,
skip_reverse=skip_reverse,
skip_field=skip_field,
through_relation_name=through_relation_name,
through_reverse_relation_name=through_reverse_relation_name,
through_foreign_key_name=through_foreign_key_name,
through_reverse_foreign_key_name=through_reverse_foreign_key_name,
through_relation_nullable=through_relation_nullable,
through_reverse_relation_nullable=through_reverse_relation_nullable,
**kwargs,
)
Field = type("ManyToMany", (ManyToManyField, BaseField), {})
return Field(**namespace)
class ManyToManyField( # type: ignore
ForeignKeyField,
ormar.QuerySetProtocol,
ormar.RelationProtocol,
):
"""
Actual class returned from ManyToMany function call and stored in model_fields.
"""
def __init__(self, **kwargs: Any) -> None:
if TYPE_CHECKING: # pragma: no cover
self.__type__: type
self.to: type["Model"]
self.through: type["Model"]
super().__init__(**kwargs)
def get_source_related_name(self) -> str:
"""
Returns name to use for source relation name.
For FK it's the same, differs for m2m fields.
It's either set as `related_name` or by default it's field name.
:return: name of the related_name or default related name.
:rtype: str
"""
return (
self.through.ormar_config.model_fields[
self.default_source_field_name()
].related_name
or self.name
)
def has_unresolved_forward_refs(self) -> bool:
"""
Verifies if the filed has any ForwardRefs that require updating before the
model can be used.
:return: result of the check
:rtype: bool
"""
return self.to.__class__ == ForwardRef or self.through.__class__ == ForwardRef
def evaluate_forward_ref(self, globalns: Any, localns: Any) -> None:
"""
Evaluates the ForwardRef to actual Field based on global and local namespaces
:param globalns: global namespace
:type globalns: Any
:param localns: local namespace
:type localns: Any
:return: None
:rtype: None
"""
if self.to.__class__ == ForwardRef:
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._evaluate_forward_ref(globalns, localns, is_through=True)
forbid_through_relations(self.through)
def get_relation_name(self) -> str:
"""
Returns name of the relation, which can be a own name or through model
names for m2m models
:return: result of the check
:rtype: bool
"""
if self.self_reference and self.name == self.self_reference_primary:
return self.default_source_field_name()
return self.default_target_field_name()
def get_source_model(self) -> type["Model"]:
"""
Returns model from which the relation comes -> either owner or through model
:return: source model
:rtype: type["Model"]
"""
return self.through
def get_filter_clause_target(self) -> type["Model"]:
return self.through
def get_model_relation_fields(self, use_alias: bool = False) -> str:
"""
Extract names of the database columns or model fields that are connected
with given relation based on use_alias switch.
:param use_alias: use db names aliases or model fields
:type use_alias: bool
:return: name or names of the related columns/ fields
:rtype: Union[str, list[str]]
"""
pk_field = self.owner.ormar_config.model_fields[self.owner.ormar_config.pkname]
result = pk_field.get_alias() if use_alias else pk_field.name
return result
def get_related_field_alias(self) -> str:
"""
Extract names of the related database columns or that are connected
with given relation based to use as a target in filter clause.
:return: name or names of the related columns/ fields
:rtype: Union[str, dict[str, str]]
"""
if self.self_reference and self.self_reference_primary == self.name:
field_name = self.default_target_field_name()
else:
field_name = self.default_source_field_name()
sub_field = self.through.ormar_config.model_fields[field_name]
return sub_field.get_alias()
def get_related_field_name(self) -> Union[str, list[str]]:
"""
Returns name of the relation field that should be used in prefetch query.
This field is later used to register relation in prefetch query,
populate relations dict, and populate nested model in prefetch query.
:return: name(s) of the field
:rtype: Union[str, list[str]]
"""
if self.self_reference and self.self_reference_primary == self.name:
return self.default_target_field_name()
return self.default_source_field_name()
def create_default_through_model(self) -> None:
"""
Creates default empty through model if no additional fields are required.
"""
owner_name = self.owner.get_name(lower=False)
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"
base_namespace = {
"__module__": self.owner.__module__,
"__qualname__": f"{self.owner.__qualname__}.{class_name}",
}
# Default through tables live in the owner's schema (the side declaring
# the M2M). User-supplied through models can override by configuring
# their own OrmarConfig.schema.
new_config = ormar.models.ormar_config.OrmarConfig(
tablename=table_name,
database=self.owner.ormar_config.database,
metadata=self.owner.ormar_config.metadata,
schema=self.owner.ormar_config.schema,
)
through_model = type(
class_name,
(ormar.Model,),
{
**base_namespace,
"ormar_config": new_config,
"id": ormar.Integer(name="id", primary_key=True),
},
)
self.through = cast(type["Model"], through_model)
ormar-orm-ormar-a7e7fde/ormar/fields/model_fields.py 0000664 0000000 0000000 00000054440 15211567312 0022727 0 ustar 00root root 0000000 0000000 import datetime
import decimal
import uuid
from enum import Enum as E
from enum import EnumMeta
from typing import Any, Optional
import pydantic
import sqlalchemy
import ormar # noqa I101
from ormar import ModelDefinitionError # noqa I101
from ormar.fields import sqlalchemy_uuid
from ormar.fields.base import BaseField # noqa I101
from ormar.fields.sqlalchemy_encrypted import EncryptBackends
try:
from typing import Self # type: ignore
except ImportError: # pragma: no cover
from typing_extensions import Self # type: ignore
def is_field_nullable(
nullable: Optional[bool],
default: Any,
server_default: Any,
) -> bool:
"""
Checks if the given field should be nullable/ optional based on parameters given.
:param nullable: flag explicit setting a column as nullable
:type nullable: Optional[bool]
:param default: value or function to be called as default in python
:type default: Any
:param server_default: function to be called as default by sql server
:type server_default: Any
:return: result of the check
:rtype: bool
"""
if nullable is None:
return default is not None or server_default is not None
return nullable
def is_auto_primary_key(primary_key: bool, autoincrement: bool) -> bool:
"""
Checks if field is an autoincrement pk -> if yes it's optional.
:param primary_key: flag if field is a pk field
:type primary_key: bool
:param autoincrement: flag if field should be autoincrement
:type autoincrement: bool
:return: result of the check
:rtype: bool
"""
return primary_key and autoincrement
class ModelFieldFactory:
"""
Default field factory that construct Field classes and populated their values.
"""
_bases: Any = (BaseField,)
_type: Any = None
_sample: Any = None
def __new__(cls, *args: Any, **kwargs: Any) -> Self: # type: ignore
cls.validate(**kwargs)
default = kwargs.pop("default", None)
server_default = kwargs.pop("server_default", None)
nullable = kwargs.pop("nullable", None)
sql_nullable = kwargs.pop("sql_nullable", None)
primary_key = kwargs.pop("primary_key", False)
autoincrement = kwargs.pop("autoincrement", False)
encrypt_secret = kwargs.pop("encrypt_secret", None)
encrypt_backend = kwargs.pop("encrypt_backend", EncryptBackends.NONE)
encrypt_custom_backend = kwargs.pop("encrypt_custom_backend", None)
overwrite_pydantic_type = kwargs.pop("overwrite_pydantic_type", None)
nullable = is_field_nullable(
nullable, default, server_default
) or is_auto_primary_key(primary_key, autoincrement)
sql_nullable = (
False
if primary_key
else (nullable if sql_nullable is None else sql_nullable)
)
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
),
__sample__=cls._sample,
alias=kwargs.pop("name", None),
name=None,
primary_key=primary_key,
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),
autoincrement=autoincrement,
column_type=cls.get_column_type(
**kwargs, sql_nullable=sql_nullable, enum_class=enum_class
),
encrypt_secret=encrypt_secret,
encrypt_backend=encrypt_backend,
encrypt_custom_backend=encrypt_custom_backend,
**kwargs,
)
Field = type(cls.__name__, cls._bases, {})
return Field(**namespace)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any: # pragma no cover
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return None
@classmethod
def validate(cls, **kwargs: Any) -> None: # pragma no cover
"""
Used to validate if all required parameters on a given field type are set.
:param kwargs: all params passed during construction
:type kwargs: Any
"""
class String(ModelFieldFactory, str):
"""
String field factory that construct Field classes and populated their values.
"""
_type = str
_sample = "string"
def __new__( # type: ignore # noqa CFQ002
cls,
*,
max_length: int,
min_length: Optional[int] = None,
regex: Optional[str] = None,
**kwargs: Any,
) -> Self: # type: ignore
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.String(length=kwargs.get("max_length"))
@classmethod
def validate(cls, **kwargs: Any) -> None:
"""
Used to validate if all required parameters on a given field type are set.
:param kwargs: all params passed during construction
:type kwargs: Any
"""
max_length = kwargs.get("max_length", -1)
if max_length <= 0:
raise ModelDefinitionError(
"Parameter max_length is required for field String"
)
class Integer(ModelFieldFactory, int):
"""
Integer field factory that construct Field classes and populated their values.
"""
_type = int
_sample = 0
def __new__( # type: ignore
cls,
*,
minimum: Optional[int] = None,
maximum: Optional[int] = None,
multiple_of: Optional[int] = None,
**kwargs: Any,
) -> Self:
autoincrement = kwargs.pop("autoincrement", None)
autoincrement = (
autoincrement
if autoincrement is not None
else kwargs.get("primary_key", False)
)
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
kwargs["ge"] = kwargs["minimum"]
kwargs["le"] = kwargs["maximum"]
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.Integer()
class Text(ModelFieldFactory, str):
"""
Text field factory that construct Field classes and populated their values.
"""
_type = str
_sample = "text"
def __new__(cls, **kwargs: Any) -> Self: # type: ignore
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.Text()
class Float(ModelFieldFactory, float):
"""
Float field factory that construct Field classes and populated their values.
"""
_type = float
_sample = 0.0
def __new__( # type: ignore
cls,
*,
minimum: Optional[float] = None,
maximum: Optional[float] = None,
multiple_of: Optional[int] = None,
**kwargs: Any,
) -> Self:
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
kwargs["ge"] = kwargs["minimum"]
kwargs["le"] = kwargs["maximum"]
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.Float()
class Boolean(ModelFieldFactory, int):
"""
Boolean field factory that construct Field classes and populated their values.
"""
_type = bool
_sample = True
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.Boolean()
class DateTime(ModelFieldFactory, datetime.datetime):
"""
DateTime field factory that construct Field classes and populated their values.
"""
_type = datetime.datetime
_sample = "datetime"
def __new__( # type: ignore # noqa CFQ002
cls, *, timezone: bool = False, **kwargs: Any
) -> Self: # type: ignore
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.DateTime(timezone=kwargs.get("timezone", False))
class Date(ModelFieldFactory, datetime.date):
"""
Date field factory that construct Field classes and populated their values.
"""
_type = datetime.date
_sample = "date"
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.Date()
class Time(ModelFieldFactory, datetime.time):
"""
Time field factory that construct Field classes and populated their values.
"""
_type = datetime.time
_sample = "time"
def __new__( # type: ignore # noqa CFQ002
cls, *, timezone: bool = False, **kwargs: Any
) -> Self: # type: ignore
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.Time(timezone=kwargs.get("timezone", False))
class JSON(ModelFieldFactory, pydantic.Json):
"""
JSON field factory that construct Field classes and populated their values.
"""
_type = pydantic.Json
_sample = '{"json": "json"}'
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.JSON(none_as_null=kwargs.get("sql_nullable", False))
class LargeBinary(ModelFieldFactory, bytes):
"""
LargeBinary field factory that construct Field classes
and populated their values.
"""
_type = bytes
_sample = "bytes"
def __new__( # type: ignore # noqa CFQ002
cls, *, max_length: int, represent_as_base64_str: bool = False, **kwargs: Any
) -> Self: # type: ignore
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.LargeBinary(length=kwargs.get("max_length"))
@classmethod
def validate(cls, **kwargs: Any) -> None:
"""
Used to validate if all required parameters on a given field type are set.
:param kwargs: all params passed during construction
:type kwargs: Any
"""
max_length = kwargs.get("max_length", None)
if max_length <= 0:
raise ModelDefinitionError(
"Parameter max_length is required for field LargeBinary"
)
class BigInteger(Integer, int):
"""
BigInteger field factory that construct Field classes and populated their values.
"""
_type = int
_sample = 0
def __new__( # type: ignore
cls,
*,
minimum: Optional[int] = None,
maximum: Optional[int] = None,
multiple_of: Optional[int] = None,
**kwargs: Any,
) -> Self:
autoincrement = kwargs.pop("autoincrement", None)
autoincrement = (
autoincrement
if autoincrement is not None
else kwargs.get("primary_key", False)
)
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
kwargs["ge"] = kwargs["minimum"]
kwargs["le"] = kwargs["maximum"]
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.BigInteger()
class SmallInteger(Integer, int):
"""
SmallInteger field factory that construct Field classes and populated their values.
"""
_type = int
_sample = 0
def __new__( # type: ignore
cls,
*,
minimum: Optional[int] = None,
maximum: Optional[int] = None,
multiple_of: Optional[int] = None,
**kwargs: Any,
) -> Self:
autoincrement = kwargs.pop("autoincrement", None)
autoincrement = (
autoincrement
if autoincrement is not None
else kwargs.get("primary_key", False)
)
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
kwargs["ge"] = kwargs["minimum"]
kwargs["le"] = kwargs["maximum"]
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
return sqlalchemy.SmallInteger()
class Decimal(ModelFieldFactory, decimal.Decimal):
"""
Decimal field factory that construct Field classes and populated their values.
"""
_type = decimal.Decimal
_sample = 0.0
def __new__( # type: ignore # noqa CFQ002
cls,
*,
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,
) -> Self:
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
kwargs["ge"] = kwargs["minimum"]
kwargs["le"] = kwargs["maximum"]
if kwargs.get("max_digits"):
kwargs["precision"] = kwargs["max_digits"]
elif kwargs.get("precision"):
kwargs["max_digits"] = kwargs["precision"]
if kwargs.get("decimal_places"):
kwargs["scale"] = kwargs["decimal_places"]
elif kwargs.get("scale"):
kwargs["decimal_places"] = kwargs["scale"]
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
precision = kwargs.get("precision")
scale = kwargs.get("scale")
return sqlalchemy.DECIMAL(precision=precision, scale=scale)
@classmethod
def validate(cls, **kwargs: Any) -> None:
"""
Used to validate if all required parameters on a given field type are set.
:param kwargs: all params passed during construction
:type kwargs: Any
"""
precision = kwargs.get("precision")
scale = kwargs.get("scale")
if precision is None or precision < 0 or scale is None or scale < 0:
raise ModelDefinitionError(
"Parameters scale and precision are required for field Decimal"
)
class UUID(ModelFieldFactory, uuid.UUID):
"""
UUID field factory that construct Field classes and populated their values.
"""
_type = uuid.UUID
_sample = "uuid"
def __new__( # type: ignore # noqa CFQ002
cls, *, uuid_format: str = "hex", **kwargs: Any
) -> Self:
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
return super().__new__(cls, **kwargs)
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
"""
Return proper type of db column for given field type.
Accepts required and optional parameters that each column type accepts.
:param kwargs: key, value pairs of sqlalchemy options
:type kwargs: Any
:return: initialized column with proper options
:rtype: sqlalchemy Column
"""
uuid_format = kwargs.get("uuid_format", "hex")
return sqlalchemy_uuid.UUID(uuid_format=uuid_format)
class Enum(ModelFieldFactory):
"""
Enum field factory that construct Field classes and populated their values.
"""
_type = E
_sample = None
def __new__( # type: ignore # noqa CFQ002
cls, *, enum_class: type[E], **kwargs: Any
) -> Self:
kwargs = {
**kwargs,
**{
k: v
for k, v in locals().items()
if k not in ["cls", "__class__", "kwargs"]
},
}
return super().__new__(cls, **kwargs)
@classmethod
def validate(cls, **kwargs: Any) -> None:
enum_class = kwargs.get("enum_class")
if enum_class is None or not isinstance(enum_class, EnumMeta):
raise ModelDefinitionError("Enum Field choices must be EnumType")
@classmethod
def get_column_type(cls, **kwargs: Any) -> Any:
enum_cls = kwargs.get("enum_class")
return sqlalchemy.Enum(enum_cls)
ormar-orm-ormar-a7e7fde/ormar/fields/model_fields.pyi 0000664 0000000 0000000 00000006516 15211567312 0023101 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from datetime import date, datetime, time
from decimal import Decimal as DecimalType
from enum import Enum as EnumBase
from typing import Any, Literal, TypeVar, Union, overload
from uuid import UUID as UuidType
T = TypeVar("T", bound=EnumBase)
@overload
def Boolean(*, nullable: Literal[False] = False, **kwargs: Any) -> bool: ...
@overload
def Boolean(*, nullable: Literal[True], **kwargs: Any) -> bool | None: ...
@overload
def String(
*, max_length: int, nullable: Literal[False] = False, **kwargs: Any
) -> str: ...
@overload
def String(
*, max_length: int, nullable: Literal[True], **kwargs: Any
) -> str | None: ...
@overload
def Integer(*, nullable: Literal[False] = False, **kwargs: Any) -> int: ...
@overload
def Integer(*, nullable: Literal[True], **kwargs: Any) -> int | None: ...
@overload
def Text(*, nullable: Literal[False] = False, **kwargs: Any) -> str: ...
@overload
def Text(*, nullable: Literal[True], **kwargs: Any) -> str | None: ...
@overload
def Float(*, nullable: Literal[False] = False, **kwargs: Any) -> float: ...
@overload
def Float(*, nullable: Literal[True], **kwargs: Any) -> float | None: ...
@overload
def DateTime(*, nullable: Literal[False] = False, **kwargs: Any) -> datetime: ...
@overload
def DateTime(*, nullable: Literal[True], **kwargs: Any) -> datetime | None: ...
@overload
def Date(*, nullable: Literal[False] = False, **kwargs: Any) -> date: ...
@overload
def Date(*, nullable: Literal[True], **kwargs: Any) -> date | None: ...
@overload
def Time(*, nullable: Literal[False] = False, **kwargs: Any) -> time: ...
@overload
def Time(*, nullable: Literal[True], **kwargs: Any) -> time | None: ...
@overload
def JSON(*, nullable: Literal[False] = False, **kwargs: Any) -> Any: ...
@overload
def JSON(*, nullable: Literal[True], **kwargs: Any) -> Any | None: ...
@overload
def BigInteger(*, nullable: Literal[False] = False, **kwargs: Any) -> int: ...
@overload
def BigInteger(*, nullable: Literal[True], **kwargs: Any) -> int | None: ...
@overload
def SmallInteger(*, nullable: Literal[False] = False, **kwargs: Any) -> int: ...
@overload
def SmallInteger(*, nullable: Literal[True], **kwargs: Any) -> int | None: ...
@overload
def Decimal(
*,
max_digits: int,
decimal_places: int,
nullable: Literal[False] = False,
**kwargs: Any,
) -> DecimalType: ...
@overload
def Decimal(
*, max_digits: int, decimal_places: int, nullable: Literal[True], **kwargs: Any
) -> DecimalType | None: ...
@overload
def UUID(*, nullable: Literal[False] = False, **kwargs: Any) -> UuidType: ...
@overload
def UUID(*, nullable: Literal[True], **kwargs: Any) -> UuidType | None: ...
@overload
def LargeBinary(
max_length: int, *, represent_as_base64_str: Literal[True], **kwargs: Any
) -> str: ...
@overload
def LargeBinary(
max_length: int, *, represent_as_base64_str: Literal[False], **kwargs: Any
) -> bytes: ...
@overload
def LargeBinary(
max_length: int, represent_as_base64_str: Literal[False] = ..., **kwargs: Any
) -> bytes: ...
@overload
def LargeBinary(
max_length: int, represent_as_base64_str: bool = False, **kwargs: Any
) -> Union[str, bytes]: ...
@overload
def Enum(
enum_class: type[T],
*,
nullable: Literal[False] = False,
**kwargs: Any,
) -> T: ...
@overload
def Enum(
enum_class: type[T],
*,
nullable: Literal[True],
**kwargs: Any,
) -> T | None: ...
ormar-orm-ormar-a7e7fde/ormar/fields/parsers.py 0000664 0000000 0000000 00000003166 15211567312 0021757 0 ustar 00root root 0000000 0000000 import datetime
import decimal
import uuid
from typing import Callable, Optional
import orjson as json
import ormar_rust_utils
import pydantic
from pydantic_core import SchemaValidator, core_schema
def parse_bool(value: str) -> bool:
return value == "true"
def encode_bool(value: bool) -> str:
return "true" if value else "false"
def encode_decimal(value: decimal.Decimal, precision: Optional[int] = None) -> float:
return (
round(float(value), precision) if isinstance(value, decimal.Decimal) else value
)
encode_bytes = ormar_rust_utils.encode_bytes
decode_bytes = ormar_rust_utils.decode_bytes
encode_json = ormar_rust_utils.encode_json
ENCODERS_MAP: dict[type, Callable] = {
datetime.datetime: lambda x: x.isoformat(),
datetime.date: lambda x: x.isoformat(),
datetime.time: lambda x: x.isoformat(),
pydantic.Json: encode_json,
decimal.Decimal: encode_decimal,
uuid.UUID: str,
bytes: encode_bytes,
}
SQL_ENCODERS_MAP: dict[type, Callable] = {bool: encode_bool, **ENCODERS_MAP}
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,
}
ormar-orm-ormar-a7e7fde/ormar/fields/referential_actions.py 0000664 0000000 0000000 00000001203 15211567312 0024306 0 ustar 00root root 0000000 0000000 """
Gathers all referential actions by ormar.
"""
from enum import Enum
class ReferentialAction(Enum):
"""
Because the database management system(DBMS) enforces referential constraints,
it must ensure data integrity
if rows in a referenced table are to be deleted (or updated).
If dependent rows in referencing tables still exist,
those references have to be considered.
SQL specifies 5 different referential actions
that shall take place in such occurrences.
"""
CASCADE = "CASCADE"
RESTRICT = "RESTRICT"
SET_NULL = "SET NULL"
SET_DEFAULT = "SET DEFAULT"
DO_NOTHING = "NO ACTION"
ormar-orm-ormar-a7e7fde/ormar/fields/sqlalchemy_encrypted.py 0000664 0000000 0000000 00000015517 15211567312 0024522 0 ustar 00root root 0000000 0000000 # inspired by sqlalchemy-utils (https://github.com/kvesteri/sqlalchemy-utils)
import abc
import base64
from enum import Enum
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
import sqlalchemy.types as types
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
import cryptography # type: ignore
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
except ImportError: # pragma: nocover
pass
if TYPE_CHECKING: # pragma: nocover
from ormar import BaseField
class EncryptBackend(abc.ABC):
def _refresh(self, key: Union[str, bytes]) -> None:
if isinstance(key, str):
key = key.encode()
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(key)
engine_key = digest.finalize()
self._initialize_backend(engine_key)
@abc.abstractmethod
def _initialize_backend(self, secret_key: bytes) -> None: # pragma: nocover
pass
@abc.abstractmethod
def encrypt(self, value: Any) -> str: # pragma: nocover
pass
@abc.abstractmethod
def decrypt(self, value: Any) -> str: # pragma: nocover
pass
class HashBackend(EncryptBackend):
"""
One-way hashing - in example for passwords, no way to decrypt the value!
"""
def _initialize_backend(self, secret_key: bytes) -> None:
self.secret_key = base64.urlsafe_b64encode(secret_key)
def encrypt(self, value: Any) -> str:
if not isinstance(value, str): # pragma: nocover
value = repr(value)
value = value.encode()
digest = hashes.Hash(hashes.SHA512(), backend=default_backend())
digest.update(self.secret_key)
digest.update(value)
hashed_value = digest.finalize()
return hashed_value.hex()
def decrypt(self, value: Any) -> str:
if not isinstance(value, str): # pragma: nocover
value = str(value)
return value
class FernetBackend(EncryptBackend):
"""
Two-way encryption, data stored in db are encrypted but decrypted during query.
"""
def _initialize_backend(self, secret_key: bytes) -> None:
self.secret_key = base64.urlsafe_b64encode(secret_key)
self.fernet = Fernet(self.secret_key)
def encrypt(self, value: Any) -> str:
if not isinstance(value, str):
value = repr(value)
value = value.encode()
encrypted = self.fernet.encrypt(value)
return encrypted.decode("utf-8")
def decrypt(self, value: Any) -> str:
if not isinstance(value, str): # pragma: nocover
value = str(value)
decrypted: Union[str, bytes] = self.fernet.decrypt(value.encode())
if not isinstance(decrypted, str):
decrypted = decrypted.decode("utf-8")
return decrypted
class EncryptBackends(Enum):
NONE = 0
FERNET = 1
HASH = 2
CUSTOM = 3
BACKENDS_MAP = {
EncryptBackends.FERNET: FernetBackend,
EncryptBackends.HASH: HashBackend,
}
class EncryptedString(types.TypeDecorator):
"""
Used to store encrypted values in a database
"""
impl = types.TypeEngine
cache_ok = True
def __init__(
self,
encrypt_secret: Union[str, Callable],
encrypt_backend: EncryptBackends = EncryptBackends.FERNET,
encrypt_custom_backend: Optional[type[EncryptBackend]] = None,
**kwargs: Any,
) -> None:
_field_type = kwargs.pop("_field_type")
super().__init__()
if not cryptography: # pragma: nocover
raise ModelDefinitionError(
"In order to encrypt a column 'cryptography' is required!"
)
backend = BACKENDS_MAP.get(encrypt_backend, encrypt_custom_backend)
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()
self._field_type: "BaseField" = _field_type
self._underlying_type: Any = _field_type.column_type
self._key: Union[str, Callable] = encrypt_secret
type_ = self._field_type.__type__
if type_ is None: # pragma: nocover
raise ModelDefinitionError(
f"Improperly configured field {self._field_type.name}"
)
self.type_: Any = type_
def __repr__(self) -> str: # pragma: nocover
return "TEXT()"
def load_dialect_impl(self, dialect: Dialect) -> Any:
return dialect.type_descriptor(types.TEXT())
def _refresh(self) -> None:
key = self._key() if callable(self._key) else self._key
self.backend._refresh(key)
def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]:
if value is None:
return value
self._refresh()
try:
value = self._underlying_type.process_bind_param(value, dialect)
except AttributeError:
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
def process_result_value(self, value: Any, dialect: Dialect) -> Any:
if value is None: # pragma: no cover
return value
self._refresh()
decrypted_value = self.backend.decrypt(value)
try:
return self._underlying_type.process_result_value(decrypted_value, dialect)
except AttributeError:
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
ormar-orm-ormar-a7e7fde/ormar/fields/sqlalchemy_uuid.py 0000664 0000000 0000000 00000002725 15211567312 0023470 0 ustar 00root root 0000000 0000000 import uuid
from typing import Any, Optional
from sqlalchemy import CHAR
from sqlalchemy.engine import Dialect
from sqlalchemy.types import TypeDecorator
class UUID(TypeDecorator):
"""
Platform-independent GUID type.
Uses CHAR(36) if in a string mode, otherwise uses CHAR(32), to store UUID.
For details for different methods check documentation of parent class.
"""
impl = CHAR
cache_ok = True
def __init__(self, *args: Any, uuid_format: str = "hex", **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.uuid_format = uuid_format
def __repr__(self) -> str: # pragma: nocover
if self.uuid_format == "string":
return "CHAR(36)"
return "CHAR(32)"
def load_dialect_impl(self, dialect: Dialect) -> Any:
return (
dialect.type_descriptor(CHAR(36))
if self.uuid_format == "string"
else dialect.type_descriptor(CHAR(32))
)
def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]:
if value is None:
return value
return str(value) if self.uuid_format == "string" else "%.32x" % value.int
def process_result_value(
self, value: Optional[str], dialect: Dialect
) -> Optional[uuid.UUID]:
if value is None: # pragma: no cover
return value
if not isinstance(value, uuid.UUID):
return uuid.UUID(value)
return value # pragma: nocover
ormar-orm-ormar-a7e7fde/ormar/fields/through_field.py 0000664 0000000 0000000 00000004037 15211567312 0023121 0 ustar 00root root 0000000 0000000 import sys
from typing import TYPE_CHECKING, Any, Optional, Union
from ormar.fields.base import BaseField
from ormar.fields.foreign_key import ForeignKeyField
if TYPE_CHECKING: # pragma no cover
from pydantic.typing import ForwardRef
from ormar import Model
if sys.version_info < (3, 7):
ToType = type[Model]
else:
ToType = Union[type[Model], ForwardRef]
def Through( # noqa CFQ002
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.
It's a special field populated only for m2m relations.
Accepts number of relation setting parameters as well as all BaseField ones.
:param to: target related ormar Model
:type to: Model class
:param name: name of the database field - later called alias
:type name: str
:param related_name: name of reversed FK relation populated for you on to model
:type related_name: str
It is for reversed FK and auto generated FK on through model in Many2Many relations.
:param kwargs: all other args to be populated by BaseField
:type kwargs: Any
:return: ormar ForeignKeyField with relation to selected model
:rtype: ForeignKeyField
"""
nullable = kwargs.pop("nullable", False)
owner = kwargs.pop("owner", None)
namespace = dict(
__type__=to,
to=to,
through=None,
alias=name,
name=kwargs.pop("real_name", None),
related_name=related_name,
virtual=True,
owner=owner,
nullable=nullable,
unique=False,
column_type=None,
primary_key=False,
index=False,
default=None,
server_default=None,
is_relation=True,
is_through=True,
)
Field = type("Through", (ThroughField, BaseField), {})
return Field(**namespace)
class ThroughField(ForeignKeyField): # type: ignore[misc]
"""
Field class used to access ManyToMany model through model.
"""
ormar-orm-ormar-a7e7fde/ormar/models/ 0000775 0000000 0000000 00000000000 15211567312 0017735 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/models/__init__.py 0000664 0000000 0000000 00000001333 15211567312 0022046 0 ustar 00root root 0000000 0000000 """
Definition of Model, it's parents NewBaseModel and mixins used by models.
Also defines a Metaclass that handles all constructions and relations registration,
ass well as vast number of helper functions for pydantic, sqlalchemy and relations.
"""
from ormar.models.newbasemodel import NewBaseModel # noqa I100
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",
"OrmarConfig",
]
ormar-orm-ormar-a7e7fde/ormar/models/descriptors/ 0000775 0000000 0000000 00000000000 15211567312 0022276 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/models/descriptors/__init__.py 0000664 0000000 0000000 00000000444 15211567312 0024411 0 ustar 00root root 0000000 0000000 from ormar.models.descriptors.descriptors import (
BytesDescriptor,
JsonDescriptor,
PkDescriptor,
PydanticDescriptor,
RelationDescriptor,
)
__all__ = [
"PydanticDescriptor",
"RelationDescriptor",
"PkDescriptor",
"JsonDescriptor",
"BytesDescriptor",
]
ormar-orm-ormar-a7e7fde/ormar/models/descriptors/descriptors.py 0000664 0000000 0000000 00000007571 15211567312 0025223 0 ustar 00root root 0000000 0000000 import base64
from typing import TYPE_CHECKING, Any, Optional, cast
from ormar.fields.parsers import decode_bytes, encode_json
if TYPE_CHECKING: # pragma: no cover
from ormar import Model
class PydanticDescriptor:
"""
Pydantic descriptor simply delegates everything to pydantic model
"""
def __init__(self, name: str) -> None:
self.name = name
def __get__(self, instance: "Model", owner: type["Model"]) -> Any:
value = instance.__dict__.get(self.name, None)
return value
def __set__(self, instance: "Model", value: Any) -> None:
instance._internal_set(self.name, value)
instance.set_save_status(False)
class JsonDescriptor:
"""
Json descriptor dumps/loads strings to actual data on write/read
"""
def __init__(self, name: str) -> None:
self.name = name
def __get__(self, instance: "Model", owner: type["Model"]) -> Any:
value = instance.__dict__.get(self.name, None)
return value
def __set__(self, instance: "Model", value: Any) -> None:
value = encode_json(value)
instance._internal_set(self.name, value)
instance.set_save_status(False)
class BytesDescriptor:
"""
Bytes descriptor converts strings to bytes on write and converts bytes to str
if represent_as_base64_str flag is set, so the value can be dumped to json
"""
def __init__(self, name: str) -> None:
self.name = name
def __get__(self, instance: "Model", owner: type["Model"]) -> Any:
value = instance.__dict__.get(self.name, None)
field = instance.ormar_config.model_fields[self.name]
if (
value is not None
and field.represent_as_base64_str
and not isinstance(value, str)
):
value = base64.b64encode(value).decode()
return value
def __set__(self, instance: "Model", value: Any) -> None:
field = instance.ormar_config.model_fields[self.name]
if isinstance(value, str):
value = decode_bytes(
value=value, represent_as_string=field.represent_as_base64_str
)
instance._internal_set(self.name, value)
instance.set_save_status(False)
class PkDescriptor:
"""
As of now it's basically a copy of PydanticDescriptor but that will
change in the future with multi column primary keys
"""
def __init__(self, name: str) -> None:
self.name = name
def __get__(self, instance: "Model", owner: type["Model"]) -> Any:
value = instance.__dict__.get(self.name, None)
return value
def __set__(self, instance: "Model", value: Any) -> None:
instance._internal_set(self.name, value)
instance.set_save_status(False)
class RelationDescriptor:
"""
Relation descriptor expands the relation to initialize the related model
before setting it to __dict__. Note that expanding also registers the
related model in RelationManager.
"""
def __init__(self, name: str) -> None:
self.name = name
def __get__(self, instance: "Model", owner: type["Model"]) -> Any:
if self.name in instance._orm:
return instance._orm.get(self.name) # type: ignore
return None # pragma no cover
def __set__(self, instance: "Model", value: Any) -> None:
field = instance.ormar_config.model_fields[self.name]
field.expand_relationship(value=value, child=instance)
if (
value is None
and field.nullable
and not field.virtual
and not field.is_multi
and self.name in instance._orm
):
previous = cast(Optional["Model"], instance._orm.get(self.name))
if previous is not None:
instance._orm.remove(self.name, previous)
if not isinstance(instance.__dict__.get(self.name), list):
instance.set_save_status(False)
ormar-orm-ormar-a7e7fde/ormar/models/excludable.py 0000664 0000000 0000000 00000077066 15211567312 0022437 0 ustar 00root root 0000000 0000000 from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
from ormar.exceptions import QueryDefinitionError
from ormar.queryset.utils import (
PathParts,
build_flatten_map,
get_relationship_alias_model_and_str,
translate_list_to_dict,
)
if TYPE_CHECKING: # pragma: no cover
from ormar import Model
Slot = Literal["include", "exclude", "flatten"]
def skip_ellipsis(
items: Union[set, dict, None],
key: str,
default: Any = None,
) -> Union[set, dict, None]:
"""
Descend one level into an include/exclude dict by ``key``, returning
``default`` when the lookup yields ``Ellipsis``. Ellipsis at this position
means "all fields below" rather than a concrete set/dict to recurse into.
:param items: current include/exclude value
:type items: Union[set, dict, None]
:param key: key for nested relations to check
:type key: str
:param default: value returned when the lookup yields Ellipsis
:type default: Any
:return: nested value of the items
:rtype: Union[set, dict, None]
"""
result = items.get(key, {}) if isinstance(items, dict) else items
return result if result is not Ellipsis else default
def convert_all(items: Union[set, dict, None]) -> Union[set, dict, None]:
"""
Convert pydantic ``__all__`` special index into the ormar form, which does
not support index-based exclusions.
:param items: current include/exclude value
:type items: Union[set, dict, None]
:return: items with ``__all__`` unwrapped if present
:rtype: Union[set, dict, None]
"""
if isinstance(items, dict) and "__all__" in items:
return items.get("__all__")
return items
def normalize_to_dict(items: Union[set, dict, None]) -> Optional[dict]:
"""
Convert a set form of include/exclude into its dict equivalent, leaving
dicts and ``None`` unchanged.
:param items: include/exclude value in any user-accepted form
:type items: Union[set, dict, None]
:return: dict form of items, or ``None``
:rtype: Optional[dict]
"""
return translate_list_to_dict(items) if isinstance(items, set) else items
def filter_not_excluded_fields(
fields: Union[list, set],
include: Optional[dict],
exclude: Optional[dict],
) -> list:
"""
Apply include / exclude dicts to a flat field list, returning the field
names that survive filtering. Mirrors the rules used during ``model_dump``
serialization.
:param fields: candidate field names
:type fields: Union[list, set]
:param include: fields to include
:type include: Optional[dict]
:param exclude: fields to exclude
:type exclude: Optional[dict]
:return: list of field names that pass the include/exclude filter
:rtype: list
"""
fields = [*fields] if not isinstance(fields, list) else fields
if include:
fields = [field for field in fields if field in include]
if exclude:
fields = [
field
for field in fields
if field not in exclude
or (
exclude.get(field) is not Ellipsis and exclude.get(field) != {"__all__"}
)
]
return fields
class FlattenMap:
"""
Nested-dict view of flatten directives with Ellipsis at leaves. Wraps the
``Optional[dict]`` shape produced by :class:`ExcludableItems` so the few
operations that consume it (leaf check, descent, validation against
include/exclude) live alongside the data.
A truthy instance means at least one relation will be rendered as its
primary key.
"""
__slots__ = ("_data", "_flatten_all")
def __init__(self, data: Optional[dict] = None, flatten_all: bool = False) -> None:
self._data = data
self._flatten_all = flatten_all
@property
def data(self) -> Optional[dict]:
"""
Underlying nested-Ellipsis dict, or ``None`` when only ``flatten_all``
drives behavior. Exposed primarily for inspection and tests.
:return: stored nested-Ellipsis dict, or None
:rtype: Optional[dict]
"""
return self._data
def is_field_flattened(self, field: str) -> bool:
"""
Decide if a relation field should be rendered as its primary key.
``flatten_all`` wins globally; otherwise an Ellipsis leaf at ``field``
triggers flattening.
:param field: name of the relation field
:type field: str
:return: True if the field should be rendered as a pk value
:rtype: bool
"""
if self._flatten_all:
return True
return self._data is not None and self._data.get(field) is Ellipsis
def descend(self, field: str) -> Optional["FlattenMap"]:
"""
Return the flatten sub-map for the nested model reached via ``field``,
or ``None`` when there is no sub-map. Callers must check
``is_field_flattened`` first — ``flatten_all`` instances short-circuit
there before reaching ``descend``.
:param field: relation name to descend into
:type field: str
:return: nested FlattenMap or None
:rtype: Optional[FlattenMap]
"""
value = self._data.get(field) if self._data else None
if isinstance(value, dict):
return FlattenMap(data=value)
return None
def check_vs_selector(
self,
selector: Union[set, dict, None],
selector_kind: str,
path: str = "",
) -> None:
"""
Walk the flatten map alongside an include/exclude dict and raise when
a flattened leaf has sub-field selection on its target.
``flatten_all`` alone never produces a conflict because there is no
explicit per-relation map to compare against.
:param selector: current level of include / exclude nested dict
:type selector: Union[set, dict, None]
:param selector_kind: "include" or "exclude" - used in error message
:type selector_kind: str
:param path: dunder path accumulator for the error message
:type path: str
"""
if not self._data:
return
walk_flatten_vs_selector(self._data, selector, selector_kind, path)
def __bool__(self) -> bool:
return self._flatten_all or bool(self._data)
def walk_flatten_vs_selector(
flatten_map: dict,
selector: Union[set, dict, None],
selector_kind: str,
path: str,
) -> None:
"""
Recursive worker behind :meth:`FlattenMap.check_vs_selector` — kept at
module level so the recursion stays on a plain dict and does not allocate
intermediate :class:`FlattenMap` instances.
:param flatten_map: current level of the flatten nested dict
:type flatten_map: dict
:param selector: current level of include / exclude nested dict
:type selector: Union[set, dict, None]
:param selector_kind: "include" or "exclude" - used in error message
:type selector_kind: str
:param path: dunder path accumulator for the error message
:type path: str
"""
if not selector or not isinstance(selector, dict):
return
for key, value in flatten_map.items():
sel = selector.get(key)
full_path = f"{path}__{key}" if path else key
if value is Ellipsis:
conflicts: Optional[set] = None
if isinstance(sel, dict):
conflicts = set(sel.keys()) - {...}
elif isinstance(sel, set):
conflicts = {item for item in sel if item is not ...}
if conflicts:
raise QueryDefinitionError(
f"Flatten conflict: relation '{full_path}' is flattened "
f"but {selector_kind} specifies children "
f"{sorted(conflicts)}. A flattened relation renders only "
f"its primary key and cannot have children selected."
)
elif isinstance(value, dict):
walk_flatten_vs_selector(
flatten_map=value,
selector=sel,
selector_kind=selector_kind,
path=full_path,
)
@dataclass
class Excludable:
"""
Class that keeps sets of fields to include, exclude, and flatten for a single
model at a given alias/relation level.
``pk_only`` is a short-circuit consulted by :meth:`own_table_columns`: when
set, only the primary-key column is projected for this model and the
``include``/``exclude`` sets are ignored. Producers should not combine
``pk_only=True`` with a non-empty ``include``/``exclude`` - the flag wins.
"""
include: set = field(default_factory=set)
exclude: set = field(default_factory=set)
flatten: set = field(default_factory=set)
pk_only: bool = False
def get_copy(self) -> "Excludable":
"""
Return copy of self to avoid in place modifications.
:return: copy of self with copied sets
:rtype: ormar.models.excludable.Excludable
"""
_copy = self.__class__()
_copy.include = {x for x in self.include}
_copy.exclude = {x for x in self.exclude}
_copy.flatten = {x for x in self.flatten}
_copy.pk_only = self.pk_only
return _copy
def set_values(self, value: set, slot: Slot) -> None:
"""
Appends the data to the chosen slot (include/exclude/flatten).
:param value: set of values to add
:type value: set
:param slot: which set to add the values to
:type slot: Slot
"""
current_value = getattr(self, slot)
current_value.update(value)
setattr(self, slot, current_value)
def is_included(self, key: str) -> bool:
"""
Check if field in included (in set or set is {...}).
:param key: key to check
:type key: str
:return: result of the check
:rtype: bool
"""
return (... in self.include or key in self.include) if self.include else True
def is_explicitly_included(self, key: str) -> bool:
"""
Check whether ``key`` is explicitly named in the include set.
Unlike :meth:`is_included`, this returns ``False`` for an empty
include set rather than ``True`` - callers asking "did the user
name this?" want a no when nothing was specified at all.
:param key: key to check
:type key: str
:return: True if include is non-empty and contains key (or ``...``)
:rtype: bool
"""
return bool(self.include) and self.is_included(key)
def is_excluded(self, key: str) -> bool:
"""
Check if field in excluded (in set or set is {...}).
:param key: key to check
:type key: str
:return: result of the check
:rtype: bool
"""
return (... in self.exclude or key in self.exclude) if self.exclude else False
def is_flattened(self, key: str) -> bool:
"""
Check if relation is flattened (in set or set is {...}).
:param key: relation name to check
:type key: str
:return: result of the check
:rtype: bool
"""
return (... in self.flatten or key in self.flatten) if self.flatten else False
class ExcludableItems:
"""
Keeps a dictionary of Excludables by alias + model_name keys
to allow quick lookup by nested models without need to travers
deeply nested dictionaries and passing include/exclude around.
"""
def __init__(self) -> None:
self.items: dict[str, Excludable] = dict()
self._flatten_paths: set[PathParts] = set()
self._flatten_map_cache: Optional[FlattenMap] = None
@classmethod
def from_excludable(cls, other: "ExcludableItems") -> "ExcludableItems":
"""
Copy passed ExcludableItems to avoid inplace modifications.
:param other: other excludable items to be copied
:type other: ormar.models.excludable.ExcludableItems
:return: copy of other
:rtype: ormar.models.excludable.ExcludableItems
"""
new_excludable = cls()
for key, value in other.items.items():
new_excludable.items[key] = value.get_copy()
new_excludable._flatten_paths = set(other._flatten_paths)
return new_excludable
def include_entry_count(self) -> int:
"""
Returns count of include items inside.
"""
count = 0
for key in self.items.keys():
count += len(self.items[key].include)
return count
def flatten_map(self) -> Optional[FlattenMap]:
"""
Return a :class:`FlattenMap` over the stored flatten paths, built
lazily on first call and cached for reuse. The cache is invalidated
whenever ``_set_slot`` adds a new flatten path. Returns ``None`` when
no flatten paths are stored, so callers can short-circuit.
:return: FlattenMap wrapping the nested-Ellipsis dict, or ``None``
when no flatten paths are stored
:rtype: Optional[FlattenMap]
"""
if not self._flatten_paths:
return None
if self._flatten_map_cache is None:
self._flatten_map_cache = FlattenMap(
data=build_flatten_map(self._flatten_paths)
)
return self._flatten_map_cache
@staticmethod
def _make_key(model_cls: type["Model"], alias: str = "") -> str:
"""
Build the items-dict key for ``(alias, model)``. Centralized so
every call site shares one definition of "which Excludable is
this".
"""
prefix = f"{alias}_" if alias else ""
return f"{prefix}{model_cls.get_name(lower=True)}"
@staticmethod
def _resolve_path(
source_model: type["Model"],
parts_prefix: tuple,
) -> tuple[str, type["Model"]]:
"""
Resolve a dunder path prefix to its target ``(alias, model)``.
An empty prefix returns ``("", source_model)``. Goes through
:func:`get_relationship_alias_model_and_str`, which mutates its
``related_parts`` argument while resolving through models - the
prefix tuple is copied to a list to insulate the caller.
Examples (alias strings are hashes from the alias manager and
shown here as ``<...>`` since they are not stable across runs)::
_resolve_path(Post, ())
# => ("", Post)
_resolve_path(Post, ("category",))
# => ("", Category)
_resolve_path(Role, ("users", "categories"))
# => ("", Category)
:param source_model: model from which the path is rooted
:type source_model: type[Model]
:param parts_prefix: pre-split path segments
:type parts_prefix: tuple
:return: alias and target model at the prefix
:rtype: tuple[str, type[Model]]
"""
if not parts_prefix:
return "", source_model
alias, model, _, _ = get_relationship_alias_model_and_str(
source_model=source_model,
related_parts=list(parts_prefix),
)
return alias, model
def _referenced_at(
self,
source_model: type["Model"],
parts_prefix: tuple,
) -> bool:
"""
Return whether this :class:`ExcludableItems` already carries any
include or exclude entry for the model at ``parts_prefix``.
Used by :meth:`with_projection_exclusions` to detect whether the
user's ``fields()`` call referenced a given path - an empty
``Excludable`` (or none at all) means "not referenced".
Examples, contrasting the same path under two different
``fields()`` specs::
# Built from Post.objects.fields(["name"])
# items = {"post": Excludable(include={"name"})}
excludable._referenced_at(Post, ())
# => True (Post has a non-empty include)
excludable._referenced_at(Post, ("category",))
# => False (no entry for Category)
# Built from Post.objects.fields(["name", "category__name"])
# items = {
# "post": Excludable(include={"name"}),
# "_category": Excludable(include={"name"}),
# }
excludable._referenced_at(Post, ("category",))
# => True (Category has a non-empty include)
:param source_model: model from which the path is rooted
:type source_model: type[Model]
:param parts_prefix: pre-split path segments to check
:type parts_prefix: tuple
:return: True if a non-empty entry exists at the prefix
:rtype: bool
"""
alias, model = self._resolve_path(source_model, parts_prefix)
exc = self.items.get(self._make_key(model, alias))
return exc is not None and bool(exc.include or exc.exclude)
def get(self, model_cls: type["Model"], alias: str = "") -> Excludable:
"""
Return Excludable for given model and alias.
:param model_cls: target model to check
:type model_cls: ormar.models.metaclass.ModelMetaclass
:param alias: table alias from relation manager
:type alias: str
:return: Excludable for given model and alias
:rtype: ormar.models.excludable.Excludable
"""
key = self._make_key(model_cls, alias)
excludable = self.items.get(key)
if not excludable:
excludable = Excludable()
self.items[key] = excludable
return excludable
def build(
self,
items: Union[list[str], str, tuple[str], set[str], dict],
model_cls: type["Model"],
slot: Slot = "include",
) -> None:
"""
Receives the one of the types of items and parses them as to achieve
a end situation with one excludable per alias/model in relation.
Each excludable has three sets of values - include, exclude, and flatten.
:param items: values to be included, excluded or flattened
:type items: Union[list[str], str, tuple[str], set[str], dict]
:param model_cls: source model from which relations are constructed
:type model_cls: ormar.models.metaclass.ModelMetaclass
:param slot: which slot to write parsed values into
:type slot: Slot
"""
if isinstance(items, str):
items = {items}
if isinstance(items, dict):
self._traverse_dict(
values=items,
source_model=model_cls,
model_cls=model_cls,
slot=slot,
)
else:
items = set(items)
nested_items = set(x for x in items if "__" in x)
items.difference_update(nested_items)
if items:
self._set_slot(
items=items,
model_cls=model_cls,
slot=slot,
)
if nested_items:
self._traverse_list(values=nested_items, model_cls=model_cls, slot=slot)
if slot == "flatten":
self._validate_flatten_prefix_collisions()
def _set_slot(
self,
items: set,
model_cls: type["Model"],
slot: Slot,
alias: str = "",
path_parts: PathParts = (),
) -> None:
"""
Sets set of values to be stored for the given slot on the key that
corresponds to the passed model + alias.
:param items: items to write
:type items: set
:param model_cls: target model on which the items are stored
:type model_cls: type[Model]
:param slot: which slot to write to
:type slot: Slot
:param alias: table alias from relation manager
:type alias: str
:param path_parts: tuple of dunder path segments leading to this model
:type path_parts: PathParts
"""
if slot == "flatten":
self._validate_flatten_leaves(
items=items, model_cls=model_cls, path_parts=path_parts
)
for item in items:
self._flatten_paths.add(path_parts + (item,))
self._flatten_map_cache = None
key = self._make_key(model_cls, alias)
excludable = self.items.setdefault(key, Excludable())
excludable.set_values(value=items, slot=slot)
def _traverse_dict( # noqa: CFQ002
self,
values: dict,
source_model: type["Model"],
model_cls: type["Model"],
slot: Slot,
path_parts: PathParts = (),
alias: str = "",
) -> None:
"""
Goes through dict of nested values and construct/update Excludables.
:param values: items to include/exclude/flatten
:type values: dict
:param source_model: source model from which relations are constructed
:type source_model: ormar.models.metaclass.ModelMetaclass
:param model_cls: model reached via ``path_parts``
:type model_cls: ormar.models.metaclass.ModelMetaclass
:param slot: which slot to write into
:type slot: Slot
:param path_parts: tuple of dunder path segments leading to ``model_cls``
:type path_parts: PathParts
:param alias: alias of relation
:type alias: str
"""
self_fields = set()
for key, value in values.items():
if value is ...:
self_fields.add(key)
elif isinstance(value, set):
nested_parts = path_parts + (key,)
table_prefix, target_model, _, _ = get_relationship_alias_model_and_str(
source_model=source_model,
related_parts=list(nested_parts),
)
self._set_slot(
items=value,
model_cls=target_model,
slot=slot,
alias=table_prefix,
path_parts=nested_parts,
)
else:
nested_parts = path_parts + (key,)
table_prefix, target_model, _, _ = get_relationship_alias_model_and_str(
source_model=source_model,
related_parts=list(nested_parts),
)
self._traverse_dict(
values=value,
source_model=source_model,
model_cls=target_model,
slot=slot,
path_parts=nested_parts,
alias=table_prefix,
)
if self_fields:
self._set_slot(
items=self_fields,
model_cls=model_cls,
slot=slot,
alias=alias,
path_parts=path_parts,
)
def _traverse_list(
self, values: set[str], model_cls: type["Model"], slot: Slot
) -> None:
"""
Consume a set of dunder-style paths (``"a__b__c"``) and write each leaf
to the Excludable for its target model/alias. Each path is split once
into a tuple and threaded through ``_set_slot`` — no further string
joins happen downstream.
:param values: set of dunder-style path strings
:type values: set[str]
:param model_cls: source model from which relations are resolved
:type model_cls: type[Model]
:param slot: which slot to write into
:type slot: Slot
"""
for dunder_path in values:
parts = tuple(dunder_path.split("__"))
table_prefix, target_model, _, _ = get_relationship_alias_model_and_str(
source_model=model_cls,
related_parts=list(parts[:-1]),
)
self._set_slot(
items={parts[-1]},
model_cls=target_model,
slot=slot,
alias=table_prefix,
path_parts=parts[:-1],
)
@staticmethod
def _validate_flatten_leaves(
items: set, model_cls: type["Model"], path_parts: PathParts
) -> None:
"""
Ensure every leaf addressed by a flatten spec is a real relation on the
target model (not a scalar column and not a through model).
:param items: set of leaf names being flattened on ``model_cls``
:type items: set
:param model_cls: target model on which leaves must resolve to relations
:type model_cls: type[Model]
:param path_parts: tuple of segments leading to ``model_cls`` (only
joined for error messages, never parsed)
:type path_parts: PathParts
"""
model_fields = model_cls.ormar_config.model_fields
for item in items:
related_field = model_fields.get(item)
if related_field is None:
raise QueryDefinitionError(
f"Unknown relation '{item}' on model "
f"{model_cls.get_name(lower=False)} in flatten_fields path "
f"'{join_path(path_parts, item)}'."
)
if getattr(related_field, "is_through", False):
raise QueryDefinitionError(
f"Cannot flatten through model '{item}' at path "
f"'{join_path(path_parts, item)}'. Flatten the many-to-many "
f"relation itself instead."
)
if not getattr(related_field, "is_relation", False):
raise QueryDefinitionError(
f"flatten_fields target '{join_path(path_parts, item)}' is "
f"not a relation on model {model_cls.get_name(lower=False)}. "
f"Only foreign keys, many-to-many, and reverse relations can "
f"be flattened."
)
def _validate_flatten_prefix_collisions(self) -> None:
"""
Ensure no flatten path is a strict ancestor of another. Flattening a
relation replaces it with a scalar PK, so a deeper flatten through the
same chain is unreachable.
Works directly on tuple paths — sorted so each prefix's descendants
follow it immediately, enabling an early break once the prefix no
longer matches.
:raises QueryDefinitionError: on any prefix collision
"""
paths = sorted(self._flatten_paths)
for i, short in enumerate(paths):
short_len = len(short)
for longer in paths[i + 1 :]:
if longer[:short_len] != short:
break
raise QueryDefinitionError(
f"Conflicting flatten directives: "
f"'{join_path(short)}' is flattened to its primary key, "
f"so nested flatten '{join_path(longer)}' is unreachable."
)
def with_projection_exclusions(
self,
source_model: type["Model"],
select_related: list[str],
) -> "ExcludableItems":
"""
Return a copy with relations not referenced by ``fields()`` removed
from a flat values projection.
A filter like ``filter(project__id=...)`` auto-joins ``project``;
without this, a flat ``values()`` call leaks every Project column
even though only one main-model field was requested. Returns the
original instance unchanged when ``fields()`` was never called -
the leak is a values-projection problem, not an ORM-load problem.
:param source_model: model from which relation paths are rooted
:type source_model: type[Model]
:param select_related: paths joined into the query (dunder strings)
:type select_related: list[str]
:return: new :class:`ExcludableItems` with implicit excludes added,
or ``self`` when no ``fields()`` call had any effect
:rtype: ExcludableItems
"""
if self.include_entry_count() == 0:
return self
excludable = ExcludableItems.from_excludable(self)
# If fields() never names the source model, project only its PK.
if not excludable._referenced_at(source_model, ()):
excludable.get(source_model).pk_only = True
for path in select_related:
parts = tuple(path.split("__"))
# Snapshot which prefixes carry a fields() reference before any
# exclusions are added below - the exclude additions would
# otherwise show up as "referenced" on the next iteration.
referenced = [
excludable._referenced_at(source_model, parts[: d + 1])
for d in range(len(parts))
]
# Walk deepest-first so kept_deeper accumulates inward. Each
# segment still needs its own exclude when not kept, because
# ReverseAliasResolver only consults the immediate parent.
kept_deeper = False
for i in reversed(range(len(parts))):
kept_deeper = kept_deeper or referenced[i]
segment = parts[i]
parent_alias, parent_model = excludable._resolve_path(
source_model, parts[:i]
)
parent_exc = excludable.get(parent_model, alias=parent_alias)
if parent_exc.is_explicitly_included(segment):
continue
if kept_deeper:
# Intermediate relation: keep the join but project
# PK-only when this segment was never explicitly named.
if not referenced[i]:
target_alias, target_model = excludable._resolve_path(
source_model, parts[: i + 1]
)
excludable.get(target_model, alias=target_alias).pk_only = True
continue
field = parent_model.ormar_config.model_fields[segment]
parent_exc.exclude.add(segment)
if field.is_multi:
parent_exc.exclude.add(field.through.get_name())
return excludable
def validate_flatten_vs_excludable(self, source_model: type["Model"]) -> None:
"""
Ensure no flattened relation has sub-field include/exclude on its target.
Whole-relation include/exclude at the parent level is allowed; only
sub-field selection on the flattened child is flagged.
:param source_model: model from which flatten paths are rooted
:type source_model: type[Model]
:raises QueryDefinitionError: when a flattened relation has sub-field
include/exclude on its target model
"""
for parts in self._flatten_paths:
table_prefix, target_model, _, _ = get_relationship_alias_model_and_str(
source_model=source_model,
related_parts=list(parts),
)
child = self.items.get(self._make_key(target_model, table_prefix))
if not child:
continue
conflicts = (child.include | child.exclude) - {...}
if conflicts:
raise QueryDefinitionError(
f"Flatten conflict: relation '{join_path(parts)}' is "
f"flattened but include/exclude specifies children "
f"{sorted(conflicts)}. A flattened relation renders only "
f"its primary key and cannot have children selected."
)
def join_path(parts: PathParts, tail: Optional[str] = None) -> str:
"""
Build a user-facing dunder path string from a pre-split tuple, optionally
appending one extra segment. Used only for error messages — the rest of
the module keeps paths as tuples.
:param parts: pre-split path segments
:type parts: PathParts
:param tail: optional additional segment to append
:type tail: Optional[str]
:return: dunder-joined path string
:rtype: str
"""
if tail is None:
return "__".join(parts)
return "__".join(parts + (tail,)) if parts else tail
ormar-orm-ormar-a7e7fde/ormar/models/helpers/ 0000775 0000000 0000000 00000000000 15211567312 0021377 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/models/helpers/__init__.py 0000664 0000000 0000000 00000002430 15211567312 0023507 0 ustar 00root root 0000000 0000000 from ormar.models.helpers.models import (
check_required_config_parameters,
config_field_not_set,
extract_annotations_and_default_vals,
populate_default_options_values,
)
from ormar.models.helpers.pydantic import (
get_potential_fields,
get_pydantic_base_orm_config,
merge_or_generate_pydantic_config,
)
from ormar.models.helpers.relations import (
alias_manager,
expand_reverse_relationships,
register_relation_in_alias_manager,
)
from ormar.models.helpers.sqlalchemy import (
populate_config_sqlalchemy_table_if_required,
populate_config_tablename_columns_and_pk,
sqlalchemy_columns_from_model_fields,
)
from ormar.models.helpers.validation import modify_schema_example
__all__ = [
"expand_reverse_relationships",
"extract_annotations_and_default_vals",
"populate_config_tablename_columns_and_pk",
"populate_config_sqlalchemy_table_if_required",
"populate_default_options_values",
"alias_manager",
"register_relation_in_alias_manager",
"get_potential_fields",
"get_pydantic_base_orm_config",
"merge_or_generate_pydantic_config",
"check_required_config_parameters",
"sqlalchemy_columns_from_model_fields",
"config_field_not_set",
"remove_excluded_parent_fields",
"modify_schema_example",
]
ormar-orm-ormar-a7e7fde/ormar/models/helpers/models.py 0000664 0000000 0000000 00000013425 15211567312 0023241 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Any, ForwardRef
import ormar_rust_utils
import pydantic
import ormar # noqa: I100
from ormar.models.helpers.pydantic import populate_pydantic_default_values
if TYPE_CHECKING: # pragma no cover
from ormar import Model
from ormar.fields import BaseField
def is_field_an_forward_ref(field: "BaseField") -> bool:
"""
Checks if field is a relation field and whether any of the referenced models
are ForwardRefs that needs to be updated before proceeding.
:param field: model field to verify
:type field: type[BaseField]
:return: result of the check
:rtype: bool
"""
return field.is_relation and (
field.to.__class__ == ForwardRef or field.through.__class__ == ForwardRef
)
def populate_default_options_values( # noqa: CCR001
new_model: type["Model"], model_fields: dict
) -> None:
"""
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.
Current options are:
* constraints = []
* abstract = False
:param new_model: newly constructed Model
:type new_model: Model class
:param model_fields: dict of model fields
:type model_fields: Union[dict[str, type], dict]
"""
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 model_fields.items() if field.__type__ == pydantic.Json
}
new_model._bytes_fields = {
name for name, field in model_fields.items() if field.__type__ is bytes
}
new_model._onupdate_fields = {
name for name, field in model_fields.items() if field.has_on_update()
}
new_model.__relation_map__ = None
new_model.__ormar_fields_validators__ = None
def check_required_config_parameters(new_model: type["Model"]) -> None:
"""
Verifies if ormar.Model has database and metadata set.
Recreates Connection pool for sqlite3
:param new_model: newly declared ormar Model
:type new_model: Model class
"""
if new_model.ormar_config.proxy and new_model.ormar_config.abstract:
raise ormar.ModelDefinitionError(
f"{new_model.__name__} cannot be both proxy and abstract."
)
skip_db_metadata_checks = (
new_model.ormar_config.abstract or new_model.ormar_config.proxy
)
if new_model.ormar_config.database is None and not skip_db_metadata_checks:
raise ormar.ModelDefinitionError(
f"{new_model.__name__} does not have database defined."
)
if new_model.ormar_config.metadata is None and not skip_db_metadata_checks:
raise ormar.ModelDefinitionError(
f"{new_model.__name__} does not have metadata defined."
)
def extract_annotations_and_default_vals(attrs: dict) -> tuple[dict, dict]:
"""
Extracts annotations from class namespace dict and triggers
extraction of ormar model_fields.
:param attrs: namespace of the class created
:type attrs: dict
:return: namespace of the class updated, dict of extracted model_fields
:rtype: tuple[dict, dict]
"""
key = "__annotations__"
attrs[key] = attrs.get(key, {})
attrs, model_fields = populate_pydantic_default_values(attrs)
return attrs, model_fields
def group_related_list(list_: list) -> dict:
"""
Translates the list of related strings into a dictionary.
That way nested models are grouped to traverse them in a right order
and to avoid repetition.
Sample: ["people__houses", "people__cars__models", "people__cars__colors"]
will become:
{'people': {'houses': [], 'cars': ['models', 'colors']}}
Result dictionary is sorted by length of the values and by key
:param list_: list of related models used in select related
:type list_: list[str]
:return: list converted to dictionary to avoid repetition and group nested models
:rtype: dict[str, list]
"""
return ormar_rust_utils.group_related_list(list_)
def ordered_join_paths(related_tree: Any, prefix: str = "") -> list[str]:
"""
Flattens a grouped relation tree (as produced by :func:`group_related_list`)
into the depth-first order in which the join builder walks the relations, with
shared prefixes visited only once.
Sample: {'people': {'houses': [], 'cars': ['models', 'colors']}}
will become:
["people", "people__houses", "people__cars", "people__cars__models",
"people__cars__colors"]
:param related_tree: grouped relation tree (nested dict or list of leaf names)
:type related_tree: Any
:param prefix: relation path accumulated from the parent levels
:type prefix: str
:return: depth-first ordered list of relation paths
:rtype: list[str]
"""
items = (
related_tree.items()
if isinstance(related_tree, dict)
else [(name, None) for name in related_tree]
)
paths: list[str] = []
for name, subtree in items:
path = f"{prefix}__{name}" if prefix else name
paths.append(path)
if subtree:
paths.extend(ordered_join_paths(subtree, path))
return paths
def config_field_not_set(model: type["Model"], field_name: str) -> bool:
"""
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).
:param model: newly constructed model
:type model: Model class
:param field_name: name of the ormar field
:type field_name: str
:return: result of the check
:rtype: bool
"""
return not getattr(model.ormar_config, field_name)
ormar-orm-ormar-a7e7fde/ormar/models/helpers/pydantic.py 0000664 0000000 0000000 00000010066 15211567312 0023567 0 ustar 00root root 0000000 0000000 from types import MappingProxyType
from typing import TYPE_CHECKING, Optional, Union
import pydantic
from pydantic import ConfigDict
from pydantic.fields import FieldInfo
from ormar.exceptions import ModelDefinitionError # noqa: I100, I202
from ormar.fields import BaseField
if TYPE_CHECKING: # pragma no cover
from ormar import Model
from ormar.fields import ManyToManyField
def create_pydantic_field(
field_name: str, model: type["Model"], model_field: "ManyToManyField"
) -> None:
"""
Registers pydantic field on through model that leads to passed model
and is registered as field_name passed.
Through model is fetched from through attributed on passed model_field.
:param field_name: field name to register
:type field_name: str
:param model: type of field to register
:type model: Model class
:param model_field: relation field from which through model is extracted
:type model_field: ManyToManyField class
"""
model_field.through.model_fields[field_name] = FieldInfo.from_annotated_attribute(
annotation=Optional[model], # type: ignore
default=None,
)
model_field.through.model_rebuild(
force=True, _types_namespace={model_field.owner.__name__: model_field.owner}
)
def populate_pydantic_default_values(attrs: dict) -> tuple[dict, dict]:
"""
Extracts ormar fields from annotations (deprecated) and from namespace
dictionary of the class. Fields declared on model are all subclasses of the
BaseField class.
Trigger conversion of ormar field into pydantic FieldInfo, which has all needed
parameters saved.
Overwrites the annotations of ormar fields to corresponding types declared on
ormar fields (constructed dynamically for relations).
Those annotations are later used by pydantic to construct it's own fields.
:param attrs: current class namespace
:type attrs: dict
:return: namespace of the class updated, dict of extracted model_fields
:rtype: tuple[dict, dict]
"""
model_fields = {}
potential_fields = {}
potential_fields.update(get_potential_fields(attrs))
for field_name, field in potential_fields.items():
field.name = field_name
model_fields[field_name] = field
default_type = (
field.__type__ if not field.nullable else Optional[field.__type__]
)
overwrite_type = (
field.__pydantic_type__
if field.__type__ != field.__pydantic_type__
else None
)
attrs["__annotations__"][field_name] = overwrite_type or default_type
return attrs, model_fields
def merge_or_generate_pydantic_config(attrs: dict, name: str) -> None:
"""
Checks if the user provided pydantic Config,
and if he did merges it with the default one.
Updates the attrs in place with a new config.
:rtype: None
"""
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 dictionary."
)
config = {**default_config, **provided_config}
attrs["model_config"] = config
else:
attrs["model_config"] = default_config
def get_pydantic_base_orm_config() -> pydantic.ConfigDict:
"""
Returns empty pydantic Config with orm_mode set to True.
:return: empty default config with orm_mode set.
:rtype: pydantic Config
"""
return ConfigDict(validate_assignment=True, ser_json_bytes="base64")
def get_potential_fields(attrs: Union[dict, MappingProxyType]) -> dict:
"""
Gets all the fields in current class namespace that are Fields.
:param attrs: current class namespace
:type attrs: dict
:return: extracted fields that are ormar Fields
:rtype: dict
"""
return {
k: v
for k, v in attrs.items()
if (
(isinstance(v, type) and issubclass(v, BaseField))
or isinstance(v, BaseField)
)
}
ormar-orm-ormar-a7e7fde/ormar/models/helpers/related_names_validation.py 0000664 0000000 0000000 00000003420 15211567312 0026765 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, ForwardRef, Optional
import ormar # noqa: I100
if TYPE_CHECKING: # pragma no cover
from ormar import Model
def validate_related_names_in_relations( # noqa CCR001
model_fields: dict, new_model: type["Model"]
) -> None:
"""
Performs a validation of relation_names in relation fields.
If multiple fields are leading to the same related model
only one can have empty related_name param
(populated by default as model.name.lower()+'s').
Also related_names have to be unique for given related model.
:raises ModelDefinitionError: if validation of related_names fail
:param model_fields: dictionary of declared ormar model fields
:type model_fields: dict[str, ormar.Field]
:param new_model:
:type new_model: Model class
"""
already_registered: dict[str, list[Optional[str]]] = dict()
for field in model_fields.values():
if field.is_relation:
to_name = (
field.to.get_name()
if not field.to.__class__ == ForwardRef
else str(field.to)
)
previous_related_names = already_registered.setdefault(to_name, [])
if field.related_name in previous_related_names:
raise ormar.ModelDefinitionError(
f"Multiple fields declared on {new_model.get_name(lower=False)} "
f"model leading to {field.to.get_name(lower=False)} model without "
f"related_name property set. \nThere can be only one relation with "
f"default/empty name: '{new_model.get_name() + 's'}'"
f"\nTip: provide different related_name for FK and/or M2M fields"
)
previous_related_names.append(field.related_name)
ormar-orm-ormar-a7e7fde/ormar/models/helpers/relations.py 0000664 0000000 0000000 00000035045 15211567312 0023760 0 ustar 00root root 0000000 0000000 import inspect
import warnings
from typing import (
TYPE_CHECKING,
Any,
ForwardRef,
Optional,
Union,
cast,
get_args,
get_origin,
)
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
from ormar.fields import Through
from ormar.models.descriptors import RelationDescriptor
from ormar.models.helpers.sqlalchemy import adjust_through_many_to_many_model
from ormar.relations import AliasManager
if TYPE_CHECKING: # pragma no cover
from ormar import Model
from ormar.fields import BaseField, ForeignKeyField, ManyToManyField
alias_manager = AliasManager()
def register_relation_on_build(field: "ForeignKeyField") -> None:
"""
Registers ForeignKey relation in alias_manager to set a table_prefix.
Registration include also reverse relation side to be able to join both sides.
Relation is registered by model name and relation field name to allow for multiple
relations between two Models that needs to have different
aliases for proper sql joins.
:param field: relation field
:type field: ForeignKey class
"""
alias_manager.add_relation_type(
source_model=field.owner,
relation_name=field.name,
reverse_name=field.get_source_related_name(),
)
def register_many_to_many_relation_on_build(field: "ManyToManyField") -> None:
"""
Registers connection between through model and both sides of the m2m relation.
Registration include also reverse relation side to be able to join both sides.
Relation is registered by model name and relation field name to allow for multiple
relations between two Models that needs to have different
aliases for proper sql joins.
By default relation name is a model.name.lower().
:param field: relation field
:type field: ManyToManyField class
"""
alias_manager.add_relation_type(
source_model=field.through,
relation_name=field.default_source_field_name(),
reverse_name=field.get_source_related_name(),
)
alias_manager.add_relation_type(
source_model=field.through,
relation_name=field.default_target_field_name(),
reverse_name=field.get_related_name(),
)
def expand_reverse_relationship(model_field: "ForeignKeyField") -> None:
"""
If the reverse relation has not been set before it's set here.
:param model_field:
:type model_field:
:return: None
:rtype: None
"""
if reverse_field_not_already_registered(model_field=model_field):
register_reverse_model_fields(model_field=model_field)
def expand_reverse_relationships(model: type["Model"]) -> None:
"""
Iterates through model_fields of given model and verifies if all reverse
relation have been populated on related models.
If the reverse relation has not been set before it's set here.
:param model: model on which relation should be checked and registered
:type model: Model class
"""
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()
and not model_field.is_through
):
model_field = cast("ForeignKeyField", model_field)
expand_reverse_relationship(model_field=model_field)
def register_reverse_model_fields(model_field: "ForeignKeyField") -> None:
"""
Registers reverse ForeignKey field on related model.
By default it's name.lower()+'s' of the model on which relation is defined.
But if the related_model name is provided it's registered with that name.
Autogenerated reverse fields also set related_name to the original field name.
:param model_field: original relation ForeignKey field
:type model_field: relation Field
"""
related_name = model_field.get_related_name()
related_model_fields = model_field.to.ormar_config.model_fields
if model_field.is_multi:
related_model_fields[related_name] = ManyToMany( # type: ignore
model_field.owner,
through=model_field.through,
name=related_name,
virtual=True,
related_name=model_field.name,
owner=model_field.to,
self_reference=model_field.self_reference,
self_reference_primary=model_field.self_reference_primary,
orders_by=model_field.related_orders_by,
skip_field=model_field.skip_reverse,
through_relation_name=model_field.through_reverse_relation_name,
through_reverse_relation_name=model_field.through_relation_name,
through_relation_nullable=model_field.through_reverse_relation_nullable,
through_reverse_relation_nullable=model_field.through_relation_nullable,
)
# register foreign keys on through model
model_field = cast("ManyToManyField", model_field)
register_through_shortcut_fields(model_field=model_field)
adjust_through_many_to_many_model(model_field=model_field)
else:
related_model_fields[related_name] = ForeignKey( # type: ignore
model_field.owner,
real_name=related_name,
virtual=True,
related_name=model_field.name,
owner=model_field.to,
self_reference=model_field.self_reference,
orders_by=model_field.related_orders_by,
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,
_types_namespace={
**{model_field.owner.__name__: model_field.owner},
**{
field.to.__name__: field.to
for field in related_model_fields.values()
if field.is_relation and field.to.__class__ != ForwardRef
},
},
)
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:
try:
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore", message="Pydantic serializer warnings"
)
return handler(children)
except ValueError as exc: # pragma: no cover
if not str(exc).startswith("Circular reference"):
raise exc
result = []
for child in children:
if not hasattr(child, "ormar_config"):
continue
# If there is one circular ref dump all children as pk only
result.append({child.ormar_config.pkname: child.pk})
return result
except ReferenceError:
# Pydantic >= 2.13 may invoke this serializer with weakref
# children whose referent is already gone (e.g. when ormar's
# late metaclass rebuild changed dispatch ordering).
return None
decorator = field_serializer(related_name, mode="wrap", check_fields=False)(
serialize
)
setattr(to_model, f"serialize_{related_name}", decorator)
# DecoratorInfos.build will overwrite __pydantic_decorators__ on to_model,
# deleting the previous decorators. We need to save them and then merge them.
prev_decorators = getattr(to_model, "__pydantic_decorators__", DecoratorInfos())
new_decorators = DecoratorInfos.build(to_model)
prev_decorators.validators.update(new_decorators.validators)
prev_decorators.field_validators.update(new_decorators.field_validators)
prev_decorators.root_validators.update(new_decorators.root_validators)
prev_decorators.field_serializers.update(new_decorators.field_serializers)
prev_decorators.model_serializers.update(new_decorators.model_serializers)
prev_decorators.model_validators.update(new_decorators.model_validators)
prev_decorators.computed_fields.update(new_decorators.computed_fields)
setattr(to_model, "__pydantic_decorators__", prev_decorators)
def replace_models_with_copy(
annotation: Any, 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)
origin = get_origin(annotation)
if origin is list:
args = get_args(annotation)
if not args:
return annotation # pragma: no cover
return list[ # type: ignore
replace_models_with_copy(
annotation=args[0],
source_model_field=source_model_field,
)
]
elif origin is Union:
args = get_args(annotation)
new_args = [
replace_models_with_copy(
annotation=arg, source_model_field=source_model_field
)
for arg in args
]
return Union[tuple(new_args)]
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.
:param model_field: relation field defined in parent model
:type model_field: ManyToManyField
"""
through_model = model_field.through
through_name = through_model.get_name(lower=True)
related_name = model_field.get_related_name()
model_field.owner.ormar_config.model_fields[through_name] = Through(
through_model,
real_name=through_name,
virtual=True,
related_name=model_field.name,
owner=model_field.owner,
nullable=True,
)
model_field.to.ormar_config.model_fields[through_name] = Through(
through_model,
real_name=through_name,
virtual=True,
related_name=related_name,
owner=model_field.to,
nullable=True,
)
setattr(model_field.owner, through_name, RelationDescriptor(name=through_name))
setattr(model_field.to, through_name, RelationDescriptor(name=through_name))
def register_relation_in_alias_manager(field: "BaseField") -> None:
"""
Registers the relation (and reverse relation) in alias manager.
The m2m relations require registration of through model between
actual end models of the relation.
Delegates the actual registration to:
m2m - register_many_to_many_relation_on_build
fk - register_relation_on_build
:param field: relation field
:type field: ForeignKey or ManyToManyField class
"""
if field.is_multi:
if field.has_unresolved_forward_refs():
return
field = cast("ManyToManyField", field)
register_many_to_many_relation_on_build(field=field)
elif field.is_relation and not field.is_through:
if field.has_unresolved_forward_refs():
return
register_relation_on_build(field=cast("ForeignKeyField", field))
def verify_related_name_dont_duplicate(
related_name: str, model_field: "ForeignKeyField"
) -> None:
"""
Verifies whether the used related_name (regardless of the fact if user defined or
auto generated) is already used on related model, but is connected with other model
than the one that we connect right now.
:raises ModelDefinitionError: if name is already used but lead to different related
model
:param related_name:
:type related_name:
:param model_field: original relation ForeignKey field
:type model_field: relation Field
:return: None
:rtype: None
"""
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.ormar_config != model_field.owner.ormar_config
):
raise ormar.ModelDefinitionError(
f"Relation with related_name "
f"'{related_name}' "
f"leading to model "
f"{model_field.to.get_name(lower=False)} "
f"cannot be used on model "
f"{model_field.owner.get_name(lower=False)} "
f"because it's already used by model "
f"{fk_field.to.get_name(lower=False)}"
)
def reverse_field_not_already_registered(model_field: "ForeignKeyField") -> bool:
"""
Checks if child is already registered in parents pydantic fields.
:raises ModelDefinitionError: if related name is already used but lead to different
related model
:param model_field: original relation ForeignKey field
:type model_field: relation Field
:return: result of the check
:rtype: bool
"""
related_name = model_field.get_related_name()
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(
related_name=related_name, model_field=model_field
)
if not check_result2:
verify_related_name_dont_duplicate(
related_name=model_field.owner.get_name(), model_field=model_field
)
return check_result and check_result2
ormar-orm-ormar-a7e7fde/ormar/models/helpers/sqlalchemy.py 0000664 0000000 0000000 00000035447 15211567312 0024130 0 ustar 00root root 0000000 0000000 import logging
from typing import TYPE_CHECKING, ForwardRef, Optional, Union
import sqlalchemy
import ormar # noqa: I100, I202
from ormar.models.descriptors import RelationDescriptor
from ormar.models.helpers.pydantic import create_pydantic_field
from ormar.models.helpers.related_names_validation import (
validate_related_names_in_relations,
)
if TYPE_CHECKING: # pragma no cover
from ormar import BaseField, ForeignKeyField, ManyToManyField, Model
from ormar.models import NewBaseModel
from ormar.models.ormar_config import OrmarConfig
def qualified_fk_reference(target: type["Model"], column_alias: str) -> str:
"""
Builds the dotted reference string passed to ``sqlalchemy.ForeignKey``.
SQLAlchemy expects ``"schema.table.column"`` for cross-schema foreign keys
and ``"table.column"`` when the target is in the default schema.
:param target: target ormar Model
:type target: type["Model"]
:param column_alias: alias of the referenced column on the target table
:type column_alias: str
:return: dotted reference string
:rtype: str
"""
config = target.ormar_config
prefix = f"{config.schema}." if config.schema else ""
return f"{prefix}{config.tablename}.{column_alias}"
def validate_cross_schema_constraints(
metadata: sqlalchemy.MetaData, dialect_name: str
) -> None:
"""
Rejects cross-schema foreign keys on dialects that cannot enforce them.
SQLite forbids foreign keys whose parent and child tables live in different
attached databases. Detecting this at metaclass time is unreliable because
models are usually declared before the engine/dialect is known, so the check
runs against the assembled metadata before ``create_all``.
:raises ModelDefinitionError: if a cross-schema FK is found on SQLite
:param metadata: metadata holding all the registered ormar tables
:type metadata: sqlalchemy.MetaData
:param dialect_name: name of the dialect about to issue DDL
:type dialect_name: str
"""
if dialect_name != "sqlite":
return
for table in metadata.sorted_tables:
for fk in table.foreign_keys:
parent_schema = fk.column.table.schema
child_schema = table.schema
if parent_schema != child_schema:
raise ormar.ModelDefinitionError(
f"SQLite does not support foreign keys across schemas: "
f"{table.fullname}.{fk.parent.name} -> {fk.column.table.fullname}."
" Use PostgreSQL or MySQL, or keep both tables in the same schema."
)
def adjust_through_many_to_many_model(model_field: "ManyToManyField") -> None:
"""
Registers m2m relation on through model.
Sets ormar.ForeignKey from through model to both child and parent models.
Sets sqlalchemy.ForeignKey to both child and parent models.
Sets pydantic fields with child and parent model types.
:param model_field: relation field defined in parent model
:type model_field: ManyToManyField
"""
parent_name = model_field.default_target_field_name()
child_name = model_field.default_source_field_name()
model_fields = model_field.through.ormar_config.model_fields
model_fields[parent_name] = ormar.ForeignKey( # type: ignore
model_field.to,
real_name=parent_name,
ondelete="CASCADE",
owner=model_field.through,
)
model_fields[child_name] = ormar.ForeignKey( # type: ignore
model_field.owner,
real_name=child_name,
ondelete="CASCADE",
owner=model_field.through,
)
create_and_append_m2m_fk(
model=model_field.to,
model_field=model_field,
field_name=parent_name,
foreign_key_name=model_field.through_reverse_foreign_key_name,
nullable=model_field.through_reverse_relation_nullable,
)
create_and_append_m2m_fk(
model=model_field.owner,
model_field=model_field,
field_name=child_name,
foreign_key_name=model_field.through_foreign_key_name,
nullable=model_field.through_relation_nullable,
)
create_pydantic_field(parent_name, model_field.to, model_field)
create_pydantic_field(child_name, model_field.owner, model_field)
setattr(model_field.through, parent_name, RelationDescriptor(name=parent_name))
setattr(model_field.through, child_name, RelationDescriptor(name=child_name))
def create_and_append_m2m_fk(
model: type["Model"],
model_field: "ManyToManyField",
field_name: str,
foreign_key_name: Optional[str] = None,
nullable: bool = True,
) -> None:
"""
Registers sqlalchemy Column with sqlalchemy.ForeignKey leading to the model.
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
:param model: Model class to which FK should be created
:type model: Model class
:param model_field: field with ManyToMany relation
:type model_field: ManyToManyField field
:param foreign_key_name: optional override for the generated FK constraint name.
:type foreign_key_name: Optional[str]
:param nullable: whether the created column is nullable.
:type nullable: bool
"""
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"
)
through_table = model_field.through.ormar_config.tablename
target_table = model.ormar_config.tablename
default_name = f"fk_{through_table}_{target_table}_{field_name}_{pk_alias}"
column = sqlalchemy.Column(
field_name,
pk_column.type,
sqlalchemy.schema.ForeignKey(
qualified_fk_reference(model, pk_alias),
ondelete="CASCADE",
onupdate="CASCADE",
name=foreign_key_name or default_name,
),
nullable=nullable,
)
model_field.through.ormar_config.columns.append(column)
model_field.through.ormar_config.table.append_column(column, replace_existing=True)
def check_pk_column_validity(
field_name: str, field: "BaseField", pkname: Optional[str]
) -> Optional[str]:
"""
Receives the field marked as primary key and verifies if the pkname
was not already set (only one allowed per model).
:raises ModelDefintionError: if pkname already set
:param field_name: name of field
:type field_name: str
:param field: ormar.Field
:type field: BaseField
:param pkname: already set pkname
:type pkname: Optional[str]
:return: name of the field that should be set as pkname
:rtype: str
"""
if pkname is not None:
raise ormar.ModelDefinitionError("Only one primary key column is allowed.")
return field_name
def sqlalchemy_columns_from_model_fields(
model_fields: dict, new_model: type["Model"]
) -> tuple[Optional[str], list[sqlalchemy.Column]]:
"""
Iterates over declared on Model model fields and extracts fields that
should be treated as database fields.
If the model is empty it sets mandatory id field as primary key
(used in through models in m2m relations).
Triggers a validation of relation_names in relation fields. If multiple 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
Sets `owner` on each model_field as reference to newly created Model.
:raises ModelDefinitionError: if validation of related_names fail,
or pkname validation fails.
:param model_fields: dictionary of declared ormar model fields
:type model_fields: dict[str, ormar.Field]
:param new_model:
:type new_model: Model class
:return: pkname, list of sqlalchemy columns
:rtype: tuple[Optional[str], list[sqlalchemy.Column]]
"""
if len(model_fields.keys()) == 0:
model_fields["id"] = ormar.Integer(name="id", primary_key=True)
logging.warning(
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)
return _process_fields(model_fields=model_fields, new_model=new_model)
def _process_fields(
model_fields: dict, new_model: type["Model"]
) -> tuple[Optional[str], list[sqlalchemy.Column]]:
"""
Helper method.
Populates pkname and columns.
Sets `owner` on each model_field as reference to newly created Model.
:raises ModelDefinitionError: if validation of related_names fail,
or pkname validation fails.
:param model_fields: dictionary of declared ormar model fields
:type model_fields: dict[str, ormar.Field]
:param new_model:
:type new_model: Model class
:return: pkname, list of sqlalchemy columns
:rtype: tuple[Optional[str], list[sqlalchemy.Column]]
"""
columns = []
pkname = None
for field_name, field in model_fields.items():
field.owner = new_model
if _is_through_model_not_set(field):
field.create_default_through_model()
if field.primary_key:
pkname = check_pk_column_validity(field_name, field, pkname)
if _is_db_field(field):
columns.append(field.get_column(field.get_alias()))
return pkname, columns
def _is_through_model_not_set(field: "BaseField") -> bool:
"""
Alias to if check that verifies if through model was created.
:param field: field to check
:type field: "BaseField"
:return: result of the check
:rtype: bool
"""
return field.is_multi and not field.through and not field.to.__class__ == ForwardRef
def _is_db_field(field: "BaseField") -> bool:
"""
Alias to if check that verifies if field should be included in database.
:param field: field to check
:type field: "BaseField"
:return: result of the check
:rtype: bool
"""
return not field.virtual and not field.is_multi
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 OrmarConfig.
Default tablename if not present is class name lower + s (i.e. Bed becomes -> beds)
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.
:raises ModelDefinitionError: if pkname is not present raises ModelDefinitionError.
Each model has to have pk.
:param name: name of the current Model
:type name: str
:param new_model: currently constructed Model
:type new_model: ormar.models.metaclass.ModelMetaclass
:return: Model with populated pkname and columns in OrmarConfig
:rtype: ormar.models.metaclass.ModelMetaclass
"""
tablename = name.lower() + "s"
new_model.ormar_config.tablename = (
new_model.ormar_config.tablename
if new_model.ormar_config.tablename
else tablename
)
pkname: Optional[str]
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.ormar_config.model_fields, new_model
)
if pkname is None:
raise ormar.ModelDefinitionError("Table has to have a primary key.")
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(config: "OrmarConfig") -> bool:
"""
Check is any column is of NUllType() meaning it's empty column from ForwardRef
: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 config.columns
)
def populate_config_sqlalchemy_table_if_required(config: "OrmarConfig") -> None:
"""
Constructs sqlalchemy table out of columns and parameters set on OrmarConfig.
It populates name, metadata, columns and constraints.
:param config: OrmarConfig of the Model without sqlalchemy table constructed
:type config: Model class OrmarConfig
"""
if config.table is None and check_for_null_type_columns_from_forward_refs(
config=config
):
set_constraint_names(config=config)
table = sqlalchemy.Table(
config.tablename,
config.metadata,
*config.columns,
*config.constraints,
schema=config.schema,
comment=config.comment,
)
config.table = table
def set_constraint_names(config: "OrmarConfig") -> None:
"""
Populates the names on IndexColumns and UniqueColumns and CheckColumns constraints.
:param config: OrmarConfig of the Model without sqlalchemy table constructed
:type config: Model class OrmarConfig
"""
for constraint in config.constraints:
if isinstance(constraint, sqlalchemy.UniqueConstraint) and not constraint.name:
constraint.name = (
f"uc_{config.tablename}_"
f"{'_'.join([str(col) for col in constraint._pending_colargs])}"
)
elif (
isinstance(constraint, sqlalchemy.Index)
and constraint.name == "TEMPORARY_NAME"
):
constraint.name = (
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_{config.tablename}_{sql_condition}"
def update_column_definition(
model: Union[type["Model"], type["NewBaseModel"]], field: "ForeignKeyField"
) -> None:
"""
Updates a column with a new type column based on updated parameters in FK fields.
:param model: model on which columns needs to be updated
:type model: type["Model"]
:param field: field with column definition that requires update
:type field: ForeignKeyField
:return: None
:rtype: None
"""
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())
columns[ind] = new_column
break
ormar-orm-ormar-a7e7fde/ormar/models/helpers/validation.py 0000664 0000000 0000000 00000015321 15211567312 0024105 0 ustar 00root root 0000000 0000000 import decimal
import numbers
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
import pydantic
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
from ormar import Model
from ormar.fields import BaseField
def generate_model_example(
model: type["Model"], relation_map: Optional[dict] = None
) -> dict:
"""
Generates example to be included in schema in fastapi.
:param model: ormar.Model
:type model: type["Model"]
:param relation_map: dict with relations to follow
:type relation_map: Optional[dict]
:return: dict with example values
:rtype: dict[str, int]
"""
example: dict[str, Any] = dict()
relation_map = (
relation_map
if relation_map is not None
else translate_list_to_dict(model._iterate_related_models())
)
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.ormar_config.model_fields}
pydantic_repr = generate_pydantic_example(pydantic_model=model, exclude=to_exclude)
example.update(pydantic_repr)
return example
def populates_sample_fields_values(
example: dict[str, Any],
name: str,
field: "BaseField",
relation_map: Optional[dict] = None,
) -> None:
"""
Iterates the field and sets fields to sample values
:param field: ormar field
:type field: BaseField
:param name: name of the field
:type name: str
:param example: example dict
:type example: dict[str, Any]
:param relation_map: dict with relations to follow
:type relation_map: Optional[dict]
"""
if not field.is_relation:
is_bytes_str = field.__type__ is bytes and field.represent_as_base64_str
example[name] = field.__sample__ if not is_bytes_str else "string"
elif isinstance(relation_map, dict) and name in relation_map:
example[name] = get_nested_model_example(
name=name, field=field, relation_map=relation_map
)
def get_nested_model_example(
name: str, field: "BaseField", relation_map: dict
) -> Union[list, dict]:
"""
Gets representation of nested model.
:param name: name of the field to follow
:type name: str
:param field: ormar field
:type field: BaseField
:param relation_map: dict with relation map
:type relation_map: dict
:return: nested model or list of nested model repr
:rtype: Union[list, dict]
"""
value = generate_model_example(field.to, relation_map=relation_map.get(name, {}))
new_value: Union[list, dict] = [value] if field.is_multi or field.virtual else value
return new_value
def generate_pydantic_example(
pydantic_model: type[pydantic.BaseModel], exclude: Optional[set] = None
) -> dict:
"""
Generates dict with example.
:param pydantic_model: model to parse
:type pydantic_model: type[pydantic.BaseModel]
:param exclude: list of fields to exclude
:type exclude: Optional[set]
:return: dict with fields and sample values
:rtype: dict
"""
example: dict[str, Any] = dict()
exclude = exclude or set()
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.model_fields[name]
type_ = field.annotation
example[name] = get_pydantic_example_repr(type_)
return example
def get_pydantic_example_repr(type_: Any) -> Any:
"""
Gets sample representation of pydantic field for example dict.
:param type_: type of pydantic field
:type type_: 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):
return generate_pydantic_example(pydantic_model=type_)
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__ is 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:
"""
Overwrites the example with properly nested children models.
Overwrites the description if it's taken from ormar.Model.
:param schema: schema of current model
:type schema: dict[str, Any]
:param model: model class
:type model: type["Model"]
"""
schema["example"] = generate_model_example(model=model)
def overwrite_binary_format(schema: dict[str, Any], model: type["Model"]) -> None:
"""
Overwrites format of the field if it's a LargeBinary field with
a flag to represent the field as base64 encoded string.
:param schema: schema of current model
:type schema: dict[str, Any]
:param model: model class
:type model: type["Model"]
"""
for field_id, prop in schema.get("properties", {}).items():
if (
field_id in model._bytes_fields
and model.ormar_config.model_fields[field_id].represent_as_base64_str
):
prop["format"] = "base64"
def construct_schema_function() -> Callable:
"""
Modifies model example and description if needed.
Note that schema extra has to be a function, otherwise it's called to soon
before all the relations are expanded.
: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:
overwrite_example_and_description(schema=schema, model=model)
overwrite_binary_format(schema=schema, model=model)
return staticmethod(schema_extra) # type: ignore
def modify_schema_example(model: type["Model"]) -> None: # noqa CCR001
"""
Modifies the schema example in openapi schema.
:param model: newly constructed Model
:type model: Model class
"""
if not config_field_not_set(model=model, field_name="model_fields"):
model.model_config["json_schema_extra"] = construct_schema_function()
ormar-orm-ormar-a7e7fde/ormar/models/metaclass.py 0000664 0000000 0000000 00000100475 15211567312 0022272 0 ustar 00root root 0000000 0000000 import copy
import dataclasses
import sys
import warnings
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast
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 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,
PydanticDescriptor,
RelationDescriptor,
)
from ormar.models.descriptors.descriptors import BytesDescriptor
from ormar.models.helpers import (
check_required_config_parameters,
config_field_not_set,
expand_reverse_relationships,
extract_annotations_and_default_vals,
get_potential_fields,
merge_or_generate_pydantic_config,
modify_schema_example,
populate_config_sqlalchemy_table_if_required,
populate_config_tablename_columns_and_pk,
populate_default_options_values,
register_relation_in_alias_manager,
sqlalchemy_columns_from_model_fields,
)
from ormar.models.ormar_config import OrmarConfig
from ormar.models.quick_access_views import quick_access_set
from ormar.queryset import FieldAccessor, QuerySet
from ormar.signals import Signal
if TYPE_CHECKING: # pragma no cover
from ormar import Model
from ormar.models import T
CONFIG_KEY = "Config"
PARSED_FIELDS_KEY = "__parsed_fields__"
def add_cached_properties(new_model: type["Model"]) -> None:
"""
Sets cached properties for both pydantic and ormar models.
Quick access fields are fields grabbed in getattribute to skip all checks.
Related fields and names are populated to None as they can change later.
When children models are constructed they can modify parent to register itself.
All properties here are used as "cache" to not recalculate them constantly.
:param new_model: newly constructed Model
:type new_model: Model class
"""
new_model._quick_access_fields = quick_access_set
new_model._related_names = None
new_model._through_names = None
new_model._related_fields = None
new_model._json_fields = set()
new_model._bytes_fields = set()
new_model._onupdate_fields = set()
# Lazy-populated in NewBaseModel._process_kwargs on first init per class.
# ormar_config.extra and ormar_config.model_fields are not finalized at
# this point in class creation, so eager init would be premature.
new_model._pydantic_field_names = None
new_model._extra_is_ignore = None
new_model._allowed_kwarg_names = None
def add_property_fields(new_model: type["Model"], attrs: dict) -> None: # noqa: CCR001
"""
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.
:param new_model: newly constructed model
:type new_model: Model class
:param attrs:
:type attrs: dict[str, str]
"""
props = set()
for var_name, value in attrs.items():
if hasattr(value, "decorator_info") and isinstance(
value.decorator_info, ComputedFieldInfo
):
props.add(var_name)
if config_field_not_set(model=new_model, field_name="property_fields"):
new_model.ormar_config.property_fields = props
else:
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.
Predefined signals are (pre/post) + (save/update/delete).
Signals are emitted in both model own methods and in selected queryset ones.
:param new_model: newly constructed model
:type new_model: Model class
"""
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()
signals.post_save = Signal()
signals.post_update = Signal()
signals.post_delete = Signal()
signals.pre_relation_add = Signal()
signals.post_relation_add = Signal()
signals.pre_relation_remove = Signal()
signals.post_relation_remove = Signal()
signals.post_bulk_update = Signal()
def verify_constraint_names(
base_class: "Model", model_fields: dict, parent_value: list
) -> None:
"""
Verifies if redefined fields that are overwritten in subclasses did not remove
any name of the column that is used in constraint as it will fail in sqlalchemy
Table creation.
:param base_class: one of the parent classes
:type base_class: Model or model parent class
:param model_fields: ormar fields in defined in current class
:type model_fields: dict[str, BaseField]
:param parent_value: list of base class constraints
: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.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:
if any(x not in old_aliases.values() for x in column_set):
raise ModelDefinitionError(
f"Column constraints "
f"{column_set} "
f"has column names "
f"that are not in the model fields."
f"\n Check columns redefined in subclasses "
f"to verify that they have proper 'name' set."
)
def get_constraint_copy(
constraint: ColumnCollectionConstraint,
) -> Union[UniqueColumns, IndexColumns, CheckColumns]:
"""
Copy the constraint and unpacking it's values
:raises ValueError: if non subclass of ColumnCollectionConstraint
:param value: an instance of the ColumnCollectionConstraint class
:type value: Instance of ColumnCollectionConstraint child
:return: copy ColumnCollectionConstraint ormar constraints
:rtype: Union[UniqueColumns, IndexColumns, CheckColumns]
"""
constraints = {
sqlalchemy.UniqueConstraint: lambda x: UniqueColumns(*x._pending_colargs),
sqlalchemy.Index: lambda x: IndexColumns(*x._pending_colargs),
sqlalchemy.CheckConstraint: lambda x: CheckColumns(x.sqltext),
}
checks = (key if isinstance(constraint, key) else None for key in constraints)
target_class = next((target for target in checks if target is not None), None)
constructor: Optional[Callable] = (
constraints.get(target_class) if target_class else None
)
if not constructor:
raise ValueError(f"{constraint} must be a ColumnCollectionMixin!")
return constructor(constraint)
def update_attrs_from_base_config( # noqa: CCR001
base_class: "Model", attrs: dict, model_fields: dict
) -> None:
"""
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
:param attrs: new namespace for class being constructed
:type attrs: dict
:param model_fields: ormar fields in defined in current class
:type model_fields: dict[str, BaseField]
"""
params_to_update = ["metadata", "database", "constraints", "property_fields"]
for param in params_to_update:
current_value = attrs.get("ormar_config", {}).__dict__.get(
param, ormar.Undefined
)
parent_value = (
base_class.ormar_config.__dict__.get(param)
if hasattr(base_class, "ormar_config")
else None
)
if parent_value:
if param == "constraints":
verify_constraint_names(
base_class=base_class,
model_fields=model_fields,
parent_value=parent_value,
)
parent_value = [get_constraint_copy(value) for value in parent_value]
if isinstance(current_value, list):
current_value.extend(parent_value)
else:
setattr(attrs["ormar_config"], param, parent_value)
def copy_and_replace_m2m_through_model( # noqa: CFQ002
field: ManyToManyField,
field_name: str,
table_name: str,
parent_fields: dict,
attrs: dict,
ormar_config: OrmarConfig,
base_class: type["Model"],
) -> None:
"""
Clones class with Through model for m2m relations, appends child name to the name
of the cloned class.
Clones non foreign keys fields from parent model, the same with database columns.
Modifies related_name with appending child table name after '_'
For table name, the table name of child is appended after '_'.
Removes the original sqlalchemy table from metadata if it was not removed.
:param base_class: base class model
:type base_class: type["Model"]
:param field: field with relations definition
:type field: ManyToManyField
:param field_name: name of the relation field
:type field_name: str
:param table_name: name of the table
:type table_name: str
:param parent_fields: dictionary of fields to copy to new models from parent
:type parent_fields: dict
:param attrs: new namespace for class being constructed
:type attrs: dict
:param ormar_config: metaclass of currently created model
:type ormar_config: OrmarConfig
"""
Field: type[BaseField] = type( # type: ignore
field.__class__.__name__, (ManyToManyField, BaseField), {}
)
copy_field = Field(**dict(field.__dict__))
related_name = field.related_name + "_" + table_name
copy_field.related_name = related_name # type: ignore
through_class = field.through
if not through_class:
field.owner = base_class
field.create_default_through_model()
through_class = field.through
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,
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 # type: ignore
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 = 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_config, "table"):
new_config.tablename += "_" + ormar_config.tablename
new_config.table = None # type: ignore
new_config.model_fields = {
name: field
for name, field in new_config.model_fields.items()
if not field.is_relation
}
_, columns = sqlalchemy_columns_from_model_fields(
new_config.model_fields, copy_through
) # type: ignore
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.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
base_class: type["Model"],
curr_class: type,
attrs: dict,
model_fields: dict[str, Union[BaseField, ForeignKeyField, ManyToManyField]],
) -> tuple[dict, dict]:
"""
Copy the key parameters [database, metadata, property_fields and constraints]
and fields from parent models. Overwrites them if needed.
Only abstract classes can be subclassed.
Since relation fields requires different related_name for different children
:raises ModelDefinitionError: if non abstract model is subclassed
:param base_class: one of the parent classes
:type base_class: Model or model parent class
:param curr_class: current constructed class
:type curr_class: Model or model parent class
:param attrs: new namespace for class being constructed
:type attrs: dict
:param model_fields: ormar fields in defined in current class
:type model_fields: dict[str, BaseField]
:return: updated attrs and model_fields
:rtype: tuple[dict, dict]
"""
if attrs.get("ormar_config"):
child_config = attrs["ormar_config"]
child_proxy = getattr(child_config, "proxy", False)
base_abstract = base_class.ormar_config.abstract # type: ignore
if child_proxy:
if base_abstract:
raise ModelDefinitionError(
f"Proxy model {curr_class.__name__} cannot inherit from "
f"abstract class {base_class.__name__}; "
f"use concrete inheritance instead."
)
new_fields = set(model_fields) - set(
base_class.ormar_config.model_fields # type: ignore
)
if new_fields:
raise ModelDefinitionError(
f"Proxy model {curr_class.__name__} cannot declare new ormar "
f"fields: {sorted(new_fields)}."
)
update_attrs_from_base_config(
base_class=base_class, # type: ignore
attrs=attrs,
model_fields=model_fields,
)
return attrs, dict(base_class.ormar_config.model_fields)
if model_fields and not base_abstract:
raise ModelDefinitionError(
f"{curr_class.__name__} cannot inherit "
f"from non abstract class {base_class.__name__}"
)
update_attrs_from_base_config(
base_class=base_class, # type: ignore
attrs=attrs,
model_fields=model_fields,
)
parent_fields: dict = dict()
ormar_config = attrs.get("ormar_config")
if not ormar_config: # pragma: no cover
raise ModelDefinitionError(
f"Model {curr_class.__name__} declared without ormar_config"
)
table_name = (
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.ormar_config.model_fields.items():
if field.is_multi:
field = cast(ManyToManyField, field)
copy_and_replace_m2m_through_model(
field=field,
field_name=field_name,
table_name=table_name,
parent_fields=parent_fields,
attrs=attrs,
ormar_config=ormar_config,
base_class=base_class, # type: ignore
)
elif field.is_relation and (
field.related_name or cast(ForeignKeyField, field).foreign_key_name
):
fk_field = cast(ForeignKeyField, field)
Field = type( # type: ignore
field.__class__.__name__, (ForeignKeyField, BaseField), {}
)
copy_field = Field(**dict(field.__dict__))
if fk_field.related_name:
related_name = fk_field.related_name + "_" + table_name
copy_field.related_name = related_name # type: ignore
if fk_field.foreign_key_name:
new_fk_name = f"{fk_field.foreign_key_name}_{table_name}"
copy_field.foreign_key_name = new_fk_name # type: ignore
copy_field.constraints = [
dataclasses.replace(constraint, name=new_fk_name)
for constraint in fk_field.constraints
]
parent_fields[field_name] = copy_field
else:
parent_fields[field_name] = field
parent_fields.update(model_fields) # type: ignore
model_fields = parent_fields
return attrs, model_fields
def extract_from_parents_definition( # noqa: CCR001
base_class: type,
curr_class: type,
attrs: dict,
model_fields: dict[str, Union[BaseField, ForeignKeyField, ManyToManyField]],
) -> tuple[dict, dict]:
"""
Extracts fields from base classes if they have valid ormar fields.
If model was already parsed -> fields definitions need to be removed from class
cause pydantic complains about field re-definition so after first child
we need to extract from __parsed_fields__ not the class itself.
If the class is parsed first time annotations and field definition is parsed
from the class.__dict__.
If the class is a ormar.Model it is skipped.
:param base_class: one of the parent classes
:type base_class: Model or model parent class
:param curr_class: current constructed class
:type curr_class: Model or model parent class
:param attrs: new namespace for class being constructed
:type attrs: dict
:param model_fields: ormar fields in defined in current class
:type model_fields: dict[str, BaseField]
:return: updated attrs and model_fields
:rtype: tuple[dict, dict]
"""
if hasattr(base_class, "ormar_config"):
base_class = cast(type["Model"], base_class)
return copy_data_from_parent_model(
base_class=base_class,
curr_class=curr_class,
attrs=attrs,
model_fields=model_fields,
)
key = "__annotations__"
if hasattr(base_class, PARSED_FIELDS_KEY):
# model was already parsed -> fields definitions need to be removed from class
# cause pydantic complains about field re-definition so after first child
# we need to extract from __parsed_fields__ not the class itself
new_attrs, new_model_fields = getattr(base_class, PARSED_FIELDS_KEY)
new_fields = set(new_model_fields.keys())
model_fields = update_attrs_and_fields(
attrs=attrs,
new_attrs=new_attrs,
model_fields=model_fields,
new_model_fields=new_model_fields,
new_fields=new_fields,
)
return attrs, model_fields
potential_fields = get_potential_fields(base_class.__dict__)
if potential_fields:
# parent model has ormar fields defined and was not parsed before
new_attrs = {key: {k: v for k, v in base_class.__dict__.get(key, {}).items()}}
new_attrs.update(potential_fields)
new_fields = set(potential_fields.keys())
for name in new_fields:
delattr(base_class, name)
new_attrs, new_model_fields = extract_annotations_and_default_vals(new_attrs)
setattr(base_class, PARSED_FIELDS_KEY, (new_attrs, new_model_fields))
model_fields = update_attrs_and_fields(
attrs=attrs,
new_attrs=new_attrs,
model_fields=model_fields,
new_model_fields=new_model_fields,
new_fields=new_fields,
)
return attrs, model_fields
def update_attrs_and_fields(
attrs: dict,
new_attrs: dict,
model_fields: dict,
new_model_fields: dict,
new_fields: set,
) -> dict:
"""
Updates __annotations__, values of model fields (so pydantic FieldInfos)
as well as model.ormar_config.model_fields definitions from parents.
:param attrs: new namespace for class being constructed
:type attrs: dict
:param new_attrs: related of the namespace extracted from parent class
:type new_attrs: dict
:param model_fields: ormar fields in defined in current class
:type model_fields: dict[str, BaseField]
:param new_model_fields: ormar fields defined in parent classes
:type new_model_fields: dict[str, BaseField]
:param new_fields: set of new fields names
:type new_fields: set[str]
"""
key = "__annotations__"
attrs[key].update(new_attrs[key])
attrs.update({name: new_attrs[name] for name in new_fields})
updated_model_fields = {k: v for k, v in new_model_fields.items()}
updated_model_fields.update(model_fields)
return updated_model_fields
def wire_proxy_from_parent(new_model: type["Model"]) -> None:
"""
Resolve the concrete ormar parent of a proxy model and share its table,
columns, primary key, model_fields, metadata and database with the proxy.
Proxy models do not own a SQLAlchemy table. They reuse the parent's table
so that queries via the proxy class hit the same physical rows.
:raises ModelDefinitionError: if the proxy has no concrete ormar parent or
if it inherits from multiple concrete ormar models with different tables.
:param new_model: the proxy model class being constructed
:type new_model: type["Model"]
"""
concrete_bases = [
base
for base in new_model.__mro__[1:]
if hasattr(base, "ormar_config")
and not base.ormar_config.abstract
and base is not new_model
]
if not concrete_bases:
raise ModelDefinitionError(
f"Proxy model {new_model.__name__} has no concrete ormar parent."
)
primary_table = concrete_bases[0].ormar_config.table
for other in concrete_bases[1:]:
if other.ormar_config.table is not primary_table:
raise ModelDefinitionError(
f"Proxy model {new_model.__name__} cannot inherit from multiple "
f"concrete ormar models with different tables."
)
parent_cfg = concrete_bases[0].ormar_config
cfg = new_model.ormar_config
cfg.table = parent_cfg.table
cfg.tablename = parent_cfg.tablename
cfg.pkname = parent_cfg.pkname
cfg.columns = parent_cfg.columns
cfg.model_fields = parent_cfg.model_fields
cfg.metadata = parent_cfg.metadata
cfg.database = parent_cfg.database
cfg.alias_manager = parent_cfg.alias_manager
new_model.pk = PkDescriptor(name=parent_cfg.pkname)
def add_field_descriptor(
name: str, field: "BaseField", new_model: type["Model"]
) -> None:
"""
Sets appropriate descriptor for each model field.
There are 5 main types of descriptors, for bytes, json, pure pydantic fields,
and 2 ormar ones - one for relation and one for pk shortcut
:param name: name of the field
:type name: str
:param field: model field to add descriptor for
:type field: BaseField
:param new_model: model with fields
:type new_model: type["Model]
"""
if field.is_relation:
setattr(new_model, name, RelationDescriptor(name=name))
elif field.__type__ == pydantic.Json:
setattr(new_model, name, JsonDescriptor(name=name))
elif field.__type__ is bytes:
setattr(new_model, name, BytesDescriptor(name=name))
else:
setattr(new_model, name, PydanticDescriptor(name=name))
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:
try:
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore", message="Pydantic serializer warnings"
)
return handler(value)
except ValueError as exc:
if not str(exc).startswith("Circular reference"):
raise exc
# Pydantic >= 2.13 dispatches the wrap serializer with
# broader value types (e.g. QuerysetProxy for M2M reverse
# accessors) where 2.12 unwrapped to Model first. Earlier
# pydantic versions never reach this branch with a non-
# Model value, so the None return is uncovered on the
# 2.11 floor pinned in CI.
if value is None or not hasattr(value, "ormar_config"):
return None # pragma: no cover
return {value.ormar_config.pkname: value.pk}
except ReferenceError:
return None
return serialize
class ModelMetaclass(pydantic._internal._model_construction.ModelMetaclass):
def __new__( # type: ignore # noqa: CCR001
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.
Sets pydantic configuration.
Extract model_fields and convert them to pydantic FieldInfo,
updates class namespace.
Extracts settings and fields from parent classes.
Fetches methods decorated with @computed_field decorator
to expose them later in dict().
Construct parent pydantic Metaclass/ Model.
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
:param bases: base classes
:type bases: tuple
:param attrs: class namespace
:type attrs: dict
"""
merge_or_generate_pydantic_config(attrs=attrs, name=name)
attrs["__name__"] = name
attrs, model_fields = extract_annotations_and_default_vals(attrs)
for base in reversed(bases):
mod = base.__module__
if mod.startswith("ormar.models.") or mod.startswith("pydantic."):
continue
attrs, model_fields = extract_from_parents_definition(
base_class=base, curr_class=mcs, attrs=attrs, model_fields=model_fields
)
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, "ormar_config"):
populate_default_options_values(new_model, model_fields)
check_required_config_parameters(new_model)
add_property_fields(new_model, attrs)
register_signals(new_model=new_model)
modify_schema_example(model=new_model)
if new_model.ormar_config.proxy:
wire_proxy_from_parent(new_model)
elif 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)
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.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.ormar_config.pkname
new_model.model_fields[field_name] = (
FieldInfo.from_annotated_attribute(
Optional[int], # type: ignore
None,
)
)
new_model.pk = PkDescriptor(name=new_model.ormar_config.pkname)
if not (
new_model.ormar_config.abstract
or new_model.ormar_config.proxy
or new_model.ormar_config.requires_ref_update
):
# Pydantic >= 2.13 changed how TypeAdapter(Annotated[T,
# FieldInfo(...)]) resolves nested schemas — it now uses
# the cached child snapshot instead of walking the live
# model. Reverse-relation fields added by
# expand_reverse_relationships above become invisible
# from nested dumps unless we invalidate the cached
# schema. Mark incomplete so the next access (or the
# FastAPI-style TypeAdapter wrapper) recompiles afresh
# without the stale snapshot.
new_model.__pydantic_complete__ = False # type: ignore[attr-defined]
return new_model
@property
def objects(cls: type["T"]) -> "QuerySet[T]": # type: ignore
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.ormar_config.queryset_class(model_cls=cls)
def __getattr__(self, item: str) -> Any:
"""
Returns FieldAccessors on access to model fields from a class,
that way it can be used in python style filters and order_by.
:param item: name of the field
:type item: str
: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.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),
model=field.to,
access_chain=item,
)
return FieldAccessor(
source_model=cast(type["Model"], self), field=field, access_chain=item
)
return object.__getattribute__(self, item)
ormar-orm-ormar-a7e7fde/ormar/models/mixins/ 0000775 0000000 0000000 00000000000 15211567312 0021244 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/models/mixins/__init__.py 0000664 0000000 0000000 00000001264 15211567312 0023360 0 ustar 00root root 0000000 0000000 """
Package contains functionalities divided by features.
All mixins are combined into ModelTableProxy which is one of the parents of Model.
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
from ormar.models.mixins.pydantic_mixin import PydanticMixin
from ormar.models.mixins.save_mixin import SavePrepareMixin
__all__ = [
"MergeModelMixin",
"AliasMixin",
"SavePrepareMixin",
"ExcludableMixin",
"PydanticMixin",
]
ormar-orm-ormar-a7e7fde/ormar/models/mixins/alias_mixin.py 0000664 0000000 0000000 00000007734 15211567312 0024126 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING
import ormar_rust_utils
class AliasMixin:
"""
Used to translate field names into database column names.
"""
if TYPE_CHECKING: # pragma: no cover
from ormar.models.ormar_config import OrmarConfig
ormar_config: OrmarConfig
_alias_to_field_map: dict[str, str]
_field_to_alias_map: dict[str, str]
@classmethod
def _build_alias_cache(cls) -> None:
"""
Build and cache alias mappings for this model class.
Builds two dicts:
- _field_to_alias_map: field_name -> db_alias
- _alias_to_field_map: db_alias -> field_name (reverse)
"""
field_to_alias = {}
for field_name, field in cls.ormar_config.model_fields.items():
alias = field.get_alias()
if alias:
field_to_alias[field_name] = alias
cls._field_to_alias_map = field_to_alias
cls._alias_to_field_map = ormar_rust_utils.build_reverse_alias_map(
field_to_alias
)
@classmethod
def get_column_alias(cls, field_name: str) -> str:
"""
Returns db alias (column name in db) for given ormar field.
For fields without alias field name is returned.
:param field_name: name of the field to get alias from
:type field_name: str
:return: alias (db name) if set, otherwise passed name
:rtype: str
"""
try:
return cls._field_to_alias_map.get(field_name, field_name)
except AttributeError:
cls._build_alias_cache()
return cls._field_to_alias_map.get(field_name, field_name)
@classmethod
def get_column_name_from_alias(cls, alias: str) -> str:
"""
Returns ormar field name for given db alias (column name in db).
If field do not have alias it's returned as is.
:param alias:
:type alias: str
:return: field name if set, otherwise passed alias (db name)
:rtype: str
"""
try:
return cls._alias_to_field_map.get(alias, alias)
except AttributeError:
cls._build_alias_cache()
return cls._alias_to_field_map.get(alias, alias)
@classmethod
def translate_columns_to_aliases(cls, new_kwargs: dict) -> dict:
"""
Translates dictionary of model fields changing field names into aliases.
If field has no alias the field name remains intact.
Only fields present in the dictionary are translated.
:param new_kwargs: dict with fields names and their values
:type new_kwargs: dict
:return: dict with aliases and their values
:rtype: dict
"""
try:
field_to_alias = cls._field_to_alias_map
except AttributeError:
cls._build_alias_cache()
field_to_alias = cls._field_to_alias_map
for field_name in list(new_kwargs.keys()):
alias = field_to_alias.get(field_name)
if alias and alias != field_name:
new_kwargs[alias] = new_kwargs.pop(field_name)
return new_kwargs
@classmethod
def translate_aliases_to_columns(cls, new_kwargs: dict) -> dict:
"""
Translates dictionary of model fields changing aliases into field names.
If field has no alias the alias is already a field name.
Only fields present in the dictionary are translated.
:param new_kwargs: dict with aliases and their values
:type new_kwargs: dict
:return: dict with fields names and their values
:rtype: dict
"""
try:
alias_to_field = cls._alias_to_field_map
except AttributeError: # pragma: nocover
cls._build_alias_cache()
alias_to_field = cls._alias_to_field_map
for key in list(new_kwargs.keys()):
field_name = alias_to_field.get(key)
if field_name and field_name != key:
new_kwargs[field_name] = new_kwargs.pop(key)
return new_kwargs
ormar-orm-ormar-a7e7fde/ormar/models/mixins/excludable_mixin.py 0000664 0000000 0000000 00000017603 15211567312 0025141 0 ustar 00root root 0000000 0000000 from typing import (
TYPE_CHECKING,
AbstractSet,
Any,
Mapping,
Union,
cast,
)
from ormar.models.excludable import ExcludableItems
from ormar.models.mixins.relation_mixin import RelationMixin
if TYPE_CHECKING: # pragma no cover
from ormar import Model
IntStr = Union[int, str]
AbstractSetIntStr = AbstractSet[IntStr]
MappingIntStrAny = Mapping[IntStr, Any]
class ExcludableMixin(RelationMixin):
"""
Used to include/exclude given set of fields on models during load and dict() calls.
"""
if TYPE_CHECKING: # pragma: no cover
from ormar import Model
from ormar.models import ModelRow
@staticmethod
def _populate_pk_column(
model: Union[type["Model"], type["ModelRow"]],
columns: list[str],
use_alias: bool = False,
) -> list[str]:
"""
Adds primary key column/alias (depends on use_alias flag) to list of
column names that are selected.
:param model: model on columns are selected
:type model: type["Model"]
:param columns: list of columns names
:type columns: list[str]
:param use_alias: flag to set if aliases or field names should be used
:type use_alias: bool
:return: list of columns names with pk column in it
:rtype: list[str]
"""
pk_alias = (
model.get_column_alias(model.ormar_config.pkname)
if use_alias
else model.ormar_config.pkname
)
if pk_alias not in columns:
columns.append(pk_alias)
return columns
@staticmethod
def _get_table_column_pairs(
model: Union[type["Model"], type["ModelRow"]],
) -> list[tuple[str, str]]:
"""
Returns cached list of (col_name, field_name) tuples for the model's table.
Built once per model class and cached to avoid repeated SA column iteration.
:param model: model to get column pairs for
:type model: type["Model"]
:return: list of (column_name, field_name) tuples
:rtype: list[tuple[str, str]]
"""
cached = getattr(model, "_table_column_pairs", None)
if cached is not None:
return cached
pairs = [
(col.name, model.get_column_name_from_alias(col.name))
for col in model.ormar_config.table.columns
]
model._table_column_pairs = pairs # type: ignore[union-attr]
return pairs
@classmethod
def own_table_columns(
cls,
model: Union[type["Model"], type["ModelRow"]],
excludable: ExcludableItems,
alias: str = "",
use_alias: bool = False,
add_pk_columns: bool = True,
) -> list[str]:
"""
Returns list of aliases or field names for given model.
Aliases/names switch is use_alias flag.
If provided only fields included in fields will be returned.
If provided fields in exclude_fields will be excluded in return.
Primary key field is always added and cannot be excluded (will be added anyway).
:param add_pk_columns: flag if add primary key - always yes if ormar parses data
:type add_pk_columns: bool
:param alias: relation prefix
:type alias: str
:param excludable: structure of fields to include and exclude
:type excludable: ExcludableItems
:param model: model on columns are selected
:type model: type["Model"]
:param use_alias: flag if aliases or field names should be used
:type use_alias: bool
:return: list of column field names or aliases
:rtype: list[str]
"""
model_excludable = excludable.get(model_cls=model, alias=alias) # type: ignore
if model_excludable.pk_only:
if not add_pk_columns:
return []
return cls._populate_pk_column(model=model, columns=[], use_alias=use_alias)
has_include = bool(model_excludable.include)
has_exclude = bool(model_excludable.exclude)
column_pairs = cls._get_table_column_pairs(model)
columns = []
for col_name, field_name in column_pairs:
if has_include and not model_excludable.is_included(field_name):
continue
if has_exclude and model_excludable.is_excluded(field_name):
continue
columns.append(col_name if use_alias else field_name)
# always has to return pk column for ormar to work
if add_pk_columns:
columns = cls._populate_pk_column(
model=model, columns=columns, use_alias=use_alias
)
return columns
@classmethod
def _update_excluded_with_related(cls, exclude: Union[set, dict, None]) -> set:
"""
Used during generation of the dict().
To avoid cyclical references and max recurrence limit nested models have to
exclude related models that are not mandatory.
For a main model (not nested) only nullable related field names are added to
exclusion, for nested models all related models are excluded.
:param exclude: set/dict with fields to exclude
:type exclude: Union[set, dict, None]
:return: set or dict with excluded fields added.
:rtype: Union[set, dict]
"""
exclude = exclude or set()
related_set = cls.extract_related_names()
if isinstance(exclude, set):
exclude = {s for s in exclude}
exclude = exclude.union(related_set)
elif isinstance(exclude, dict):
# relations are handled in ormar - take only own fields (ellipsis in dict)
exclude = {k for k, v in exclude.items() if v is Ellipsis}
exclude = exclude.union(related_set)
return exclude
@classmethod
def _update_excluded_with_pks_and_through(
cls, exclude: set, exclude_primary_keys: bool, exclude_through_models: bool
) -> set:
"""
Updates excluded names with name of pk column if exclude flag is set.
:param exclude: set of names to exclude
:type exclude: set
:param exclude_primary_keys: flag if the primary keys should be excluded
:type exclude_primary_keys: bool
:return: set updated with pk if flag is set
:rtype: set
"""
if exclude_primary_keys:
exclude.add(cls.ormar_config.pkname)
if exclude_through_models:
exclude = exclude.union(cls.extract_through_names())
return exclude
@classmethod
def get_names_to_exclude(cls, excludable: ExcludableItems, alias: str) -> set:
"""
Returns a set of models field names that should be explicitly excluded
during model initialization.
Those fields will be set to None to avoid ormar/pydantic setting default
values on them. They should be returned as None in any case.
Used in parsing data from database rows that construct Models by initializing
them with dicts constructed from those db rows.
:param alias: alias of current relation
:type alias: str
:param excludable: structure of fields to include and exclude
:type excludable: ExcludableItems
:return: set of field names that should be excluded
:rtype: set
"""
model = cast(type["Model"], cls)
model_excludable = excludable.get(model_cls=model, alias=alias)
fields_names = cls.extract_db_own_fields()
if model_excludable.include:
fields_to_keep = model_excludable.include.intersection(fields_names)
else:
fields_to_keep = fields_names
fields_to_exclude = fields_names - fields_to_keep
if model_excludable.exclude:
fields_to_exclude = fields_to_exclude.union(
model_excludable.exclude.intersection(fields_names)
)
fields_to_exclude = fields_to_exclude - {cls.ormar_config.pkname}
return fields_to_exclude
ormar-orm-ormar-a7e7fde/ormar/models/mixins/merge_mixin.py 0000664 0000000 0000000 00000017203 15211567312 0024124 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Optional, cast
import ormar_rust_utils
import ormar
from ormar.models.excludable import skip_ellipsis
from ormar.queryset.utils import translate_list_to_dict
if TYPE_CHECKING: # pragma no cover
from ormar import Model
from ormar.models.excludable import ExcludableItems
class MergeModelMixin:
"""
Used to merge models instances returned by database,
but already initialized to ormar Models.keys
Models can duplicate during joins when parent model has multiple child rows,
in the end all parent (main) models should be unique.
"""
@classmethod
def _recursive_add(cls, model_group: list["Model"]) -> list["Model"]:
"""
Instead of accumulating the model additions one by one, this recursively adds
the models. E.G.
[1, 2, 3, 4].accumulate_add() would give [3, 3, 4], then [6, 4], then [10]
where this method looks like
[1, 2, 3, 4].recursive_add() gives [[3], [7]], [10]
It's the same number of adds, but it gives better O(N) performance on sublists
"""
if len(model_group) <= 1:
return model_group
added_values = []
iterable_group = iter(model_group)
for model in iterable_group:
next_model = next(iterable_group, None)
if next_model is not None:
combined = cls.merge_two_instances(next_model, model)
else:
combined = model
added_values.append(combined)
return cls._recursive_add(added_values)
@classmethod
def merge_instances_list(
cls,
result_rows: list["Model"],
*,
excludable: "ExcludableItems",
) -> list["Model"]:
"""
Merges a list of models into list of unique models.
Models can duplicate during joins when parent model has multiple child rows,
in the end all parent (main) models should be unique.
Each merged top-level model also has the queryset's ``ExcludableItems``
attached via the ``__ormar_excludable__`` slot so a bare ``model_dump()``
on the result can resolve the (lazily cached) flatten map without going
back through the queryset.
:param result_rows: list of already initialized Models with child models
populated, each instance is one row in db and some models can duplicate
:type result_rows: list["Model"]
:param excludable: ExcludableItems to attach to every merged top-level
instance via the ``__ormar_excludable__`` slot
:type excludable: ExcludableItems
:return: list of merged models where each main model is unique
:rtype: list["Model"]
"""
merged_rows: list["Model"] = []
if result_rows:
pks = [model.pk for model in result_rows]
index_groups = ormar_rust_utils.group_by_pk(pks)
for group_indices in index_groups:
# Single-row groups are the common case for queries with no
# parent duplication (``Model.objects.all()`` and similar);
# skip the wrapper list and the no-op ``_recursive_add`` call.
if len(group_indices) == 1:
model = result_rows[group_indices[0]]
else:
model = cls._recursive_add([result_rows[i] for i in group_indices])[
0
]
object.__setattr__(model, "__ormar_excludable__", excludable)
merged_rows.append(model)
return merged_rows
@classmethod
def merge_two_instances(
cls, one: "Model", other: "Model", relation_map: Optional[dict] = None
) -> "Model":
"""
Merges current (other) Model and previous one (one) and returns the current
Model instance with data merged from previous one.
If needed it's calling itself recurrently and merges also children models.
:param relation_map: map of models relations to follow
:type relation_map: dict
:param one: previous model instance
:type one: Model
:param other: current model instance
:type other: Model
:return: current Model instance with data merged from previous one.
:rtype: Model
"""
relation_map = (
relation_map
if relation_map is not None
else translate_list_to_dict(one._iterate_related_models())
)
for field_name in relation_map:
current_field = getattr(one, field_name)
other_value = getattr(other, field_name, [])
if isinstance(current_field, list):
value_to_set = cls._merge_items_lists(
field_name=field_name,
current_field=current_field,
other_value=other_value,
relation_map=relation_map,
)
setattr(other, field_name, value_to_set)
elif (
isinstance(current_field, ormar.Model)
and isinstance(other_value, ormar.Model)
and current_field.pk == other_value.pk
):
setattr(
other,
field_name,
cls.merge_two_instances(
current_field,
other_value,
relation_map=cast(
Optional[dict],
skip_ellipsis(relation_map, field_name, default=dict()),
),
),
)
other.set_save_status(True)
return other
@classmethod
def _merge_items_lists(
cls,
field_name: str,
current_field: list,
other_value: list,
relation_map: Optional[dict],
) -> list:
"""
Takes two list of nested models and process them going deeper
according with the map.
If model from one's list is in other -> they are merged with relations
to follow passed from map.
If one's model is not in other it's simply appended to the list.
:param field_name: name of the current relation field
:type field_name: str
:param current_field: list of nested models from one model
:type current_field: list[Model]
:param other_value: list of nested models from other model
:type other_value: list[Model]
:param relation_map: map of relations to follow
:type relation_map: dict
:return: merged list of models
:rtype: list[Model]
"""
current_pks = [getattr(m, "pk", None) for m in current_field]
other_pks = [getattr(m, "pk", None) for m in other_value]
plan = ormar_rust_utils.plan_merge_items_lists(current_pks, other_pks)
value_to_set = list(other_value)
nested_relation_map = cast(
Optional[dict],
skip_ellipsis(relation_map, field_name, default=dict()),
)
for cur_idx, other_idx in plan:
cur_item = current_field[cur_idx]
if other_idx is not None:
# ``other_idx`` is the destination slot the Rust planner
# already identified — write the merged instance there in
# place rather than rebuilding ``value_to_set`` with a pk
# filter (which was O(N) per match).
value_to_set[other_idx] = cls.merge_two_instances(
cur_item,
cast("Model", other_value[other_idx]),
relation_map=nested_relation_map,
)
else:
value_to_set.append(cur_item)
return value_to_set
ormar-orm-ormar-a7e7fde/ormar/models/mixins/pydantic_mixin.py 0000664 0000000 0000000 00000017720 15211567312 0024644 0 ustar 00root root 0000000 0000000 import copy
import string
from random import choices
from typing import TYPE_CHECKING, Any, Optional, Union, cast
import pydantic
from pydantic import BaseModel
from pydantic._internal._decorators import DecoratorInfos
from ormar.fields import BaseField, ForeignKeyField, ManyToManyField
from ormar.models.excludable import (
filter_not_excluded_fields,
normalize_to_dict,
skip_ellipsis,
)
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
__pydantic_decorators__: DecoratorInfos
@classmethod
def get_pydantic(
cls,
*,
include: Union[set, dict, None] = None,
exclude: Union[set, dict, None] = None,
) -> type[pydantic.BaseModel]:
"""
Returns a pydantic model out of ormar model.
Converts also nested ormar models into pydantic models.
Can be used to fully exclude certain fields in fastapi response and requests.
:param include: fields of own and nested models to include
:type include: Union[set, dict, None]
:param exclude: fields of own and nested models to exclude
:type exclude: Union[set, dict, None]
"""
relation_map = translate_list_to_dict(cls._iterate_related_models())
return cls._convert_ormar_to_pydantic(
include=include, exclude=exclude, relation_map=relation_map
)
@classmethod
def _convert_ormar_to_pydantic(
cls,
relation_map: dict[str, Any],
include: Union[set, dict, None] = None,
exclude: Union[set, dict, None] = None,
) -> type[pydantic.BaseModel]:
include = normalize_to_dict(include)
exclude = normalize_to_dict(exclude)
fields_dict: dict[str, Any] = dict()
defaults: dict[str, Any] = dict()
fields_to_process = filter_not_excluded_fields(
fields={*cls.ormar_config.model_fields.keys()},
include=include,
exclude=exclude,
)
fields_to_process.sort(
key=lambda x: list(cls.ormar_config.model_fields.keys()).index(x)
)
cache_key = f"{cls.__name__}_{str(include)}_{str(exclude)}"
if cache_key in cls.__cache__:
return cls.__cache__[cache_key]
for name in fields_to_process:
field = cls._determine_pydantic_field_type(
name=name,
defaults=defaults,
include=include,
exclude=exclude,
relation_map=relation_map,
)
if field is not None:
fields_dict[name] = field
model = type(
f"{cls.__name__}_{''.join(choices(string.ascii_uppercase, k=3))}",
(pydantic.BaseModel,),
{"__annotations__": fields_dict, **defaults},
)
model = cast(type[pydantic.BaseModel], model)
cls._copy_computed_fields(model=model, include=include, exclude=exclude)
cls._copy_field_validators(model=model)
cls.__cache__[cache_key] = model
return model
@classmethod
def _determine_pydantic_field_type(
cls,
name: str,
defaults: dict,
include: Union[set, dict, None],
exclude: Union[set, dict, None],
relation_map: dict[str, Any],
) -> Any:
field = cls.ormar_config.model_fields[name]
target: Any = None
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,
)
elif not field.is_relation:
defaults[name] = cls.model_fields[name].default # type: ignore
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=skip_ellipsis(include, name),
exclude=skip_ellipsis(exclude, name),
relation_map=cast(
dict[str, Any], skip_ellipsis(relation_map, name, default=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_computed_fields(
cls,
model: type[pydantic.BaseModel],
include: Optional[dict],
exclude: Optional[dict],
) -> None:
"""
Copy ``@pydantic.computed_field`` decorated methods from the ormar model
onto the generated pydantic model so they are exposed in the schema and
serialization output. Computed fields are filtered with the same
include/exclude rules as regular fields.
:param model: pydantic model generated from the ormar model
:type model: type[pydantic.BaseModel]
:param include: fields of own and nested models to include
:type include: Optional[dict]
:param exclude: fields of own and nested models to exclude
:type exclude: Optional[dict]
"""
computed_fields = cls.__pydantic_decorators__.computed_fields
if not computed_fields:
return
names_to_copy = filter_not_excluded_fields(
fields=set(computed_fields.keys()),
include=include,
exclude=exclude,
)
for name in names_to_copy:
decorator = computed_fields[name]
setattr(model, name, decorator.info.wrapped_property)
model.__pydantic_decorators__.computed_fields[name] = copy.copy(decorator)
@classmethod
def _copy_field_validators(cls, model: type[pydantic.BaseModel]) -> None:
"""
Copy field validators from ormar model to generated pydantic model.
"""
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
)
ormar-orm-ormar-a7e7fde/ormar/models/mixins/relation_mixin.py 0000664 0000000 0000000 00000015360 15211567312 0024644 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Callable, Optional, cast
from ormar import BaseField, ForeignKeyField
from ormar.models.traversible import NodeList
class RelationMixin:
"""
Used to return relation fields/names etc. from given model
"""
if TYPE_CHECKING: # pragma no cover
from ormar.models.ormar_config import OrmarConfig
ormar_config: OrmarConfig
__relation_map__: Optional[list[str]]
_related_names: Optional[set]
_through_names: Optional[set]
_related_fields: Optional[list]
get_name: Callable
@classmethod
def extract_db_own_fields(cls) -> set:
"""
Returns only fields that are stored in the own database table, exclude all
related fields.
:return: set of model fields with relation fields excluded
:rtype: set
"""
try:
return cls._db_own_fields # type: ignore[attr-defined]
except AttributeError:
related_names = cls.extract_related_names()
self_fields = {
name
for name in cls.ormar_config.model_fields.keys()
if name not in related_names
}
cls._db_own_fields = self_fields # type: ignore[attr-defined]
return self_fields
@classmethod
def extract_related_fields(cls) -> list["ForeignKeyField"]:
"""
Returns list of ormar Fields for all relations declared on a model.
List is cached in cls._related_fields for quicker access.
:return: list of related fields
:rtype: list
"""
if cls._related_fields is not None:
return cls._related_fields
related_fields = []
for name in cls.extract_related_names().union(cls.extract_through_names()):
related_fields.append(
cast("ForeignKeyField", cls.ormar_config.model_fields[name])
)
cls._related_fields = related_fields
return related_fields
@classmethod
def extract_through_names(cls) -> set[str]:
"""
Extracts related fields through names which are shortcuts to through models.
:return: set of related through fields names
:rtype: set
"""
if cls._through_names is not None:
return cls._through_names
related_names = set()
for name, field in cls.ormar_config.model_fields.items():
if isinstance(field, BaseField) and field.is_through:
related_names.add(name)
cls._through_names = related_names
return related_names
@classmethod
def extract_related_names(cls) -> set[str]:
"""
Returns list of fields names for all relations declared on a model.
List is cached in cls._related_names for quicker access.
:return: set of related fields names
:rtype: set
"""
if cls._related_names is not None:
return cls._related_names
related_names = set()
for name, field in cls.ormar_config.model_fields.items():
if (
isinstance(field, BaseField)
and field.is_relation
and not field.is_through
and not field.skip_field
):
related_names.add(name)
cls._related_names = related_names
return related_names
@classmethod
def _extract_db_related_names(cls) -> set:
"""
Returns only fields that are stored in the own database table, exclude
related fields that are not stored as foreign keys on given model.
:return: set of model fields with non fk relation fields excluded
:rtype: set
"""
related_names = cls.extract_related_names()
related_names = {
name
for name in related_names
if cls.ormar_config.model_fields[name].is_valid_uni_relation()
}
return related_names
@classmethod
def _iterate_related_models( # noqa: CCR001
cls,
node_list: Optional[NodeList] = None,
parsed_map: Optional[dict] = None,
source_relation: Optional[str] = None,
recurrent: bool = False,
) -> list[str]:
"""
Iterates related models recursively to extract relation strings of
nested not visited models.
:return: list of relation strings to be passed to select_related
:rtype: list[str]
"""
if not node_list:
if cls.__relation_map__:
return cls.__relation_map__
node_list = NodeList()
parsed_map = dict()
current_node = node_list.add(node_class=cls)
else:
current_node = node_list[-1]
relations = sorted(cls.extract_related_names())
processed_relations: list[str] = []
for relation in relations:
if not current_node.visited(relation):
target_model = cls.ormar_config.model_fields[relation].to
node_list.add(
node_class=target_model,
relation_name=relation,
parent_node=current_node,
)
relation_key = f"{cls.get_name()}_{relation}"
parsed_map = cast(dict, parsed_map)
deep_relations = parsed_map.get(relation_key)
if not deep_relations:
deep_relations = target_model._iterate_related_models(
source_relation=relation,
node_list=node_list,
recurrent=True,
parsed_map=parsed_map,
)
parsed_map[relation_key] = deep_relations
processed_relations.extend(deep_relations)
result = cls._get_final_relations(processed_relations, source_relation)
if not recurrent:
cls.__relation_map__ = result
return result
@staticmethod
def _get_final_relations(
processed_relations: list, source_relation: Optional[str]
) -> list[str]:
"""
Helper method to prefix nested relation strings with current source relation
:param processed_relations: list of already processed relation str
:type processed_relations: list[str]
:param source_relation: name of the current relation
:type source_relation: str
:return: list of relation strings to be passed to select_related
:rtype: list[str]
"""
if processed_relations:
final_relations = [
f"{source_relation + '__' if source_relation else ''}{relation}"
for relation in processed_relations
]
else:
final_relations = [source_relation] if source_relation else []
return final_relations
ormar-orm-ormar-a7e7fde/ormar/models/mixins/save_mixin.py 0000664 0000000 0000000 00000045211 15211567312 0023763 0 ustar 00root root 0000000 0000000 import base64
import uuid
from enum import Enum
from typing import TYPE_CHECKING, Any, Callable, Collection, Optional, Union, cast
from pydantic.plugin._schema_validator import (
PluggableSchemaValidator,
create_schema_validator,
)
from pydantic_core import CoreSchema, SchemaValidator
import ormar # noqa: I100, I202
from ormar.exceptions import ModelPersistenceError
from ormar.fields.parsers import encode_json
from ormar.models.excludable import skip_ellipsis
from ormar.models.mixins import AliasMixin
from ormar.models.mixins.relation_mixin import RelationMixin
if TYPE_CHECKING: # pragma: no cover
from ormar import ForeignKeyField, Model
class SavePrepareMixin(RelationMixin, AliasMixin):
"""
Used to prepare models to be saved in database
"""
if TYPE_CHECKING: # pragma: nocover
_json_fields: set[str]
_bytes_fields: set[str]
_onupdate_fields: set[str]
__pydantic_core_schema__: CoreSchema
__ormar_fields_validators__: Optional[
dict[str, Union[SchemaValidator, PluggableSchemaValidator]]
]
@classmethod
def prepare_model_to_save(cls, new_kwargs: dict) -> dict:
"""
Combines all preparation methods before saving.
Removes primary key for if it's nullable or autoincrement pk field,
and it's set to None.
Substitute related models with their primary key values as fk column.
Populates the default values for field with default set and no value.
Translate columns into aliases (db names).
:param new_kwargs: dictionary of model that is about to be saved
:type new_kwargs: dict[str, str]
:return: dictionary of model that is about to be saved
:rtype: dict[str, str]
"""
new_kwargs = cls._remove_pk_from_kwargs(new_kwargs)
new_kwargs = cls._remove_not_ormar_fields(new_kwargs)
new_kwargs = cls.substitute_models_with_pks(new_kwargs)
new_kwargs = cls.populate_default_values(new_kwargs)
new_kwargs = cls.reconvert_str_to_bytes(new_kwargs)
new_kwargs = cls.translate_columns_to_aliases(new_kwargs)
return new_kwargs
@classmethod
def prepare_model_to_update(cls, new_kwargs: dict) -> dict:
"""
Combines all preparation methods before updating.
:param new_kwargs: dictionary of model that is about to be saved
:type new_kwargs: dict[str, str]
:return: dictionary of model that is about to be updated
:rtype: dict[str, str]
"""
new_kwargs = cls.parse_non_db_fields(new_kwargs)
new_kwargs = cls.substitute_models_with_pks(new_kwargs)
new_kwargs = cls.reconvert_str_to_bytes(new_kwargs)
new_kwargs = cls.dump_all_json_fields_to_str(new_kwargs)
new_kwargs = cls.translate_columns_to_aliases(new_kwargs)
new_kwargs = cls.translate_enum_columns(new_kwargs)
return new_kwargs
@classmethod
def translate_enum_columns(cls, new_kwargs: dict) -> dict:
for key, value in new_kwargs.items():
if isinstance(value, Enum):
new_kwargs[key] = value.name
return new_kwargs
@classmethod
def _remove_not_ormar_fields(cls, new_kwargs: dict) -> dict:
"""
Removes primary key for if it's nullable or autoincrement pk field,
and it's set to None.
:param new_kwargs: dictionary of model that is about to be saved
:type new_kwargs: dict[str, str]
:return: dictionary of model that is about to be saved
:rtype: dict[str, str]
"""
try:
ormar_fields = cls._ormar_fields_set # type: ignore[attr-defined]
except AttributeError:
ormar_fields = set(cls.ormar_config.model_fields.keys())
cls._ormar_fields_set = ormar_fields # type: ignore[attr-defined]
new_kwargs = {k: v for k, v in new_kwargs.items() if k in ormar_fields}
return new_kwargs
@classmethod
def _remove_pk_from_kwargs(cls, new_kwargs: dict) -> dict:
"""
Removes primary key for if it's nullable or autoincrement pk field,
and it's set to None.
:param new_kwargs: dictionary of model that is about to be saved
:type new_kwargs: dict[str, str]
:return: dictionary of model that is about to be saved
:rtype: dict[str, str]
"""
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
):
del new_kwargs[pkname]
return new_kwargs
@classmethod
def parse_non_db_fields(cls, model_dict: dict) -> dict:
"""
Receives dictionary of model that is about to be saved and changes uuid fields
to strings in bulk_update.
:param model_dict: dictionary of model that is about to be saved
:type model_dict: dict
:return: dictionary of model that is about to be saved
:rtype: dict
"""
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
parser: Callable[..., Any] = parsers.get(uuid_format, lambda x: x)
model_dict[name] = parser(model_dict[name])
return model_dict
@classmethod
def substitute_models_with_pks(cls, model_dict: dict) -> dict: # noqa CCR001
"""
Receives dictionary of model that is about to be saved and changes all related
models that are stored as foreign keys to their fk value.
:param model_dict: dictionary of model that is about to be saved
:type model_dict: dict
:return: dictionary of model that is about to be saved
:rtype: dict
"""
for field in cls.extract_related_names():
field_value = model_dict.get(field, None)
if field_value is not None:
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:
raise ModelPersistenceError(
f"You cannot save {field_value.get_name()} "
f"model without pk set!"
)
model_dict[field] = pk_value
elif isinstance(field_value, (list, dict)) and field_value:
if isinstance(field_value, list):
model_dict[field] = [
target.get(target_pkname) for target in field_value
]
else:
model_dict[field] = field_value.get(target_pkname)
else:
model_dict.pop(field, None)
return model_dict
@classmethod
def reconvert_str_to_bytes(cls, model_dict: dict) -> dict:
"""
Receives dictionary of model that is about to be saved and changes
all bytes fields that are represented as strings back into bytes.
:param model_dict: dictionary of model that is about to be saved
:type model_dict: dict
:return: dictionary of model that is about to be saved
:rtype: dict
"""
bytes_base64_fields = {
name
for name, field in cls.ormar_config.model_fields.items()
if field.represent_as_base64_str
}
for key, value in model_dict.items():
if key in cls._bytes_fields and isinstance(value, str):
model_dict[key] = (
value.encode("utf-8")
if key not in bytes_base64_fields
else base64.b64decode(value)
)
return model_dict
@classmethod
def dump_all_json_fields_to_str(cls, model_dict: dict) -> dict:
"""
Receives dictionary of model that is about to be saved and changes
all json fields into strings
:param model_dict: dictionary of model that is about to be saved
:type model_dict: dict
:return: dictionary of model that is about to be saved
:rtype: dict
"""
for key, value in model_dict.items():
if key in cls._json_fields:
model_dict[key] = encode_json(value)
return model_dict
@classmethod
def serialize_nested_models_json_fields(cls, kwargs: dict) -> dict:
"""
Serializes JSON fields in nested model instances to strings.
This ensures that when models with JSON fields are nested in other models,
the JSON values are properly serialized as strings for Pydantic validation.
Needed as sqlalchemy auto parses the JSON fields.
:param kwargs: dictionary of keyword arguments
:type kwargs: Dict
:return: modified kwargs with serialized JSON fields in nested models
:rtype: Dict
"""
for key, value in kwargs.items():
if isinstance(value, ormar.Model) and hasattr(value, "_json_fields"):
value.dump_all_json_fields_to_str(value.__dict__)
return kwargs
@classmethod
def populate_default_values(cls, new_kwargs: dict) -> dict:
"""
Receives dictionary of model that is about to be saved and populates the default
value on the fields that have the default value set, but no actual value was
passed by the user.
:param new_kwargs: dictionary of model that is about to be saved
:type new_kwargs: dict
:return: dictionary of model that is about to be saved
:rtype: dict
"""
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 (
field.server_default is not None
and new_kwargs.get(field_name, None) is None
):
new_kwargs.pop(field_name, None)
return new_kwargs
@classmethod
def populate_onupdate_value(
cls, new_kwargs: dict, explicit_fields: Optional[set[str]] = None
) -> dict:
"""
Populates on_update values for fields that have on_update configured
and were not provided explicitly.
If ``explicit_fields`` is omitted the keys of ``new_kwargs`` are
treated as explicitly provided. Callers pass the set explicitly when
``new_kwargs`` is a full ``model_dump`` (e.g. ``bulk_update``) so that
fields the user actually mutated can be distinguished from values that
are just present by virtue of the dump.
:param new_kwargs: dictionary of values about to be used for the update
:type new_kwargs: dict
:param explicit_fields: fields that should be considered user-provided
:type explicit_fields: Optional[set[str]]
:return: new_kwargs with on_update values populated
:rtype: dict
"""
if explicit_fields is None:
explicit_fields = set(new_kwargs.keys())
for field_name in cls._onupdate_fields - explicit_fields:
field = cls.ormar_config.model_fields[field_name]
new_kwargs[field_name] = field.get_on_update()
return new_kwargs
@classmethod
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.
:param new_kwargs: dictionary of model that is about to be saved
:type new_kwargs: dict
:return: dictionary of model that is about to be saved
:rtype: dict
"""
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: # pragma: no cover
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",
save_all: bool,
previous_model: Optional["Model"],
relation_field: Optional["ForeignKeyField"],
update_count: int,
) -> int:
"""
Method updates given instance if:
* instance is not saved or
* instance have no pk or
* save_all=True flag is set
and instance is not __pk_only__.
If relation leading to instance is a ManyToMany also the through model is saved
:param instance: current model to upsert
:type instance: Model
:param save_all: flag if all models should be saved or only not saved ones
:type save_all: bool
:param relation_field: field with relation
:type relation_field: Optional[ForeignKeyField]
:param previous_model: previous model from which method came
:type previous_model: Model
:param update_count: no of updated models
:type update_count: int
:return: no of updated models
:rtype: int
"""
if (
save_all or not instance.pk or not instance.saved
) and not instance.__pk_only__:
await instance.upsert(__force_save__=True)
if relation_field and relation_field.is_multi:
await instance._upsert_through_model(
instance=instance,
relation_field=relation_field,
previous_model=cast("Model", previous_model),
)
update_count += 1
return update_count
@staticmethod
async def _upsert_through_model(
instance: "Model", previous_model: "Model", relation_field: "ForeignKeyField"
) -> None:
"""
Upsert through model for m2m relation.
:param instance: current model to upsert
:type instance: Model
:param relation_field: field with relation
:type relation_field: Optional[ForeignKeyField]
:param previous_model: previous model from which method came
:type previous_model: Model
"""
through_name = previous_model.ormar_config.model_fields[
relation_field.name
].through.get_name()
through = getattr(instance, through_name)
if through:
through_dict = through.model_dump(exclude=through.extract_related_names())
else:
through_dict = {}
await getattr(
previous_model, relation_field.name
).queryset_proxy.upsert_through_instance(instance, **through_dict)
async def _update_relation_list(
self,
fields_list: Collection["ForeignKeyField"],
follow: bool,
save_all: bool,
relation_map: dict,
update_count: int,
) -> int:
"""
Internal method used in save_related to follow deeper from
related models and update numbers of updated related instances.
:type save_all: flag if all models should be saved
:type save_all: bool
:param fields_list: list of ormar fields to follow and save
:type fields_list: Collection["ForeignKeyField"]
:param relation_map: map of relations to follow
:type relation_map: dict
:param follow: flag to trigger deep save -
by default only directly related models are saved
with follow=True also related models of related models are saved
:type follow: bool
:param update_count: internal parameter for recursive calls -
number of updated instances
:type update_count: int
:return: tuple of update count and visited
:rtype: int
"""
for field in fields_list:
values = self._get_field_values(name=field.name)
for value in values:
if follow:
update_count = await value.save_related(
follow=follow,
save_all=save_all,
relation_map=skip_ellipsis(
relation_map, field.name, default={}
),
update_count=update_count,
previous_model=self,
relation_field=field,
)
else:
update_count = await value._upsert_model(
instance=value,
save_all=save_all,
previous_model=self,
relation_field=field,
update_count=update_count,
)
return update_count
def _get_field_values(self, name: str) -> list:
"""
Extract field values and ensures it is a list.
:param name: name of the field
:type name: str
:return: list of values
:rtype: list
"""
values = getattr(self, name) or []
if not isinstance(values, list):
values = [values]
return values
ormar-orm-ormar-a7e7fde/ormar/models/model.py 0000664 0000000 0000000 00000037332 15211567312 0021417 0 ustar 00root root 0000000 0000000 import builtins
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
from sqlalchemy import Executable
import ormar.queryset # noqa I100
from ormar.exceptions import ModelPersistenceError, NoMatch
from ormar.models import NewBaseModel # noqa I100
from ormar.models.model_row import ModelRow
from ormar.queryset.utils import subtract_dict, translate_list_to_dict
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
ormar_config: OrmarConfig
def __repr__(self) -> str: # pragma nocover
_repr = {
k: getattr(self, k)
for k, v in self.ormar_config.model_fields.items()
if not v.skip_field
}
return f"{self.__class__.__name__}({str(_repr)})"
async def _execute_query(self, expr: Executable, is_select: bool = False) -> Any:
async with self.ormar_config.database.get_query_executor() as executor:
row = (
await executor.fetch_one(expr)
if is_select
else await executor.execute(expr)
)
return row
async def _emit_signal(self, name: str, **kwargs: Any) -> None:
"""
Emit a lifecycle signal on this model's SignalEmitter.
When ``ormar_config.emit_parent_signals`` is True, the same signal is
also dispatched on every concrete ormar ancestor in the MRO. Each
emit uses ``sender=ancestor_cls`` so handlers registered with
``@pre_save(Parent)`` see the parent class as the sender.
:param name: signal name on the SignalEmitter (e.g. ``"pre_save"``).
:type name: str
:param kwargs: extra payload forwarded to receivers.
:type kwargs: Any
"""
cls = type(self)
await getattr(self.ormar_config.signals, name).send(sender=cls, **kwargs)
if not self.ormar_config.emit_parent_signals:
return
seen = {id(self.ormar_config.signals)}
for ancestor in cls.__mro__[1:]:
cfg = getattr(ancestor, "ormar_config", None)
if cfg is None or cfg.abstract or id(cfg.signals) in seen:
continue
seen.add(id(cfg.signals))
await getattr(cfg.signals, name).send(sender=ancestor, **kwargs)
async def upsert(self: T, **kwargs: Any) -> T:
"""
Performs either a save or an update depending on the presence of the pk.
If the pk field is filled it's an update, otherwise the save is performed.
For save kwargs are ignored, used only in update if provided.
:param kwargs: list of fields to update
:type kwargs: Any
:return: saved Model
:rtype: Model
"""
force_save = kwargs.pop("__force_save__", False)
if force_save:
expr = self.ormar_config.table.select().where(self.pk_column == self.pk)
row = await self._execute_query(expr, is_select=True)
if not row:
return await self.save()
return await self.update(**kwargs)
if not self.pk:
return await self.save()
return await self.update(**kwargs)
async def save(self: T) -> T:
"""
Performs a save of given Model instance.
If primary key is already saved, db backend will throw integrity error.
Related models are saved by pk number, reverse relation and many to many fields
are not saved - use corresponding relations methods.
If there are fields with server_default set and those fields
are not already filled save will trigger also a second query
to refreshed the fields populated server side.
Does not recognize if model was previously saved.
If you want to perform update or insert depending on the pk
fields presence use upsert.
Sends pre_save and post_save signals.
Sets model save status to True.
:return: saved Model
:rtype: Model
"""
await self._emit_signal("pre_save", instance=self)
self_fields = self._extract_model_db_fields()
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(
{
k: v
for k, v in self_fields.items()
if k not in self.extract_related_names()
}
)
self_fields = self.translate_columns_to_aliases(self_fields)
expr = self.ormar_config.table.insert()
expr = expr.values(**self_fields)
pkname = self.ormar_config.pkname
pk_returned_from_insert = False
pk = await self._execute_query(expr)
if pk and isinstance(pk, self.pk_type()):
setattr(self, pkname, pk)
pk_returned_from_insert = True
if self.pk is None:
raise ModelPersistenceError( # pragma: no cover
f"Could not recover the generated primary key for "
f"{self.__class__.__name__} after INSERT. This happens on "
"backends that lack RETURNING support for server-side "
"defaults on non-AUTO_INCREMENT primary keys — most notably "
"Oracle MySQL, which does not implement RETURNING in any "
"version. Use autoincrement=True, provide the primary key "
"client-side, or switch to a RETURNING-capable backend "
"(PostgreSQL, SQLite 3.35+, MariaDB 10.5+)."
)
self.set_save_status(True)
# refresh server-side defaults — but skip the pk if the insert already
# returned it, so save() stays a single round-trip when the pk is the
# only server_default field.
if any(
field.server_default is not None
for name, field in self.ormar_config.model_fields.items()
if name not in self_fields
and not (pk_returned_from_insert and name == pkname)
):
await self.load()
self.__setattr_fields__.clear()
await self._emit_signal("post_save", instance=self)
return self
async def save_related( # noqa: CCR001, CFQ002
self,
follow: bool = False,
save_all: bool = False,
relation_map: Optional[builtins.dict] = None,
exclude: Union[set, builtins.dict, None] = None,
update_count: int = 0,
previous_model: Optional["Model"] = None,
relation_field: Optional["ForeignKeyField"] = None,
) -> int:
"""
Triggers a upsert method on all related models
if the instances are not already saved.
By default saves only the directly related ones.
If follow=True is set it saves also related models of related models.
To not get stuck in an infinite loop as related models also keep a relation
to parent model visited models set is kept.
That way already visited models that are nested are saved, but the save do not
follow them inside. So Model A -> Model B -> Model A -> Model C will save second
Model A but will never follow into Model C.
Nested relations of those kind need to be persisted manually.
:param relation_field: field with relation leading to this model
:type relation_field: Optional[ForeignKeyField]
:param previous_model: previous model from which method came
:type previous_model: Model
:param exclude: items to exclude during saving of relations
:type exclude: Union[set, dict]
:param relation_map: map of relations to follow
:type relation_map: dict
:param save_all: flag if all models should be saved or only not saved ones
:type save_all: bool
:param follow: flag to trigger deep save -
by default only directly related models are saved
with follow=True also related models of related models are saved
:type follow: bool
:param update_count: internal parameter for recursive calls -
number of updated instances
:type update_count: int
:return: number of updated/saved models
:rtype: int
"""
relation_map = (
relation_map
if relation_map is not None
else translate_list_to_dict(self._iterate_related_models())
)
if exclude and isinstance(exclude, set):
exclude = translate_list_to_dict(exclude)
relation_map = subtract_dict(relation_map, exclude or {})
if relation_map:
fields_to_visit = {
field
for field in self.extract_related_fields()
if field.name in relation_map
}
pre_save = {
field
for field in fields_to_visit
if not field.virtual and not field.is_multi
}
update_count = await self._update_relation_list(
fields_list=pre_save,
follow=follow,
save_all=save_all,
relation_map=relation_map,
update_count=update_count,
)
update_count = await self._upsert_model(
instance=self,
save_all=save_all,
previous_model=previous_model,
relation_field=relation_field,
update_count=update_count,
)
post_save = fields_to_visit - pre_save
update_count = await self._update_relation_list(
fields_list=post_save,
follow=follow,
save_all=save_all,
relation_map=relation_map,
update_count=update_count,
)
else:
update_count = await self._upsert_model(
instance=self,
save_all=save_all,
previous_model=previous_model,
relation_field=relation_field,
update_count=update_count,
)
return update_count
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.
Sends pre_update and post_update signals.
Sets model save status to True.
:param _columns: list of columns to update, if None all are updated
:type _columns: list
:raises ModelPersistenceError: If the pk column is not set
:param kwargs: list of fields to update as field=value pairs
:type kwargs: Any
:return: updated Model
:rtype: Model
"""
explicit_fields = self.__setattr_fields__ | kwargs.keys()
values = self.populate_onupdate_value(
dict(kwargs), explicit_fields=explicit_fields
)
if values:
self.update_from_dict(values)
if not self.pk:
raise ModelPersistenceError(
"You cannot update not saved model! Use save or upsert method."
)
await self._emit_signal("pre_update", instance=self, passed_args=kwargs)
self_fields = self._extract_model_db_fields()
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 or k in self._onupdate_fields
}
if self_fields:
self_fields = self.translate_columns_to_aliases(self_fields)
expr = self.ormar_config.table.update().values(**self_fields)
expr = expr.where(self.pk_column == getattr(self, self.ormar_config.pkname))
await self._execute_query(expr)
self.set_save_status(True)
self.__setattr_fields__.clear()
await self._emit_signal("post_update", instance=self)
return self
async def delete(self) -> int:
"""
Removes the Model instance from the database.
Sends pre_delete and post_delete signals.
Sets model save status to False.
Note it does not delete the Model itself (python object).
So you can delete and later save (since pk is deleted no conflict will arise)
or update and the Model will be saved in database again.
:return: number of deleted rows (for some backends)
:rtype: int
"""
await self._emit_signal("pre_delete", instance=self)
expr = self.ormar_config.table.delete()
expr = expr.where(self.pk_column == (getattr(self, self.ormar_config.pkname)))
result = await self._execute_query(expr)
self.set_save_status(False)
await self._emit_signal("post_delete", instance=self)
return result
async def load(self: T) -> T:
"""
Allow to refresh existing Models fields from database.
Be careful as the related models can be overwritten by pk_only models in load.
Does NOT refresh the related models fields if they were loaded before.
:raises NoMatch: If given pk is not found in database.
:return: reloaded Model
:rtype: Model
"""
expr = self.ormar_config.table.select().where(self.pk_column == self.pk)
row = await self._execute_query(expr, is_select=True)
if not row: # pragma nocover
raise NoMatch("Instance was deleted from database and cannot be refreshed")
kwargs = dict(row)
kwargs = self.translate_aliases_to_columns(kwargs)
self.update_from_dict(kwargs)
self.set_save_status(True)
self.__setattr_fields__.clear()
return self
async def load_all(
self: T,
follow: bool = False,
exclude: Union[list, str, set, dict, None] = None,
order_by: Union[list, str, None] = None,
) -> T:
"""
Allow to refresh existing Models fields from database.
Performs refresh of the related models fields.
By default, loads only self and the directly related ones.
If follow=True is set it loads also related models of related models.
To not get stuck in an infinite loop as related models also keep a relation
to parent model visited models set is kept.
That way already visited models that are nested are loaded, but the load do not
follow them inside. So Model A -> Model B -> Model C -> Model A -> Model X
will load second Model A but will never follow into Model X.
Nested relations of those kind need to be loaded manually.
:param order_by: columns by which models should be sorted
:type order_by: Union[list, str]
:raises NoMatch: If given pk is not found in database.
:param exclude: related models to exclude
:type exclude: Union[list, str, set, dict]
:param follow: flag to trigger deep save -
by default only directly related models are saved
with follow=True also related models of related models are saved
:type follow: bool
:return: reloaded Model
:rtype: Model
"""
relations = list(self.extract_related_names())
if follow:
relations = self._iterate_related_models()
queryset = self.__class__.objects
if exclude:
queryset = queryset.exclude_fields(exclude)
if order_by:
queryset = queryset.order_by(order_by)
instance = await queryset.select_related(relations).get(pk=self.pk)
self._orm.clear()
self.update_from_dict(instance.model_dump())
self.__setattr_fields__.clear()
return self
ormar-orm-ormar-a7e7fde/ormar/models/model_row.py 0000664 0000000 0000000 00000044165 15211567312 0022310 0 ustar 00root root 0000000 0000000 from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Optional, Union, cast
try:
from sqlalchemy.engine.result import ResultProxy # type: ignore
except ImportError: # pragma: no cover
from sqlalchemy.engine.result import Row as ResultProxy # type: ignore
from ormar.models import NewBaseModel # noqa: I202
from ormar.models.excludable import ExcludableItems
from ormar.models.helpers.models import group_related_list
if TYPE_CHECKING: # pragma: no cover
from ormar.fields import ForeignKeyField
from ormar.models import Model
@dataclass(frozen=True)
class RowExtractionPlan:
"""
Precomputed per-``(model_cls, table_prefix, excludable)`` view of the work
that ``from_row`` used to redo for every row: which columns to read from
the SA row (already prefixed and filtered), which field names to nullify
after construction, and the model's pk field name.
:ivar column_mappings: ordered ``(prefixed_db_column, field_name)`` pairs
the row reader iterates over to populate the model dict
:ivar excluded_field_names: field names to nullify via
``_construct_with_excluded`` after pydantic validation
:ivar pk_field_name: cached ``ormar_config.pkname`` so the row reader can
test for a populated pk without a per-row attribute lookup
"""
column_mappings: tuple[tuple[str, str], ...]
excluded_field_names: frozenset[str]
pk_field_name: str
PlanCache = dict[tuple[type, str, int], RowExtractionPlan]
class ModelRow(NewBaseModel):
@classmethod
def from_row( # noqa: CFQ002
cls,
row: ResultProxy,
source_model: type["Model"],
select_related: Optional[list] = None,
related_models: Any = None,
related_field: Optional["ForeignKeyField"] = None,
excludable: Optional[ExcludableItems] = None,
current_relation_str: str = "",
proxy_source_model: Optional[type["Model"]] = None,
used_prefixes: Optional[list[str]] = None,
plan_cache: Optional[PlanCache] = None,
) -> Optional["Model"]:
"""
Model method to convert raw sql row from database into ormar.Model instance.
Traverses nested models if they were specified in select_related for query.
Called recurrently and returns model instance if it's present in the row.
Note that it's processing one row at a time, so if there are duplicates of
parent row that needs to be joined/combined
(like parent row in sql join with 2+ child rows)
instances populated in this method are later combined in the QuerySet.
Other method working directly on raw database results is in prefetch_query,
where rows are populated in a different way as they do not have
nested models in result.
:param used_prefixes: list of already extracted prefixes
:type used_prefixes: list[str]
:param proxy_source_model: source model from which querysetproxy is constructed
:type proxy_source_model: Optional[type["ModelRow"]]
:param excludable: structure of fields to include and exclude
:type excludable: ExcludableItems
:param current_relation_str: name of the relation field
:type current_relation_str: str
:param source_model: model on which relation was defined
:type source_model: type[Model]
:param row: raw result row from the database
:type row: ResultProxy
:param select_related: list of names of related models fetched from database
:type select_related: list
:param related_models: list or dict of related models
:type related_models: Union[list, dict]
:param related_field: field with relation declaration
:type related_field: ForeignKeyField
:return: returns model if model is populated from database
:rtype: Optional[Model]
"""
item: dict[str, Any] = {}
select_related = select_related or []
related_models = related_models or []
table_prefix = ""
used_prefixes = used_prefixes if used_prefixes is not None else []
excludable = excludable or ExcludableItems()
if select_related:
related_models = group_related_list(select_related)
if related_field:
table_prefix = cls._process_table_prefix(
source_model=source_model,
current_relation_str=current_relation_str,
related_field=related_field,
used_prefixes=used_prefixes,
)
item = cls._populate_nested_models_from_row(
item=item,
row=row,
related_models=related_models,
excludable=excludable,
current_relation_str=current_relation_str,
source_model=source_model, # type: ignore
proxy_source_model=proxy_source_model, # type: ignore
table_prefix=table_prefix,
used_prefixes=used_prefixes,
plan_cache=plan_cache,
)
plan = cls.get_or_build_row_plan(table_prefix, excludable, plan_cache)
item = cls.apply_row_plan(plan, row, item)
instance: Optional["Model"] = None
if item.get(plan.pk_field_name, None) is not None:
instance = cast(
"Model",
cls._construct_with_excluded(plan.excluded_field_names, **item),
)
instance.set_save_status(True)
return instance
@classmethod
def _process_table_prefix(
cls,
source_model: type["Model"],
current_relation_str: str,
related_field: "ForeignKeyField",
used_prefixes: list[str],
) -> str:
"""
:param source_model: model on which relation was defined
:type source_model: type[Model]
:param current_relation_str: current relation string
:type current_relation_str: str
:param related_field: field with relation declaration
:type related_field: "ForeignKeyField"
:param used_prefixes: list of already extracted prefixes
:type used_prefixes: list[str]
:return: table_prefix to use
:rtype: str
"""
if related_field.is_multi:
previous_model = related_field.through
else:
previous_model = related_field.owner
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.ormar_config.alias_manager
table_prefix = manager.resolve_relation_alias_after_complex(
source_model=source_model,
relation_str=current_relation_str,
relation_field=related_field,
)
used_prefixes.append(table_prefix)
return table_prefix
@classmethod
def _populate_nested_models_from_row( # noqa: CFQ002
cls,
item: dict,
row: ResultProxy,
source_model: type["Model"],
related_models: Any,
excludable: ExcludableItems,
table_prefix: str,
used_prefixes: list[str],
current_relation_str: Optional[str] = None,
proxy_source_model: Optional[type["Model"]] = None,
plan_cache: Optional[PlanCache] = None,
) -> dict:
"""
Traverses structure of related models and populates the nested models
from the database row.
Related models can be a list if only directly related models are to be
populated, converted to dict if related models also have their own related
models to be populated.
Recurrently calls from_row method on nested instances and create nested
instances. In the end those instances are added to the final model dictionary.
:param proxy_source_model: source model from which querysetproxy is constructed
:type proxy_source_model: Optional[type["ModelRow"]]
:param excludable: structure of fields to include and exclude
:type excludable: ExcludableItems
:param source_model: source model from which relation started
:type source_model: type[Model]
:param current_relation_str: joined related parts into one string
:type current_relation_str: str
:param item: dictionary of already populated nested models, otherwise empty dict
:type item: dict
:param row: raw result row from the database
:type row: ResultProxy
:param related_models: list or dict of related models
:type related_models: Union[dict, list]
:return: dictionary with keys corresponding to model fields names
and values are database values
:rtype: dict
"""
for related in related_models:
field = cls.ormar_config.model_fields[related]
field = cast("ForeignKeyField", field)
model_cls = field.to
model_excludable = excludable.get(
model_cls=cast(type["Model"], cls), alias=table_prefix
)
if model_excludable.is_excluded(related):
continue
relation_str, remainder = cls._process_remainder_and_relation_string(
related_models=related_models,
current_relation_str=current_relation_str,
related=related,
)
child = model_cls.from_row(
row,
related_models=remainder,
related_field=field,
excludable=excludable,
current_relation_str=relation_str,
source_model=source_model,
proxy_source_model=proxy_source_model,
used_prefixes=used_prefixes,
plan_cache=plan_cache,
)
item[model_cls.get_column_name_from_alias(related)] = child
if (
field.is_multi
and child
and not model_excludable.is_excluded(field.through.get_name())
):
cls._populate_through_instance(
row=row,
item=item,
related=related,
excludable=excludable,
child=child,
proxy_source_model=proxy_source_model,
plan_cache=plan_cache,
)
return item
@staticmethod
def _process_remainder_and_relation_string(
related_models: Union[dict, list],
current_relation_str: Optional[str],
related: str,
) -> tuple[str, Optional[Union[dict, list]]]:
"""
Process remainder models and relation string
:param related_models: list or dict of related models
:type related_models: Union[dict, list]
:param current_relation_str: current relation string
:type current_relation_str: Optional[str]
:param related: name of the relation
:type related: str
"""
relation_str = (
"__".join([current_relation_str, related])
if current_relation_str
else related
)
remainder = None
if isinstance(related_models, dict) and related_models[related]:
remainder = related_models[related]
return relation_str, remainder
@classmethod
def _populate_through_instance( # noqa: CFQ002
cls,
row: ResultProxy,
item: dict,
related: str,
excludable: ExcludableItems,
child: "Model",
proxy_source_model: Optional[type["Model"]],
plan_cache: Optional[PlanCache] = None,
) -> None:
"""
Populates the through model on reverse side of current query.
Normally it's child class, unless the query is from queryset.
:param row: row from db result
:type row: ResultProxy
:param item: parent item dict
:type item: dict
:param related: current relation name
:type related: str
:param excludable: structure of fields to include and exclude
:type excludable: ExcludableItems
:param child: child item of parent
:type child: "Model"
:param proxy_source_model: source model from which querysetproxy is constructed
:type proxy_source_model: type["Model"]
:param plan_cache: optional per-queryset plan cache
:type plan_cache: Optional[PlanCache]
"""
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,
plan_cache=plan_cache,
)
if child.__class__ != proxy_source_model:
setattr(child, through_name, through_child)
else:
item[through_name] = through_child
child.set_save_status(True)
@classmethod
def _create_through_instance(
cls,
row: ResultProxy,
through_name: str,
related: str,
excludable: ExcludableItems,
plan_cache: Optional[PlanCache] = None,
) -> "ModelRow":
"""
Initialize the through model from db row.
Excluded all relation fields and other exclude/include set in excludable.
:param row: loaded row from database
:type row: sqlalchemy.engine.ResultProxy
:param through_name: name of the through field
:type through_name: str
:param related: name of the relation
:type related: str
:param excludable: structure of fields to include and exclude
:type excludable: ExcludableItems
:param plan_cache: optional per-queryset plan cache
:type plan_cache: Optional[PlanCache]
:return: initialized through model without relation
:rtype: "ModelRow"
"""
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 — must happen before the plan is
# built so the plan reflects the through-model's full exclude set
model_excludable = excludable.get(model_cls=model_cls, alias=table_prefix)
model_excludable.set_values(
value=model_cls.extract_related_names(), slot="exclude"
)
plan = model_cls.get_or_build_row_plan(table_prefix, excludable, plan_cache)
child_dict = model_cls.apply_row_plan(plan, row, {})
child = model_cls._construct_with_excluded( # type: ignore
plan.excluded_field_names, **child_dict
)
return child
@classmethod
def build_row_extraction_plan(
cls,
table_prefix: str,
excludable: ExcludableItems,
) -> RowExtractionPlan:
"""
Compute the per-row extraction plan for a ``(cls, table_prefix,
excludable)`` triple — the work that previously ran inside
``extract_prefixed_table_columns`` for every row.
:param table_prefix: prefix of the table from AliasManager
:type table_prefix: str
:param excludable: structure of fields to include and exclude
:type excludable: ExcludableItems
:return: cacheable plan for fast per-row extraction
:rtype: RowExtractionPlan
"""
selected_columns = set(
cls.own_table_columns(
model=cls, excludable=excludable, alias=table_prefix, use_alias=False
)
)
column_prefix = table_prefix + "_" if table_prefix else ""
column_pairs = cls._get_table_column_pairs(cls)
mappings = tuple(
(f"{column_prefix}{col_name}", field_name)
for col_name, field_name in column_pairs
if field_name in selected_columns
)
excluded = frozenset(
cls.get_names_to_exclude(excludable=excludable, alias=table_prefix)
)
return RowExtractionPlan(
column_mappings=mappings,
excluded_field_names=excluded,
pk_field_name=cls.ormar_config.pkname,
)
@classmethod
def get_or_build_row_plan(
cls,
table_prefix: str,
excludable: ExcludableItems,
plan_cache: Optional[PlanCache],
) -> RowExtractionPlan:
"""
Return a cached plan for the given key, or build and cache one. When
``plan_cache`` is ``None`` (legacy / external caller) the plan is
built fresh on every call so behavior matches the pre-cache shape.
:param table_prefix: prefix of the table from AliasManager
:type table_prefix: str
:param excludable: structure of fields to include and exclude
:type excludable: ExcludableItems
:param plan_cache: per-queryset cache keyed by
``(cls, table_prefix, id(excludable))``; ``None`` to bypass
:type plan_cache: Optional[PlanCache]
:return: extraction plan for this row position
:rtype: RowExtractionPlan
"""
if plan_cache is None:
return cls.build_row_extraction_plan(table_prefix, excludable)
key = (cls, table_prefix, id(excludable))
plan = plan_cache.get(key)
if plan is None:
plan = cls.build_row_extraction_plan(table_prefix, excludable)
plan_cache[key] = plan
return plan
@staticmethod
def apply_row_plan(
plan: RowExtractionPlan,
row: ResultProxy,
item: dict,
) -> dict:
"""
Populate ``item`` from ``row`` using ``plan.column_mappings``. Skips
keys already present so a partially populated dict (e.g. from
``_populate_nested_models_from_row``) is not overwritten.
:param plan: precomputed extraction plan
:type plan: RowExtractionPlan
:param row: raw result row from the database
:type row: sqlalchemy.engine.result.ResultProxy
:param item: dict to populate in place
:type item: dict
:return: ``item`` (returned for chaining symmetry with the legacy API)
:rtype: dict
"""
for prefixed_name, field_name in plan.column_mappings:
if field_name not in item:
item[field_name] = row[prefixed_name]
return item
ormar-orm-ormar-a7e7fde/ormar/models/modelproxy.py 0000664 0000000 0000000 00000000553 15211567312 0022514 0 ustar 00root root 0000000 0000000 from ormar.models.mixins import (
ExcludableMixin,
MergeModelMixin,
PydanticMixin,
SavePrepareMixin,
)
class ModelTableProxy(
MergeModelMixin,
SavePrepareMixin,
ExcludableMixin,
PydanticMixin,
):
"""
Used to combine all mixins with different set of functionalities.
One of the bases of the ormar Model class.
"""
ormar-orm-ormar-a7e7fde/ormar/models/newbasemodel.py 0000664 0000000 0000000 00000145014 15211567312 0022761 0 ustar 00root root 0000000 0000000 import base64
import builtins
import sys
import warnings
from dataclasses import dataclass
from typing import (
TYPE_CHECKING,
AbstractSet,
Any,
Literal,
Mapping,
MutableSequence,
Optional,
TypeVar,
Union,
cast,
)
import pydantic
import sqlalchemy
import typing_extensions
import ormar # noqa I100
from ormar.exceptions import ModelError, ModelPersistenceError, QueryDefinitionError
from ormar.fields.foreign_key import ForeignKeyField
from ormar.fields.parsers import decode_bytes, encode_json
from ormar.models.excludable import (
ExcludableItems,
FlattenMap,
convert_all,
filter_not_excluded_fields,
normalize_to_dict,
skip_ellipsis,
)
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_config_sqlalchemy_table_if_required,
update_column_definition,
)
from ormar.models.metaclass import ModelMetaclass
from ormar.models.modelproxy import ModelTableProxy
from ormar.models.utils import Extra
from ormar.queryset.field_accessor import FieldAccessor
from ormar.queryset.utils import extract_access_chains, 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, OrmarConfig
from ormar.signals import SignalEmitter
T = TypeVar("T", bound="NewBaseModel")
IntStr = Union[int, str]
DictStrAny = dict[str, Any]
SetStr = set[str]
AbstractSetIntStr = AbstractSet[IntStr]
MappingIntStrAny = Mapping[IntStr, Any]
@dataclass(frozen=True)
class NestedDescent:
"""
Per-field locals threaded into the recursive ``model_dump`` call when
descending into a related field. Returned from ``_resolve_field_descent``
only when the field is not handled inline (flattened or skipped).
"""
flatten_map: Optional["FlattenMap"]
relation_map: builtins.dict
include: Union[set, builtins.dict, None]
exclude: Union[set, builtins.dict, None]
class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass):
"""
Main base class of ormar Model.
Inherits from pydantic BaseModel and has all mixins combined in ModelTableProxy.
Constructed with ModelMetaclass which in turn also inherits pydantic metaclass.
Abstracts away all internals and helper functions, so final Model class has only
the logic concerned with database connection and data persistence.
"""
__slots__ = (
"_orm_id",
"_orm_saved",
"_orm",
"_pk_column",
"__pk_only__",
"__ormar_excludable__",
"__cached_hash__",
"__pydantic_extra__",
"__pydantic_fields_set__",
"__setattr_fields__",
)
if TYPE_CHECKING: # pragma no cover
pk: Any
__relation_map__: Optional[list[str]]
__cached_hash__: Optional[int]
__setattr_fields__: set[str]
_orm_relationship_manager: AliasManager
_orm: RelationsManager
_orm_id: int
_orm_saved: bool
_related_names: Optional[set]
_through_names: Optional[set]
_related_names_hash: str
_quick_access_fields: set
_json_fields: set
_bytes_fields: set
_onupdate_fields: set
_pydantic_field_names: Optional[frozenset[str]]
_extra_is_ignore: Optional[bool]
_allowed_kwarg_names: Optional[frozenset[str]]
_relation_field_names: Optional[frozenset[str]]
ormar_config: OrmarConfig
# noinspection PyMissingConstructor
def __init__(self, *args: Any, **kwargs: Any) -> None: # type: ignore
"""
Initializer that creates a new ormar Model that is also pydantic Model at the
same time.
Passed keyword arguments can be only field names and their corresponding values
as those will be passed to pydantic validation that will complain if extra
params are passed.
If relations are defined each relation is expanded and children models are also
initialized and validated. Relation from both sides is registered so you can
access related models from both sides.
Json fields are automatically loaded/dumped if needed.
Models marked as abstract=True in internal OrmarConfig cannot be initialized.
:raises ModelError: if abstract model is initialized, model has ForwardRefs
that has not been updated or unknown field is passed
:param args: ignored args
:type args: Any
:param kwargs: keyword arguments - all fields values and some special params
:type kwargs: Any
"""
self._verify_model_can_be_initialized()
self._initialize_internal_attributes()
object.__setattr__(self, "__pk_only__", False)
object.__setattr__(self, "__ormar_excludable__", None)
new_kwargs, through_tmp_dict = self._process_kwargs(kwargs)
new_kwargs = self.serialize_nested_models_json_fields(new_kwargs)
self.__pydantic_validator__.validate_python(
new_kwargs,
self_instance=self, # type: ignore
)
self._register_related_models(new_kwargs, through_tmp_dict)
@classmethod
def _internal_construct(
cls,
_pk_only: bool = False,
_excluded: Optional[set[str]] = None,
**kwargs: Any,
) -> "NewBaseModel":
"""
Internal-only factory for constructing model instances with pk_only or
excluded support. Not reachable from user-supplied kwargs or JSON
deserialization.
:param _pk_only: if True, skip validation and set only pk field
:type _pk_only: bool
:param _excluded: set of field names to explicitly set to None
:type _excluded: Optional[set[str]]
:param kwargs: field values for the model
:type kwargs: Any
:return: constructed model instance
:rtype: NewBaseModel
"""
instance = cls.__new__(cls)
instance._verify_model_can_be_initialized()
instance._initialize_internal_attributes()
object.__setattr__(instance, "__pk_only__", _pk_only)
object.__setattr__(instance, "__ormar_excludable__", None)
new_kwargs, through_tmp_dict = instance._process_kwargs(kwargs)
if _excluded:
for field_to_nullify in _excluded:
new_kwargs[field_to_nullify] = None
if not _pk_only:
new_kwargs = instance.serialize_nested_models_json_fields(new_kwargs)
instance.__pydantic_validator__.validate_python(
new_kwargs,
self_instance=instance, # type: ignore
)
else:
fields_set = {instance.ormar_config.pkname}
object.__setattr__(instance, "__dict__", new_kwargs)
object.__setattr__(instance, "__pydantic_fields_set__", fields_set)
instance._register_related_models(new_kwargs, through_tmp_dict)
return instance
def _register_related_models(
self, new_kwargs: dict[str, Any], through_tmp_dict: dict[str, Any]
) -> None:
"""
Adds back through fields and registers related models after initialization.
:param new_kwargs: processed keyword arguments with field values
:type new_kwargs: dict[str, Any]
:param through_tmp_dict: through model fields extracted during processing
:type through_tmp_dict: dict[str, Any]
"""
new_kwargs.update(through_tmp_dict)
model_fields = object.__getattribute__(self, "ormar_config").model_fields
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
)
@classmethod
def _construct_with_excluded(
cls, excluded: AbstractSet[str], **kwargs: Any
) -> typing_extensions.Self:
"""
Constructs model instance and nullifies excluded fields post-construction.
Used when loading partial results from the database.
:param excluded: collection of field names to nullify after construction
:type excluded: AbstractSet[str]
:param kwargs: field values for the model
:type kwargs: Any
:return: constructed model instance
:rtype: Self
"""
instance = cls(**kwargs)
for field_to_nullify in excluded:
instance.__dict__[field_to_nullify] = None
return instance
def __setattr__(self, name: str, value: Any) -> None: # noqa CCR001
"""
Overwrites setattr in pydantic parent as otherwise descriptors are not called.
:param name: name of the attribute to set
:type name: str
:param value: value of the attribute to set
:type value: Any
:return: None
:rtype: None
"""
prev_hash = hash(self)
if hasattr(self, name):
object.__setattr__(self, name, value)
else:
# let pydantic handle errors for unknown fields
super().__setattr__(name, value)
if self._onupdate_fields and name in self.ormar_config.model_fields:
self.__setattr_fields__.add(name)
# In this case, the hash could have changed, so update it
if name == self.ormar_config.pkname or self.pk is None:
object.__setattr__(self, "__cached_hash__", None)
new_hash = hash(self)
if prev_hash != new_hash:
self._update_relation_cache(prev_hash, new_hash)
def __getattr__(self, item: str) -> Any:
"""
Used for private attributes of pydantic v2.
:param item: name of attribute
:type item: str
:return: Any
:rtype: Any
"""
# 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.model_dump()
state["__dict__"].update(**self_dict)
return state
def __setstate__(self, state: dict[Any, Any]) -> None:
relations = {
k: v
for k, v in state["__dict__"].items()
if k in self.extract_related_names()
}
basic_state = {
k: v
for k, v in state["__dict__"].items()
if k not in self.extract_related_names()
}
state["__dict__"] = basic_state
super().__setstate__(state)
self._initialize_internal_attributes()
for name, value in relations.items():
setattr(self, name, value)
def _update_relation_cache(self, prev_hash: int, new_hash: int) -> None:
"""
Update all relation proxy caches with different hash if we have changed
:param prev_hash: The previous hash to update
:type prev_hash: int
:param new_hash: The hash to update to
:type new_hash: int
"""
def _update_cache(relations: list[Relation], recurse: bool = True) -> None:
for relation in relations:
# Read ``related_models`` directly (rather than calling
# ``relation.get()``) so an un-materialized reverse/m2m
# proxy stays un-materialized — there is nothing in an
# empty proxy to migrate hashes for.
relation_proxy = relation.related_models
if hasattr(relation_proxy, "update_cache"):
relation_proxy.update_cache(prev_hash, new_hash) # type: ignore
elif recurse and hasattr(relation_proxy, "_orm"):
_update_cache(
relation_proxy._orm._relations.values(), # type: ignore
recurse=False,
)
_update_cache(list(self._orm._relations.values()))
def _internal_set(self, name: str, value: Any) -> None:
"""
Delegates call to pydantic.
:param name: name of param
:type name: str
:param value: value to set
:type value: Any
"""
super().__setattr__(name, value)
def _verify_model_can_be_initialized(self) -> None:
"""
Raises exception if model is abstract or has ForwardRefs in relation fields.
:return: None
:rtype: None
"""
if self.ormar_config.abstract:
raise ModelError(f"You cannot initialize abstract model {self.get_name()}")
if self.ormar_config.requires_ref_update:
raise ModelError(
f"Model {self.get_name()} has not updated "
f"ForwardRefs. \nBefore using the model you "
f"need to call update_forward_refs()."
)
def _process_kwargs(self, kwargs: dict) -> tuple[dict, dict]: # noqa: CCR001
"""
Initializes nested models.
Removes property_fields
Checks if field is in the model fields or pydantic fields.
Extracts through models from kwargs into temporary dict.
:param kwargs: passed to init keyword arguments
:type kwargs: dict
:return: modified kwargs
:rtype: tuple[dict, dict]
"""
cls = type(self)
config = cls.ormar_config
model_fields = config.model_fields
pydantic_fields = cls._pydantic_field_names
if pydantic_fields is None:
pydantic_fields = frozenset(cls.model_fields.keys())
cls._pydantic_field_names = pydantic_fields
# remove property fields
for prop_field in config.property_fields:
kwargs.pop(prop_field, None)
if "pk" in kwargs:
kwargs[config.pkname] = kwargs.pop("pk")
# extract through fields
through_tmp_dict = {
field_name: kwargs.pop(field_name, None)
for field_name in self.extract_through_names()
}
extra_is_ignore = cls._extra_is_ignore
if extra_is_ignore is None:
extra_is_ignore = config.extra == Extra.ignore
cls._extra_is_ignore = extra_is_ignore
if extra_is_ignore:
allowed = cls._allowed_kwarg_names
if allowed is None:
allowed = frozenset(model_fields.keys()) | pydantic_fields
cls._allowed_kwarg_names = allowed
kwargs = {k: v for k, v in kwargs.items() if k in allowed}
json_fields = cls._json_fields
bytes_fields = cls._bytes_fields
# ``relation_field_names`` is the disjoint set of fields that need
# ``expand_relationship``; everything else can skip that call. Cached
# on the class on first access — same pattern as ``_pydantic_field_names``.
relation_field_names = getattr(cls, "_relation_field_names", None)
if relation_field_names is None:
relation_field_names = frozenset(
name for name, f in model_fields.items() if f.is_relation
)
cls._relation_field_names = relation_field_names # type: ignore[attr-defined]
has_json = bool(json_fields)
has_bytes = bool(bytes_fields)
has_relations = bool(relation_field_names)
# Validate unknown kwargs up front so the dispatch loop doesn't need
# a per-iteration check. ``extra=ignore`` already filtered above, so
# in that branch no unknowns can remain.
if not extra_is_ignore:
for k in kwargs:
if k not in model_fields and k not in pydantic_fields:
try:
model_fields[k]
except KeyError as e:
raise ModelError(
f"Unknown field '{e.args[0]}' for model "
f"{self.get_name(lower=False)}"
)
if not has_json and not has_bytes and not has_relations:
# Fast path — plain model with no relations/json/bytes. The
# validation pass above is the only per-key cost; the value
# copy is a single C-level dict construction.
return dict(kwargs), through_tmp_dict
new_kwargs: dict[str, Any] = {}
for k, v in kwargs.items():
if k in relation_field_names:
v = model_fields[k].expand_relationship(v, self, to_register=False)
if has_json and k in json_fields:
v = encode_json(v)
if has_bytes and k in bytes_fields and v is not None:
v = decode_bytes(
value=v,
represent_as_string=model_fields[k].represent_as_base64_str,
)
new_kwargs[k] = v
return new_kwargs, through_tmp_dict
def _initialize_internal_attributes(self) -> None:
"""
Initializes internal attributes during __init__()
:rtype: None
"""
# object.__setattr__(self, "_orm_id", uuid.uuid4().hex)
object.__setattr__(self, "_orm_saved", False)
object.__setattr__(self, "_pk_column", None)
object.__setattr__(self, "__setattr_fields__", set())
object.__setattr__(
self,
"_orm",
RelationsManager(
related_fields=self.extract_related_fields(), owner=cast("Model", self)
),
)
def __eq__(self, other: object) -> bool:
"""
Compares other model to this model. when == is called.
:param other: other model to compare
:type other: object
:return: result of comparison
:rtype: bool
"""
if isinstance(other, NewBaseModel):
return self.__same__(other)
return super().__eq__(other) # pragma no cover
def __hash__(self) -> int:
cached = getattr(self, "__cached_hash__", None)
if cached is not None:
return cached
pk = self.pk
cls = type(self)
if pk is not None:
# ``type(self)`` hashes by identity in CPython, so ``hash((pk, cls))``
# is uniqueness-equivalent to the original ``str(pk) + cls.__name__``
# without two string allocations per call. This is the hot path —
# everything that goes through ``_relation_cache`` is keyed on
# saved-pk Models.
ret = hash((pk, cls))
else:
# Unsaved models can hold list/dict values in ``__dict__`` (json
# fields, reverse-relation slots), so we still ``str(vals)`` to
# keep the result hashable. Cold path; not perf-critical.
related = self.extract_related_names()
vals = {k: v for k, v in self.__dict__.items() if k not in related}
ret = hash((str(vals), cls))
object.__setattr__(self, "__cached_hash__", ret)
return ret
def __same__(self, other: "NewBaseModel") -> bool:
"""
Used by __eq__, compares other model to this model.
Saved models (both with pk) compare directly by ``(pk, type)`` to
skip the hash-cache fill on the *other* side. Unsaved/mixed states
fall through to the original hash-equality semantics.
:param other: model to compare to
:type other: NewBaseModel
:return: result of comparison
:rtype: bool
"""
if type(self) is not type(other):
return False # pragma: no cover
self_pk = self.pk
other_pk = other.pk
if self_pk is not None and other_pk is not None:
return self_pk == other_pk
if (self_pk is None) != (other_pk is None):
return False
else:
return hash(self) == other.__hash__()
@classmethod
def get_name(cls, lower: bool = True) -> str:
"""
Returns name of the Model class, by default lowercase.
:param lower: flag if name should be set to lowercase
:type lower: bool
:return: name of the model
:rtype: str
"""
if lower:
try:
return cls._lower_name # type: ignore[attr-defined]
except AttributeError:
cls._lower_name = cls.__name__.lower() # type: ignore[attr-defined]
return cls._lower_name # type: ignore[attr-defined]
return cls.__name__
@property
def pk_column(self) -> sqlalchemy.Column:
"""
Retrieves primary key sqlalchemy column from models OrmarConfig.table.
Each model has to have primary key.
Only one primary key column is allowed.
:return: primary key sqlalchemy column
:rtype: sqlalchemy.Column
"""
if object.__getattribute__(self, "_pk_column") is not None:
return object.__getattribute__(self, "_pk_column")
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
@property
def saved(self) -> bool:
"""Saved status of the model. Changed by setattr and loading from db"""
return self._orm_saved
@property
def signals(self) -> "SignalEmitter":
"""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.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.ormar_config.database.dialect.name
def remove(self, parent: "Model", name: str) -> None:
"""Removes child from relation with given name in RelationshipManager"""
self._orm.remove_parent(self, parent, name)
def set_save_status(self, status: bool) -> None:
"""Sets value of the save status"""
object.__setattr__(self, "_orm_saved", status)
@classmethod
def update_forward_refs(cls, **localns: Any) -> None:
"""
Processes fields that are ForwardRef and need to be evaluated into actual
models.
Expands relationships, register relation in alias manager and substitutes
sqlalchemy columns with new ones with proper column type (null before).
Populates OrmarConfig table of the Model which is left empty before.
Sets self_reference flag on models that links to themselves.
Calls the pydantic method to evaluate pydantic fields.
:param localns: local namespace
:type localns: Any
:return: None
:rtype: None
"""
globalns = sys.modules[cls.__module__].__dict__.copy()
globalns.setdefault(cls.__name__, cls)
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)
field.evaluate_forward_ref(globalns=globalns, localns=localns)
field.set_self_reference_flag()
if field.is_multi and not field.through:
field = cast(ormar.ManyToManyField, field)
field.create_default_through_model()
expand_reverse_relationship(model_field=field)
register_relation_in_alias_manager(field=field)
update_column_definition(model=cls, field=field)
populate_config_sqlalchemy_table_if_required(config=cls.ormar_config)
# super().update_forward_refs(**localns)
cls.model_rebuild(
force=True,
_types_namespace={
field.to.__name__: field.to
for field in fields_to_check.values()
if field.is_relation
},
)
cls.ormar_config.requires_ref_update = False
@staticmethod
def _extract_nested_models_from_list( # noqa: CFQ002
relation_map: builtins.dict,
models: MutableSequence,
include: Union[set, builtins.dict, None],
exclude: Union[set, builtins.dict, None],
exclude_primary_keys: bool,
exclude_through_models: bool,
flatten_map: Optional[FlattenMap] = None,
) -> list:
"""
Converts list of models into list of dictionaries.
:param models: list of models
:type models: list
:param include: fields to include
:type include: Union[set, dict, None]
:param exclude: fields to exclude
:type exclude: Union[set, dict, None]
:param flatten_map: FlattenMap of relations to render as pk values
:type flatten_map: Optional[FlattenMap]
:return: list of models converted to dictionaries
:rtype: list[dict]
"""
result = []
for model in models:
try:
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,
flatten_fields=flatten_map,
)
if not exclude_through_models:
model.populate_through_models(
model=model,
model_dict=model_dict,
include=include,
exclude=exclude,
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: builtins.dict,
include: Union[set, builtins.dict],
exclude: Union[set, builtins.dict],
relation_map: builtins.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
"""
models_to_populate = filter_not_excluded_fields(
fields=model.extract_through_names(),
include=normalize_to_dict(include),
exclude=normalize_to_dict(exclude),
)
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()
@staticmethod
def _resolve_field_descent( # noqa: CFQ002
field: str,
nested_model: Any,
flatten_map: Optional[FlattenMap],
exclude_list: bool,
relation_map: builtins.dict,
include: Optional[builtins.dict],
exclude: Optional[builtins.dict],
dict_instance: builtins.dict,
) -> Optional[NestedDescent]:
"""
Decide how to dump a related field. When the field is flattened, write
its primary-key representation directly into ``dict_instance`` and
return ``None`` so the caller skips further descent. When the field is
an excluded list, also return ``None``. Otherwise return a
``NestedDescent`` carrying the per-field locals the caller threads
into the recursive dump.
:param field: relation field name being processed
:type field: str
:param nested_model: value of the relation on the current instance
:type nested_model: Any
:param flatten_map: current flatten directive scope, or ``None``
:type flatten_map: Optional[FlattenMap]
:param exclude_list: whether to skip list-valued relations entirely
:type exclude_list: bool
:param relation_map: relation traversal map for the current scope
:type relation_map: dict
:param include: include selector for the current scope
:type include: Optional[dict]
:param exclude: exclude selector for the current scope
:type exclude: Optional[dict]
:param dict_instance: dict being built for the current model
:type dict_instance: dict
:return: per-field locals, or ``None`` if the field was handled inline
(flattened or skipped)
:rtype: Optional[NestedDescent]
"""
is_sequence = isinstance(nested_model, MutableSequence)
if is_sequence and exclude_list:
return None
if flatten_map is not None and flatten_map.is_field_flattened(field):
if is_sequence:
dict_instance[field] = [m.pk for m in nested_model]
else:
dict_instance[field] = (
nested_model.pk if nested_model is not None else None
)
return None
return NestedDescent(
flatten_map=flatten_map.descend(field) if flatten_map is not None else None,
relation_map=cast(
builtins.dict, skip_ellipsis(relation_map, field, default=dict())
),
include=convert_all(skip_ellipsis(include, field)),
exclude=convert_all(skip_ellipsis(exclude, field)),
)
def _extract_nested_models( # noqa: CCR001, CFQ002
self,
relation_map: builtins.dict,
dict_instance: builtins.dict,
include: Optional[builtins.dict],
exclude: Optional[builtins.dict],
exclude_primary_keys: bool,
exclude_through_models: bool,
exclude_list: bool,
flatten_map: Optional[FlattenMap] = None,
) -> builtins.dict:
"""
Traverse nested models and converts them into dictionaries.
Calls itself recursively if needed.
:param relation_map: map of the relations to follow to avoid circular deps
:type relation_map: dict
:param dict_instance: current instance dict
:type dict_instance: dict
:param include: fields to include
:type include: Optional[dict]
:param exclude: fields to exclude
:type exclude: Optional[dict]
:param exclude_list: whether to exclude list-valued relations
:type exclude_list: bool
:param flatten_map: FlattenMap directing relations to render as pk values
:type flatten_map: Optional[FlattenMap]
:return: current model dict with child models converted to dictionaries
:rtype: dict
"""
fields = filter_not_excluded_fields(
fields=self.extract_related_names(), include=include, exclude=exclude
)
for field in fields:
if not relation_map or field not in relation_map:
continue
try:
nested_model = getattr(self, field)
descent = self._resolve_field_descent(
field=field,
nested_model=nested_model,
flatten_map=flatten_map,
exclude_list=exclude_list,
relation_map=relation_map,
include=include,
exclude=exclude,
dict_instance=dict_instance,
)
if descent is None:
continue
if isinstance(nested_model, MutableSequence):
dict_instance[field] = self._extract_nested_models_from_list(
relation_map=descent.relation_map,
models=nested_model,
include=descent.include,
exclude=descent.exclude,
exclude_primary_keys=exclude_primary_keys,
exclude_through_models=exclude_through_models,
flatten_map=descent.flatten_map,
)
elif nested_model is not None:
model_dict = nested_model.model_dump(
relation_map=descent.relation_map,
include=descent.include,
exclude=descent.exclude,
exclude_primary_keys=exclude_primary_keys,
exclude_through_models=exclude_through_models,
flatten_fields=descent.flatten_map,
)
if not exclude_through_models:
nested_model.populate_through_models(
model=nested_model,
model_dict=model_dict,
include=descent.include,
exclude=descent.exclude,
relation_map=descent.relation_map,
)
dict_instance[field] = model_dict
else:
dict_instance[field] = None
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, builtins.dict, None] = None,
exclude: Union[set, builtins.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[builtins.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, CFQ002
self,
*,
mode: Union[Literal["json", "python"], str] = "python",
include: Union[set, builtins.dict, None] = None,
exclude: Union[set, builtins.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[builtins.dict] = None,
flatten_fields: Union[
set, list, str, tuple, builtins.dict, "FieldAccessor", FlattenMap, None
] = None,
flatten_all: bool = False,
round_trip: bool = False,
warnings: bool = True,
) -> "DictStrAny": # noqa: A003
"""
Generate a dictionary representation of the model,
optionally specifying which fields to include or exclude.
Nested models are also parsed to dictionaries.
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
:param exclude_primary_keys: flag to exclude primary keys from dict
:type exclude_primary_keys: bool
:param include: fields to include
:type include: Union[set, dict, None]
:param exclude: fields to exclude
:type exclude: Union[set, dict, None]
:param by_alias: flag to get values by alias - passed to pydantic
:type by_alias: 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
:type exclude_defaults: bool
:param exclude_none: flag to exclude None values - passed to pydantic
: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 circular deps
:type relation_map: dict
:param flatten_fields: relations to render as their primary-key value.
Accepts dunder-strings, list/set/tuple, nested dict (with Ellipsis),
or ``FieldAccessor`` / list of accessors. Resolves to a nested-dict
spec used during traversal.
:type flatten_fields: Union[set, list, str, tuple, dict, FieldAccessor, None]
:param flatten_all: if True every nested relation is collapsed to its pk
:type flatten_all: bool
: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:
"""
flatten_map = self._resolve_flatten_map(
flatten_fields=flatten_fields, flatten_all=flatten_all
)
if exclude_primary_keys and flatten_map:
raise QueryDefinitionError(
"flatten_fields / flatten_all cannot be combined with "
"exclude_primary_keys=True: flattening renders primary keys, "
"excluding removes them."
)
include_dict = normalize_to_dict(include)
exclude_dict = normalize_to_dict(exclude)
if flatten_map is not None:
flatten_map.check_vs_selector(include_dict, "include")
flatten_map.check_vs_selector(exclude_dict, "exclude")
pydantic_exclude = self._update_excluded_with_related(exclude)
pydantic_exclude = self._update_excluded_with_pks_and_through(
exclude=pydantic_exclude,
exclude_primary_keys=exclude_primary_keys,
exclude_through_models=exclude_through_models,
)
dict_instance = super().model_dump(
mode=mode,
include=include,
exclude=pydantic_exclude,
by_alias=by_alias,
exclude_defaults=exclude_defaults,
exclude_unset=exclude_unset,
exclude_none=exclude_none,
round_trip=round_trip,
warnings=False,
)
dict_instance = {
k: self._convert_bytes_to_str(column_name=k, value=v)
for k, v in dict_instance.items()
}
relation_map = (
relation_map
if relation_map is not None
else translate_list_to_dict(self._iterate_related_models())
)
pk_only = getattr(self, "__pk_only__", False)
if relation_map and not pk_only:
dict_instance = self._extract_nested_models(
relation_map=relation_map,
dict_instance=dict_instance,
include=include_dict,
exclude=exclude_dict,
exclude_primary_keys=exclude_primary_keys,
exclude_through_models=exclude_through_models,
exclude_list=exclude_list,
flatten_map=flatten_map,
)
return dict_instance
def _resolve_flatten_map(
self,
flatten_fields: Union[
set, list, str, tuple, builtins.dict, "FieldAccessor", FlattenMap, None
],
flatten_all: bool,
) -> Optional[FlattenMap]:
"""
Produce a :class:`FlattenMap` from the various input forms accepted by
``model_dump``. Resolves dunder strings / lists / sets / nested dicts /
``FieldAccessor`` chains; an already-resolved ``FlattenMap`` (passed
during recursion) is returned as-is.
:param flatten_fields: user spec, nested dict, or FlattenMap from recursion
:type flatten_fields: Union[set, list, str, tuple, dict, FieldAccessor,
FlattenMap, None]
:param flatten_all: if True every relation flattens regardless of spec
:type flatten_all: bool
:return: resolved FlattenMap, or None when nothing should flatten
:rtype: Optional[FlattenMap]
"""
if flatten_all:
return FlattenMap(flatten_all=True)
if flatten_fields is None:
excludable = getattr(self, "__ormar_excludable__", None)
return excludable.flatten_map() if excludable is not None else None
if isinstance(flatten_fields, FlattenMap):
return flatten_fields
if isinstance(flatten_fields, builtins.dict):
return FlattenMap(data=flatten_fields)
excludable = ExcludableItems()
excludable.build(
items=extract_access_chains(flatten_fields),
model_cls=cast(type["Model"], type(self)),
slot="flatten",
)
return excludable.flatten_map()
@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, builtins.dict, None] = None,
exclude: Union[set, builtins.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, builtins.dict, None] = None,
exclude: Union[set, builtins.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,
flatten_fields: Union[
set, list, str, tuple, builtins.dict, "FieldAccessor", FlattenMap, None
] = None,
flatten_all: bool = False,
**dumps_kwargs: Any,
) -> str:
"""
Generate a JSON representation of the model, `include` and `exclude`
arguments as per `dict()`.
`encoder` is an optional function to supply as `default` to json.dumps(),
other arguments as per `json.dumps()`.
"""
data = 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,
flatten_fields=flatten_fields,
flatten_all=flatten_all,
)
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 = {
k: v for k, v in values.items() if k not in cls.extract_related_names()
}
model = cls.__new__(cls)
fields_values: dict[str, Any] = {}
for name, field in cls.model_fields.items():
if name in own_values:
fields_values[name] = own_values[name]
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)
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[builtins.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
def _construct_relations(cls: type["T"], model: "T", values: builtins.dict) -> None:
present_relations = [
relation for relation in cls.extract_related_names() if relation in values
]
for relation in present_relations:
value_to_set = values[relation]
if not isinstance(value_to_set, list):
value_to_set = [value_to_set]
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
if x is not None
]
for child in relation_value:
model._orm.add(
parent=cast("Model", child),
child=cast("Model", model),
field=cast("ForeignKeyField", relation_field),
)
def update_from_dict(self, value_dict: builtins.dict) -> "NewBaseModel":
"""
Updates self with values of fields passed in the dictionary.
:param value_dict: dictionary of fields names and values
:type value_dict: dict
:return: self
:rtype: NewBaseModel
"""
for key, value in value_dict.items():
setattr(self, key, value)
return self
def _convert_bytes_to_str(
self, column_name: str, value: Any
) -> Union[str, builtins.dict]:
"""
Converts value to str from bytes for represent_as_base64_str columns.
:param column_name: name of the field
:type column_name: str
:param value: value fo the field
:type value: Any
:return: converted value if needed, else original value
:rtype: Any
"""
if column_name not in self._bytes_fields:
return value
field = self.ormar_config.model_fields[column_name]
if (
value is not None
and not isinstance(value, str)
and field.represent_as_base64_str
):
return base64.b64encode(value).decode()
return value
def _extract_own_model_fields(self) -> builtins.dict:
"""
Returns a dictionary with field names and values for fields that are not
relations fields (ForeignKey, ManyToMany etc.)
:return: dictionary of fields names and values.
:rtype: dict
"""
related_names = self.extract_related_names()
self_fields = {k: v for k, v in self.__dict__.items() if k not in related_names}
return self_fields
def _extract_model_db_fields(self) -> builtins.dict:
"""
Returns a dictionary with field names and values for fields that are stored in
current model's table.
That includes own non-relational fields ang foreign key fields.
:return: dictionary of fields names and values.
:rtype: dict
"""
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.ormar_config.table.columns
}
for field in self._extract_db_related_names():
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]:
raise ModelPersistenceError(
f"You cannot save {relation_field.to.get_name()} "
f"model without pk set!"
)
return self_fields
ormar-orm-ormar-a7e7fde/ormar/models/ormar_config.py 0000664 0000000 0000000 00000010715 15211567312 0022760 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Optional, Union
import sqlalchemy
from pydantic_core import PydanticUndefined, PydanticUndefinedType
from sqlalchemy.ext.asyncio import AsyncEngine
from sqlalchemy.sql.schema import ColumnCollectionConstraint
from ormar.databases.connection import DatabaseConnection
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: DatabaseConnection
engine: AsyncEngine
tablename: str
schema: Optional[str]
comment: Optional[str]
order_by: list[str]
abstract: bool
proxy: bool
emit_parent_signals: bool
exclude_parent_fields: list[str]
constraints: list[ColumnCollectionConstraint]
def __init__(
self,
metadata: Optional[sqlalchemy.MetaData] = None,
database: Optional[DatabaseConnection] = None,
engine: Optional[AsyncEngine] = None,
tablename: Optional[str] = None,
schema: Optional[str] = None,
comment: Optional[str] = None,
order_by: Optional[list[str]] = None,
abstract: bool = False,
proxy: bool = False,
emit_parent_signals: bool = False,
queryset_class: type[QuerySet] = QuerySet,
extra: Extra = Extra.forbid,
constraints: Optional[list[ColumnCollectionConstraint]] = None,
) -> None:
self.pkname = None # type: ignore
self.metadata = metadata # type: ignore
self.database = database # type: ignore
self.engine = engine # type: ignore
self.tablename = tablename # type: ignore
self.schema = schema
self.comment = comment
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.proxy = proxy
self.emit_parent_signals = emit_parent_signals
self.requires_ref_update: bool = False
self.extra = extra
self.queryset_class = queryset_class
self.table: sqlalchemy.Table = None # type: ignore
def copy(
self,
metadata: Optional[sqlalchemy.MetaData] = None,
database: Optional[DatabaseConnection] = None,
engine: Optional[AsyncEngine] = None,
tablename: Optional[str] = None,
schema: Union[str, None, PydanticUndefinedType] = PydanticUndefined,
comment: Union[str, None, PydanticUndefinedType] = PydanticUndefined,
order_by: Optional[list[str]] = None,
abstract: Optional[bool] = None,
proxy: Optional[bool] = None,
emit_parent_signals: Optional[bool] = None,
queryset_class: Optional[type[QuerySet]] = None,
extra: Optional[Extra] = None,
constraints: Optional[list[ColumnCollectionConstraint]] = None,
) -> "OrmarConfig":
# PydanticUndefined distinguishes "omitted" (inherit parent schema)
# from an explicit ``schema=None`` (clear schema).
resolved_schema = (
self.schema if isinstance(schema, PydanticUndefinedType) else schema
)
resolved_comment = (
self.comment if isinstance(comment, PydanticUndefinedType) else comment
)
return OrmarConfig(
metadata=metadata or self.metadata,
database=database or self.database,
engine=engine or self.engine,
tablename=tablename,
schema=resolved_schema,
comment=resolved_comment,
order_by=order_by,
abstract=abstract or self.abstract,
proxy=proxy if proxy is not None else self.proxy,
emit_parent_signals=(
emit_parent_signals
if emit_parent_signals is not None
else self.emit_parent_signals
),
queryset_class=queryset_class or self.queryset_class,
extra=extra or self.extra,
constraints=constraints,
)
ormar-orm-ormar-a7e7fde/ormar/models/quick_access_views.py 0000664 0000000 0000000 00000003457 15211567312 0024172 0 ustar 00root root 0000000 0000000 """
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",
"model_config",
"model_fields",
"__cached_hash__",
"__class__",
"__config__",
"__custom_root_type__",
"__dict__",
"model_fields",
"__fields_set__",
"__json_encoder__",
"__ormar_excludable__",
"__pk_only__",
"__post_root_validators__",
"__pre_root_validators__",
"__private_attributes__",
"__same__",
"_calculate_keys",
"_extract_db_related_names",
"_extract_model_db_fields",
"_extract_nested_models",
"_extract_nested_models_from_list",
"_extract_own_model_fields",
"_extract_related_model_instead_of_field",
"_get_value",
"_init_private_attributes",
"_is_conversion_to_json_needed",
"_iter",
"_iterate_related_models",
"_orm",
"_orm_id",
"_orm_saved",
"_related_names",
"_update_and_follow",
"_update_excluded_with_related_not_required",
"_verify_model_can_be_initialized",
"copy",
"delete",
"dict",
"extract_related_names",
"extract_related_fields",
"extract_through_names",
"update_from_dict",
"get_column_alias",
"get_column_name_from_alias",
"get_filtered_names_to_extract",
"get_name",
"get_properties",
"get_related_field_name",
"get_relation_model_id",
"json",
"keys",
"load",
"load_all",
"pk_column",
"pk_type",
"populate_default_values",
"prepare_model_to_save",
"remove",
"resolve_relation_field",
"resolve_relation_name",
"save",
"save_related",
"saved",
"set_save_status",
"signals",
"translate_aliases_to_columns",
"translate_columns_to_aliases",
"update",
"upsert",
}
ormar-orm-ormar-a7e7fde/ormar/models/traversible.py 0000664 0000000 0000000 00000007401 15211567312 0022633 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING: # pragma no cover
from ormar.models.mixins.relation_mixin import RelationMixin
class NodeList:
"""
Helper class that helps with iterating nested models
"""
def __init__(self) -> None:
self.node_list: list["Node"] = []
def __getitem__(self, item: Any) -> Any:
return self.node_list.__getitem__(item)
def add(
self,
node_class: type["RelationMixin"],
relation_name: Optional[str] = None,
parent_node: Optional["Node"] = None,
) -> "Node":
"""
Adds new Node or returns the existing one
:param node_class: Model in current node
:type node_class: ormar.models.metaclass.ModelMetaclass
:param relation_name: name of the current relation
:type relation_name: str
:param parent_node: parent node
:type parent_node: Optional[Node]
:return: returns new or already existing node
:rtype: Node
"""
existing_node = self.find(
relation_name=relation_name, node_class=node_class, parent_node=parent_node
)
if not existing_node:
current_node = Node(
node_class=node_class,
relation_name=relation_name,
parent_node=parent_node,
)
self.node_list.append(current_node)
return current_node
return existing_node # pragma: no cover
def find(
self,
node_class: type["RelationMixin"],
relation_name: Optional[str] = None,
parent_node: Optional["Node"] = None,
) -> Optional["Node"]:
"""
Searches for existing node with given parameters
:param node_class: Model in current node
:type node_class: ormar.models.metaclass.ModelMetaclass
:param relation_name: name of the current relation
:type relation_name: str
:param parent_node: parent node
:type parent_node: Optional[Node]
:return: returns already existing node or None
:rtype: Optional[Node]
"""
for node in self.node_list:
if (
node.node_class == node_class
and node.parent_node == parent_node
and node.relation_name == relation_name
):
return node # pragma: no cover
return None
class Node:
def __init__(
self,
node_class: type["RelationMixin"],
relation_name: Optional[str] = None,
parent_node: Optional["Node"] = None,
) -> None:
self.relation_name = relation_name
self.node_class = node_class
self.parent_node = parent_node
self.visited_children: list["Node"] = []
if self.parent_node:
self.parent_node.visited_children.append(self)
def __repr__(self) -> str: # pragma: no cover
return (
f"{self.node_class.get_name(lower=False)}, "
f"relation:{self.relation_name}, "
f"parent: {self.parent_node}"
)
def visited(self, relation_name: str) -> bool:
"""
Checks if given relation was already visited.
Relation was visited if it's name is in current node children.
Relation was visited if one of the parent node had the same Model class
:param relation_name: name of relation
:type relation_name: str
:return: result of the check
:rtype: bool
"""
target_model = self.node_class.ormar_config.model_fields[relation_name].to
if self.parent_node:
node = self
while node.parent_node:
node = node.parent_node
if node.node_class == target_model:
return True
return False
ormar-orm-ormar-a7e7fde/ormar/models/utils.py 0000664 0000000 0000000 00000000134 15211567312 0021445 0 ustar 00root root 0000000 0000000 from enum import Enum
class Extra(str, Enum):
ignore = "ignore"
forbid = "forbid"
ormar-orm-ormar-a7e7fde/ormar/protocols/ 0000775 0000000 0000000 00000000000 15211567312 0020476 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/protocols/__init__.py 0000664 0000000 0000000 00000000262 15211567312 0022607 0 ustar 00root root 0000000 0000000 from ormar.protocols.queryset_protocol import QuerySetProtocol
from ormar.protocols.relation_protocol import RelationProtocol
__all__ = ["QuerySetProtocol", "RelationProtocol"]
ormar-orm-ormar-a7e7fde/ormar/protocols/queryset_protocol.py 0000664 0000000 0000000 00000003466 15211567312 0024663 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Any, Optional, Sequence, Union
try:
from typing import Protocol
except ImportError: # pragma: nocover
from typing_extensions import Protocol # type: ignore
if TYPE_CHECKING: # noqa: C901; #pragma nocover
from ormar import Model
from ormar.relations.querysetproxy import QuerysetProxy
class QuerySetProtocol(Protocol): # pragma: nocover
def filter(self, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001
...
def exclude(self, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001
...
def select_related(self, related: Union[list, str]) -> "QuerysetProxy": ...
def prefetch_related(self, related: Union[list, str]) -> "QuerysetProxy": ...
async def exists(self) -> bool: ...
async def count(self, distinct: bool = True) -> int: ...
async def clear(self) -> int: ...
def limit(self, limit_count: int) -> "QuerysetProxy": ...
def offset(self, offset: int) -> "QuerysetProxy": ...
async def first(self, **kwargs: Any) -> "Model": ...
async def get(self, **kwargs: Any) -> "Model": ...
async def all( # noqa: A003, A001
self, **kwargs: Any
) -> Sequence[Optional["Model"]]: ...
async def create(self, **kwargs: Any) -> "Model": ...
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]: ...
async def update_or_create(self, **kwargs: Any) -> "Model": ...
def 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": ...
ormar-orm-ormar-a7e7fde/ormar/protocols/relation_protocol.py 0000664 0000000 0000000 00000000641 15211567312 0024607 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Union
try:
from typing import Protocol
except ImportError: # pragma: nocover
from typing_extensions import Protocol # type: ignore
if TYPE_CHECKING: # pragma: nocover
from ormar import Model
class RelationProtocol(Protocol): # pragma: nocover
def add(self, child: "Model") -> None: ...
def remove(self, child: Union["Model", type["Model"]]) -> None: ...
ormar-orm-ormar-a7e7fde/ormar/py.typed 0000664 0000000 0000000 00000000000 15211567312 0020137 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/queryset/ 0000775 0000000 0000000 00000000000 15211567312 0020333 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/queryset/__init__.py 0000664 0000000 0000000 00000001205 15211567312 0022442 0 ustar 00root root 0000000 0000000 """
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 NullsOrdering, and_, or_
from ormar.queryset.field_accessor import FieldAccessor
from ormar.queryset.queries import FilterQuery, LimitQuery, OffsetQuery, OrderQuery
from ormar.queryset.queryset import QuerySet
__all__ = [
"QuerySet",
"FilterQuery",
"LimitQuery",
"OffsetQuery",
"OrderQuery",
"FilterAction",
"OrderAction",
"SelectAction",
"NullsOrdering",
"and_",
"or_",
"FieldAccessor",
]
ormar-orm-ormar-a7e7fde/ormar/queryset/actions/ 0000775 0000000 0000000 00000000000 15211567312 0021773 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/queryset/actions/__init__.py 0000664 0000000 0000000 00000000363 15211567312 0024106 0 ustar 00root root 0000000 0000000 from ormar.queryset.actions.filter_action import FilterAction
from ormar.queryset.actions.order_action import OrderAction
from ormar.queryset.actions.select_action import SelectAction
__all__ = ["FilterAction", "OrderAction", "SelectAction"]
ormar-orm-ormar-a7e7fde/ormar/queryset/actions/filter_action.py 0000664 0000000 0000000 00000012051 15211567312 0025166 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Any
from sqlalchemy import TextClause
import ormar # noqa: I100, I202
from ormar.exceptions import QueryDefinitionError
from ormar.queryset.actions.query_action import QueryAction
if TYPE_CHECKING: # pragma: nocover
from ormar import Model
FILTER_OPERATORS = {
"exact": "__eq__",
"iexact": "ilike",
"contains": "like",
"icontains": "ilike",
"startswith": "like",
"istartswith": "ilike",
"endswith": "like",
"iendswith": "ilike",
"isnull": "is_",
"in": "in_",
"gt": "__gt__",
"gte": "__ge__",
"lt": "__lt__",
"lte": "__le__",
}
METHODS_TO_OPERATORS = {
"__eq__": "exact",
"__mod__": "contains",
"__gt__": "gt",
"__ge__": "gte",
"__lt__": "lt",
"__le__": "lte",
"iexact": "iexact",
"contains": "contains",
"icontains": "icontains",
"startswith": "startswith",
"istartswith": "istartswith",
"endswith": "endswith",
"iendswith": "iendswith",
"isnull": "isnull",
"in": "in",
}
ESCAPE_CHARACTERS = ["%", "_"]
class FilterAction(QueryAction):
"""
Represents a single WHERE predicate on a queryset.
Created for each ``key=value`` (or ``key__op=value``) passed to
``filter(...)`` / ``exclude(...)`` and to filtering shortcuts like
``get(...)`` / ``first(...)``.
"""
def __init__(self, filter_str: str, value: Any, model_cls: type["Model"]) -> None:
super().__init__(query_str=filter_str, model_cls=model_cls)
self.filter_value = value
self._escape_characters_in_clause()
def has_escaped_characters(self) -> bool:
"""Check if value is a string that contains characters to escape"""
return isinstance(self.filter_value, str) and any(
c for c in ESCAPE_CHARACTERS if c in self.filter_value
)
def _split_value_into_parts(self, query_str: str) -> None:
parts = query_str.split("__")
if parts[-1] in FILTER_OPERATORS:
self.operator = parts[-1]
self.field_name = parts[-2]
self.related_parts = parts[:-2]
else:
self.operator = "exact"
self.field_name = parts[-1]
self.related_parts = parts[:-1]
def _escape_characters_in_clause(self) -> None:
"""
Escapes the special characters ["%", "_"] if needed.
Adds `%` for `like` queries.
:raises QueryDefinitionError: if contains or icontains is used with
ormar model instance
:return: escaped value and flag if escaping is needed
:rtype: tuple[Any, bool]
"""
self.has_escaped_character = False
if self.operator in [
"contains",
"icontains",
"startswith",
"istartswith",
"endswith",
"iendswith",
]:
if isinstance(self.filter_value, ormar.Model):
raise QueryDefinitionError(
"You cannot use contains and icontains with instance of the Model"
)
self.has_escaped_character = self.has_escaped_characters()
if self.has_escaped_character:
self._escape_chars()
self._prefix_suffix_quote()
def _escape_chars(self) -> None:
"""Actually replaces chars to escape in value"""
for char in ESCAPE_CHARACTERS:
self.filter_value = self.filter_value.replace(char, f"\\{char}")
def _prefix_suffix_quote(self) -> None:
"""
Adds % to the beginning of the value if operator checks for containment and not
starts with.
Adds % to the end of the value if operator checks for containment and not
end with.
:return:
:rtype:
"""
prefix = "%" if "start" not in self.operator else ""
sufix = "%" if "end" not in self.operator else ""
self.filter_value = f"{prefix}{self.filter_value}{sufix}"
def get_text_clause(self) -> TextClause:
"""
Escapes characters if it's required.
Substitutes values of the models if value is a ormar Model with its pk value.
Compiles the clause.
:return: complied and escaped clause
:rtype: sqlalchemy.sql.elements.TextClause
"""
if isinstance(self.filter_value, ormar.Model):
self.filter_value = self.filter_value.pk
op_attr = FILTER_OPERATORS[self.operator]
if self.operator == "isnull":
op_attr = "is_" if self.filter_value else "isnot"
filter_value = None
else:
filter_value = self.filter_value
if self.table_prefix:
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:
aliased_column = self.column
clause = getattr(aliased_column, op_attr)(filter_value)
if self.has_escaped_character:
clause.modifiers["escape"] = "\\"
return clause
ormar-orm-ormar-a7e7fde/ormar/queryset/actions/order_action.py 0000664 0000000 0000000 00000015636 15211567312 0025030 0 ustar 00root root 0000000 0000000 import copy
from typing import TYPE_CHECKING, Optional
import sqlalchemy
from sqlalchemy import text
from ormar.queryset.actions.query_action import QueryAction # noqa: I100, I202
if TYPE_CHECKING: # pragma: nocover
from ormar import Model
class OrderAction(QueryAction):
"""
Represents a single ORDER BY directive on a queryset.
Created when ``order_by(...)`` or ``Field.asc()`` / ``Field.desc()`` is
used, and internally to apply the default primary-key ordering.
"""
def __init__(
self,
order_str: str,
model_cls: type["Model"],
alias: Optional[str] = None,
nulls_ordering: Optional[str] = None,
) -> None:
self.direction: str = ""
super().__init__(query_str=order_str, model_cls=model_cls)
self.is_source_model_order = False
if alias:
self.table_prefix = alias
if self.source_model == self.target_model and "__" not in self.related_str:
self.is_source_model_order = True
self.nulls_ordering = nulls_ordering
@property
def field_alias(self) -> str:
return self.target_model.get_column_alias(self.field_name)
@property
def is_postgres_bool(self) -> bool:
dialect = self.target_model.ormar_config.database.dialect.name
field_type = self.target_model.ormar_config.model_fields[
self.field_name
].__type__
return dialect == "postgresql" and field_type is bool
def get_field_name_text(self) -> str:
"""
Escapes characters if it's required.
Substitutes values of the models if value is a ormar Model with its pk value.
Compiles the clause.
:return: complied and escaped clause
:rtype: sqlalchemy.sql.elements.TextClause
"""
prefix = f"{self.table_prefix}_" if self.table_prefix else ""
return f"{prefix}{self.table.name}.{self.field_alias}"
def get_min_or_max(self) -> sqlalchemy.sql.expression.TextClause:
"""
Used in limit sub queries where you need to use aggregated functions
in order to order by columns not included in group by. For postgres bool
field it's using bool_or function as aggregates does not work with this type
of columns.
:return: min or max function to order
:rtype: sqlalchemy.sql.elements.TextClause
"""
reference = self.get_field_name_text()
if self.direction == "":
function = "min" if not self.is_postgres_bool else "bool_or"
return text(f"{function}({reference})")
function = "max" if not self.is_postgres_bool else "bool_or"
return text(f"{function}({reference}) desc")
def get_text_clause(self) -> sqlalchemy.sql.expression.TextClause:
"""
Escapes characters if it's required.
Substitutes values of the models if value is a ormar Model with its pk value.
Compiles the clause.
:return: complied and escaped clause
:rtype: sqlalchemy.sql.elements.TextClause
"""
dialect = self.target_model.ormar_config.database.dialect
quoter = dialect.identifier_preparer.quote
prefix = f"{self.table_prefix}_" if self.table_prefix else ""
table_name = self.table.name
field_name = self.field_alias
if not prefix:
table_name = quoter(table_name)
else:
table_name = quoter(f"{prefix}{table_name}")
field_name = quoter(field_name)
return text(self._build_order_expression(f"{table_name}.{field_name}"))
def _build_order_expression(self, full_column: str) -> str:
"""
Builds the final ORDER BY expression for a fully-qualified column,
optionally including a `NULLS FIRST`/`NULLS LAST` annotation. On MySQL,
which lacks the SQL:2003 `NULLS` syntax, emulate it by prepending an
`IS NULL` / `IS NOT NULL` sort key — it only affects ordering,
not the result set.
:param full_column: fully-qualified, quoted column reference
:type full_column: str
:return: ORDER BY expression as raw SQL text
:rtype: str
"""
direction = f" {self.direction}" if self.direction else ""
base = f"{full_column}{direction}"
if self.nulls_ordering is None:
return base
dialect_name = self.target_model.ormar_config.database.dialect.name
if dialect_name == "mysql": # pragma: no cover
not_kw = "not " if self.nulls_ordering == "first" else ""
return f"{full_column} is {not_kw}null, {base}"
return f"{base} nulls {self.nulls_ordering}" # pragma: no cover
def _split_value_into_parts(self, order_str: str) -> None:
if order_str.startswith("-"):
self.direction = "desc"
order_str = order_str[1:]
parts = order_str.split("__")
self.field_name = parts[-1]
self.related_parts = parts[:-1]
@classmethod
def from_model_defaults(cls, model_cls: type["Model"]) -> list["OrderAction"]:
"""
Builds the default list of ``OrderAction`` instances from a model's
``OrmarConfig.orders_by`` (which always contains at least the primary
key, populated by the metaclass).
:param model_cls: model class whose defaults should be used
:type model_cls: type["Model"]
:return: list of default OrderAction instances for the model
:rtype: list[OrderAction]
"""
return [
cls(order_str=str(name), model_cls=model_cls)
for name in model_cls.ormar_config.orders_by
]
def flipped(self) -> "OrderAction":
"""
Returns a shallow copy of this order action with the sort direction
and any `NULLS FIRST`/`NULLS LAST` annotation inverted.
Used by reverse slicing to turn an ASC/DESC ordering into its mirror
image so that ``LIMIT N`` fetches rows from the tail of the original
ordering. Callers are responsible for reversing the result list in
memory afterwards so the caller-visible ordering is preserved.
:return: new OrderAction with flipped direction and nulls ordering
:rtype: OrderAction
"""
flipped = copy.copy(self)
flipped.direction = "" if self.direction == "desc" else "desc"
if self.nulls_ordering == "first":
flipped.nulls_ordering = "last"
elif self.nulls_ordering == "last":
flipped.nulls_ordering = "first"
return flipped
def check_if_filter_apply(self, target_model: type["Model"], alias: str) -> bool:
"""
Checks filter conditions to find if they apply to current join.
:param target_model: model which is now processed
:type target_model: type["Model"]
:param alias: prefix of the relation
:type alias: str
:return: result of the check
:rtype: bool
"""
return target_model == self.target_model and alias == self.table_prefix
ormar-orm-ormar-a7e7fde/ormar/queryset/actions/query_action.py 0000664 0000000 0000000 00000006212 15211567312 0025050 0 ustar 00root root 0000000 0000000 import abc
from typing import TYPE_CHECKING, Any
import sqlalchemy
from ormar.queryset.utils import get_relationship_alias_model_and_str # noqa: I202
if TYPE_CHECKING: # pragma: nocover
from ormar import Model
class QueryAction(abc.ABC):
"""
Base QueryAction class with common params for Filter and Order actions.
"""
def __init__(self, query_str: str, model_cls: type["Model"]) -> None:
self.query_str = query_str
self.field_name: str = ""
self.related_parts: list[str] = []
self.related_str: str = ""
self.table_prefix = ""
self.source_model = model_cls
self.target_model = model_cls
self.is_through = False
self._split_value_into_parts(query_str)
self._determine_filter_target_table()
def __eq__(self, other: object) -> bool: # pragma: no cover
if not isinstance(other, QueryAction):
return False
return self.query_str == other.query_str
def __hash__(self) -> Any:
return hash((self.table_prefix, self.query_str))
@abc.abstractmethod
def _split_value_into_parts(self, query_str: str) -> None: # pragma: no cover
"""
Splits string into related parts and field_name
:param query_str: query action string to split (i..e filter or order by)
:type query_str: str
"""
pass
@abc.abstractmethod
def get_text_clause(
self,
) -> sqlalchemy.sql.expression.ClauseElement: # pragma: no cover
pass
@property
def table(self) -> sqlalchemy.Table:
"""Shortcut to sqlalchemy Table of filtered target model"""
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.ormar_config.table.columns[aliased_name]
def update_select_related(self, select_related: list[str]) -> list[str]:
"""
Updates list of select related with related part included in the filter key.
That way If you want to just filter by relation you do not have to provide
select_related separately.
:param select_related: list of relation join strings
:type select_related: list[str]
:return: list of relation joins with implied joins from filter added
:rtype: list[str]
"""
select_related = select_related[:]
if self.related_str and not any(
rel.startswith(self.related_str) for rel in select_related
):
select_related.append(self.related_str)
return select_related
def _determine_filter_target_table(self) -> None:
"""
Walks the relation to retrieve the actual model on which the clause should be
constructed, extracts alias based on last relation leading to target model.
"""
(
self.table_prefix,
self.target_model,
self.related_str,
self.is_through,
) = get_relationship_alias_model_and_str(self.source_model, self.related_parts)
ormar-orm-ormar-a7e7fde/ormar/queryset/actions/select_action.py 0000664 0000000 0000000 00000003127 15211567312 0025164 0 ustar 00root root 0000000 0000000 import decimal
from typing import TYPE_CHECKING, Any, Callable
import sqlalchemy
from ormar.queryset.actions.query_action import QueryAction # noqa: I202
if TYPE_CHECKING: # pragma: no cover
from ormar import Model
class SelectAction(QueryAction):
"""
Represents a single column target for an aggregate function on a queryset.
Created when ``.min()`` / ``.max()`` / ``.sum()`` / ``.avg()`` is called,
optionally traversing relations via ``__``.
"""
def __init__(self, select_str: str, model_cls: type["Model"]) -> None:
super().__init__(query_str=select_str, model_cls=model_cls)
def _split_value_into_parts(self, order_str: str) -> None:
parts = order_str.split("__")
self.field_name = parts[-1]
self.related_parts = parts[:-1]
@property
def is_numeric(self) -> bool:
return self.get_target_field_type() in [int, float, decimal.Decimal]
def get_target_field_type(self) -> Any:
return self.target_model.ormar_config.model_fields[self.field_name].__type__
def get_text_clause(self) -> sqlalchemy.sql.expression.ColumnClause:
alias = f"{self.table_prefix}_" if self.table_prefix else ""
return sqlalchemy.column(f"{alias}{self.field_name}")
def apply_func(
self, func: Callable, use_label: bool = True
) -> sqlalchemy.sql.expression.ColumnElement:
result = func(self.get_text_clause())
if use_label:
rel_prefix = f"{self.related_str}__" if self.related_str else ""
result = result.label(f"{rel_prefix}{self.field_name}")
return result
ormar-orm-ormar-a7e7fde/ormar/queryset/clause.py 0000664 0000000 0000000 00000032022 15211567312 0022160 0 ustar 00root root 0000000 0000000 from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING, Any, Generator, Optional, Union
import sqlalchemy
from sqlalchemy import ColumnElement
import ormar # noqa I100
from ormar.models.helpers.models import group_related_list, ordered_join_paths
from ormar.queryset.actions.filter_action import FilterAction
from ormar.queryset.utils import get_relationship_alias_model_and_str
if TYPE_CHECKING: # pragma no cover
from ormar import Model
class FilterType(Enum):
AND = 1
OR = 2
class NullsOrdering(str, Enum):
"""Nulls placement options for the `.order_by()` queries."""
FIRST = "first"
LAST = "last"
class FilterGroup:
"""
Filter groups are used in complex queries condition to group and and or
clauses in where condition
"""
def __init__(
self,
*args: Any,
_filter_type: FilterType = FilterType.AND,
_exclude: bool = False,
**kwargs: Any,
) -> None:
self.filter_type = _filter_type
self.exclude = _exclude
self._nested_groups: list["FilterGroup"] = list(args)
self._resolved = False
self.is_source_model_filter = False
self._kwargs_dict = kwargs
self.actions: list[FilterAction] = []
def __and__(self, other: "FilterGroup") -> "FilterGroup":
return FilterGroup(self, other)
def __or__(self, other: "FilterGroup") -> "FilterGroup":
return FilterGroup(self, other, _filter_type=FilterType.OR)
def __invert__(self) -> "FilterGroup":
self.exclude = not self.exclude
return self
def resolve(
self,
model_cls: type["Model"],
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
complex relation prefixes if needed and nested groups also resolved.
:param model_cls: model from which the query is run
:type model_cls: type["Model"]
:param select_related: list of models to join
:type select_related: list[str]
:param filter_clauses: list of filter conditions
:type filter_clauses: list[FilterAction]
:return: list of filter conditions and select_related list
:rtype: tuple[list[FilterAction], list[str]]
"""
select_related = select_related if select_related is not None else []
filter_clauses = filter_clauses if filter_clauses is not None else []
qryclause = QueryClause(
model_cls=model_cls,
select_related=select_related,
filter_clauses=filter_clauses,
)
own_filter_clauses, select_related = qryclause.prepare_filter(
_own_only=True, **self._kwargs_dict
)
self.actions = own_filter_clauses
filter_clauses = filter_clauses + own_filter_clauses
self._resolved = True
if self._nested_groups:
for group in self._nested_groups:
filter_clauses, select_related = group.resolve(
model_cls=model_cls,
select_related=select_related,
filter_clauses=filter_clauses,
)
return filter_clauses, select_related
def _iter(self) -> Generator:
"""
Iterates all actions in a tree
:return: generator yielding from own actions and nested groups
:rtype: Generator
"""
for group in self._nested_groups:
yield from group._iter()
yield from self.actions
def _get_text_clauses(
self,
) -> list[Union[sqlalchemy.sql.expression.TextClause, ColumnElement[Any]]]:
"""
Helper to return list of text queries from actions and nested groups
:return: list of text queries from actions and nested groups
:rtype: list[sqlalchemy.sql.elements.TextClause]
"""
return [x.get_text_clause() for x in self._nested_groups] + [
x.get_text_clause() for x in self.actions
]
def get_text_clause(self) -> ColumnElement[bool]:
"""
Returns all own actions and nested groups conditions compiled and joined
inside parentheses.
Escapes characters if it's required.
Substitutes values of the models if value is a ormar Model with its pk value.
Compiles the clause.
:return: complied and escaped clause
:rtype: sqlalchemy.sql.elements.TextClause
"""
if self.filter_type == FilterType.AND:
clause = sqlalchemy.sql.and_(*self._get_text_clauses()).self_group()
else:
clause = sqlalchemy.sql.or_(*self._get_text_clauses()).self_group()
if self.exclude:
clause = sqlalchemy.sql.not_(clause)
return clause
def or_(*args: FilterGroup, **kwargs: Any) -> FilterGroup:
"""
Construct or filter from nested groups and keyword arguments
:param args: nested filter groups
:type args: tuple[FilterGroup]
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: FilterGroup ready to be resolved
:rtype: ormar.queryset.clause.FilterGroup
"""
return FilterGroup(_filter_type=FilterType.OR, *args, **kwargs)
def and_(*args: FilterGroup, **kwargs: Any) -> FilterGroup:
"""
Construct and filter from nested groups and keyword arguments
:param args: nested filter groups
:type args: tuple[FilterGroup]
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: FilterGroup ready to be resolved
:rtype: ormar.queryset.clause.FilterGroup
"""
return FilterGroup(_filter_type=FilterType.AND, *args, **kwargs)
@dataclass
class Prefix:
source_model: type["Model"]
table_prefix: str
model_cls: type["Model"]
relation_str: str
is_through: bool
@property
def alias_key(self) -> str:
source_model_name = self.source_model.get_name()
return f"{source_model_name}_{self.relation_str}"
class QueryClause:
"""
Constructs FilterActions from strings passed as arguments
"""
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.ormar_config.table
self._complex_dup_keys: set[str] = set()
def prepare_filter( # noqa: A003
self, _own_only: bool = False, **kwargs: Any
) -> tuple[list[FilterAction], list[str]]:
"""
Main external access point that processes the clauses into sqlalchemy text
clauses and updates select_related list with implicit related tables
mentioned in select_related strings but not included in select_related.
:param _own_only:
:type _own_only:
:param kwargs: key, value pair with column names and values
:type kwargs: Any
:return: tuple with list of where clauses and updated select_related list
:rtype: tuple[list[sqlalchemy.sql.elements.TextClause], list[str]]
"""
if kwargs.get("pk"):
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(
_own_only=_own_only, **kwargs
)
return filter_clauses, select_related
def _populate_filter_clauses(
self, _own_only: bool, **kwargs: Any
) -> tuple[list[FilterAction], list[str]]:
"""
Iterates all clauses and extracts used operator and field from related
models if needed. Based on the chain of related names the target table
is determined and the final clause is escaped if needed and compiled.
:param kwargs: key, value pair with column names and values
:type kwargs: Any
:return: tuple with list of where clauses and updated select_related list
:rtype: tuple[list[sqlalchemy.sql.elements.TextClause], list[str]]
"""
filter_clauses = self.filter_clauses
own_filter_clauses = []
select_related = list(self._select_related)
for key, value in kwargs.items():
filter_action = FilterAction(
filter_str=key, value=value, model_cls=self.model_cls
)
select_related = filter_action.update_select_related(
select_related=select_related
)
own_filter_clauses.append(filter_action)
self._register_complex_duplicates(select_related)
filter_clauses = self._switch_filter_action_prefixes(
filter_clauses=filter_clauses + own_filter_clauses
)
if _own_only:
return own_filter_clauses, select_related
return filter_clauses, select_related
def _register_complex_duplicates(self, select_related: list[str]) -> None:
"""
Checks if duplicate aliases are presented which can happen in self relation
or when two joins end with the same pair of models.
Mirrors the join builder (:meth:`ormar.queryset.join.SqlJoin._forward_join`):
relation paths are walked in the same depth-first order the join uses, the
first path to reach a given basic alias keeps it, and every later path that
would reuse the same alias is registered as a complex duplicate keyed on the
whole relation string (not just the last relation name).
The keys registered for *this* query are stored on
:attr:`_complex_dup_keys` so that :meth:`_verify_prefix_and_switch` only
reroutes filters that are genuinely duplicated here, ignoring complex
aliases that linger in the globally shared alias manager from earlier
queries.
:param select_related: list of relation strings
:type select_related: list[str]
:return: None
:rtype: None
"""
manager = self.model_cls.ormar_config.alias_manager
self._complex_dup_keys = set()
seen_prefixes: set[str] = set()
for prefix in self._parse_related_prefixes(select_related=select_related):
if prefix.table_prefix in seen_prefixes:
self._complex_dup_keys.add(prefix.alias_key)
if prefix.alias_key not in manager:
manager.add_alias(alias_key=prefix.alias_key)
else:
seen_prefixes.add(prefix.table_prefix)
def _parse_related_prefixes(self, select_related: list[str]) -> list[Prefix]:
"""
Walks all relation strings and parses the target models and prefixes.
Relation strings are expanded into the depth-first order of joined tables
(shared prefixes visited once) so the duplicate detection matches the join
builder exactly.
:param select_related: list of relation strings
:type select_related: list[str]
:return: list of parsed prefixes
:rtype: list[Prefix]
"""
prefixes: list[Prefix] = []
for related in ordered_join_paths(group_related_list(select_related)):
prefix = Prefix(
self.model_cls,
*get_relationship_alias_model_and_str(
self.model_cls, related.split("__")
),
)
prefixes.append(prefix)
return prefixes
def _switch_filter_action_prefixes(
self, filter_clauses: list[FilterAction]
) -> list[FilterAction]:
"""
Substitutes aliases for filter action if the complex key (whole relation str) is
present in alias_manager.
:param filter_clauses: raw list of actions
:type filter_clauses: list[FilterAction]
:return: list of actions with aliases changed if needed
:rtype: list[FilterAction]
"""
for action in filter_clauses:
if isinstance(action, FilterGroup):
for action2 in action._iter():
self._verify_prefix_and_switch(action2)
else:
self._verify_prefix_and_switch(action)
return filter_clauses
def _verify_prefix_and_switch(self, action: "FilterAction") -> None:
"""
Helper to switch prefix to complex relation one if required.
Only reroutes filters whose relation string is a complex duplicate
registered for this query (see :meth:`_register_complex_duplicates`), so a
complex alias left in the shared alias manager by an earlier query cannot
hijack a filter that is not duplicated here.
:param action: action to switch prefix in
:type action: ormar.queryset.actions.filter_action.FilterAction
"""
manager = self.model_cls.ormar_config.alias_manager
alias_key = f"{self.model_cls.get_name()}_{action.related_str}"
if "__" in action.related_str and alias_key in self._complex_dup_keys:
action.table_prefix = manager.resolve_relation_alias(
self.model_cls, action.related_str
)
ormar-orm-ormar-a7e7fde/ormar/queryset/field_accessor.py 0000664 0000000 0000000 00000024774 15211567312 0023670 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Any, Optional, cast
from ormar.queryset.actions import OrderAction
from ormar.queryset.actions.filter_action import METHODS_TO_OPERATORS
from ormar.queryset.clause import FilterGroup, NullsOrdering
if TYPE_CHECKING: # pragma: no cover
from ormar import BaseField, Model
class FieldAccessor:
"""
Helper to access ormar fields directly from Model class also for nested
models attributes.
"""
def __init__(
self,
source_model: type["Model"],
field: Optional["BaseField"] = None,
model: Optional[type["Model"]] = None,
access_chain: str = "",
) -> None:
self._source_model = source_model
self._field = field
self._model = model
self._access_chain = access_chain
def __getattr__(self, item: str) -> Any:
"""
Accessor return new accessor for each field and nested models.
Thanks to that operator overload is possible to use in filter.
:param item: attribute name
:type item: str
:return: FieldAccessor for field or nested model
:rtype: ormar.queryset.field_accessor.FieldAccessor
"""
if (
object.__getattribute__(self, "_field")
and item == object.__getattribute__(self, "_field").name
):
return self._field
if (
object.__getattribute__(self, "_model")
and item
in object.__getattribute__(self, "_model").ormar_config.model_fields
):
field = cast("Model", self._model).ormar_config.model_fields[item]
if field.is_relation:
return FieldAccessor(
source_model=self._source_model,
model=field.to,
access_chain=self._access_chain + f"__{item}",
)
else:
return FieldAccessor(
source_model=self._source_model,
field=field,
access_chain=self._access_chain + f"__{item}",
)
return object.__getattribute__(self, item) # pragma: no cover
def _check_field(self) -> None:
if self._field:
return
field = self._source_model.ormar_config.model_fields.get(self._access_chain)
if field is not None and not field.virtual and not field.is_multi:
return
raise AttributeError("Cannot filter by Model, you need to provide model name")
def _select_operator(self, op: str, other: Any) -> FilterGroup:
self._check_field()
filter_kwg = {self._access_chain + f"__{METHODS_TO_OPERATORS[op]}": other}
return FilterGroup(**filter_kwg)
def __eq__(self, other: Any) -> FilterGroup: # type: ignore
"""
overloaded to work as sql `column = `
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="__eq__", other=other)
def __ge__(self, other: Any) -> FilterGroup:
"""
overloaded to work as sql `column >= `
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="__ge__", other=other)
def __gt__(self, other: Any) -> FilterGroup:
"""
overloaded to work as sql `column > `
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="__gt__", other=other)
def __le__(self, other: Any) -> FilterGroup:
"""
overloaded to work as sql `column <= `
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="__le__", other=other)
def __lt__(self, other: Any) -> FilterGroup:
"""
overloaded to work as sql `column < `
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="__lt__", other=other)
def __mod__(self, other: Any) -> FilterGroup:
"""
overloaded to work as sql `column LIKE '%%'`
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="__mod__", other=other)
def __lshift__(self, other: Any) -> FilterGroup:
"""
overloaded to work as sql `column IN (, ,...)`
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="in", other=other)
def __rshift__(self, other: Any) -> FilterGroup:
"""
overloaded to work as sql `column IS NULL`
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="isnull", other=True)
def in_(self, other: Any) -> FilterGroup:
"""
works as sql `column IN (, ,...)`
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="in", other=other)
def iexact(self, other: Any) -> FilterGroup:
"""
works as sql `column = ` case-insensitive
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="iexact", other=other)
def contains(self, other: Any) -> FilterGroup:
"""
works as sql `column LIKE '%%'`
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="contains", other=other)
def icontains(self, other: Any) -> FilterGroup:
"""
works as sql `column LIKE '%%'` case-insensitive
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="icontains", other=other)
def startswith(self, other: Any) -> FilterGroup:
"""
works as sql `column LIKE '%'`
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="startswith", other=other)
def istartswith(self, other: Any) -> FilterGroup:
"""
works as sql `column LIKE '%'` case-insensitive
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="istartswith", other=other)
def endswith(self, other: Any) -> FilterGroup:
"""
works as sql `column LIKE '%'`
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="endswith", other=other)
def iendswith(self, other: Any) -> FilterGroup:
"""
works as sql `column LIKE '%'` case-insensitive
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="iendswith", other=other)
def isnull(self, other: Any) -> FilterGroup:
"""
works as sql `column IS NULL` or `IS NOT NULL`
:param other: value to check agains operator
:type other: str
:return: FilterGroup for operator
:rtype: ormar.queryset.clause.FilterGroup
"""
return self._select_operator(op="isnull", other=other)
def asc(self, nulls_ordering: Optional[NullsOrdering] = None) -> OrderAction:
"""
works as sql `column asc`
:param nulls_ordering: optional nulls placement, `NullsOrdering.FIRST`
or `NullsOrdering.LAST`
:type nulls_ordering: Optional[NullsOrdering]
:raises ValueError: if `nulls_ordering` is not a `NullsOrdering` member
:return: OrderGroup for operator
:rtype: ormar.queryset.actions.OrderGroup
"""
nulls_value = self._coerce_nulls_ordering(nulls_ordering)
return OrderAction(
order_str=self._access_chain,
model_cls=self._source_model,
nulls_ordering=nulls_value,
)
def desc(self, nulls_ordering: Optional[NullsOrdering] = None) -> OrderAction:
"""
works as sql `column desc`
:param nulls_ordering: optional nulls placement, `NullsOrdering.FIRST`
or `NullsOrdering.LAST`
:type nulls_ordering: Optional[NullsOrdering]
:raises ValueError: if `nulls_ordering` is not a `NullsOrdering` member
:return: OrderGroup for operator
:rtype: ormar.queryset.actions.OrderGroup
"""
nulls_value = self._coerce_nulls_ordering(nulls_ordering)
return OrderAction(
order_str="-" + self._access_chain,
model_cls=self._source_model,
nulls_ordering=nulls_value,
)
@staticmethod
def _coerce_nulls_ordering(
nulls_ordering: Optional[NullsOrdering],
) -> Optional[str]:
if nulls_ordering is None:
return None
if not isinstance(nulls_ordering, NullsOrdering):
raise ValueError("Invalid option for ordering nulls values.")
return nulls_ordering.value
ormar-orm-ormar-a7e7fde/ormar/queryset/join.py 0000664 0000000 0000000 00000037175 15211567312 0021661 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Any, Optional, cast
import sqlalchemy
from sqlalchemy import TextClause, text
import ormar # noqa I100
from ormar.exceptions import ModelDefinitionError, RelationshipInstanceError
from ormar.relations import AliasManager
if TYPE_CHECKING: # pragma no cover
from ormar import ManyToManyField, Model
from ormar.models.excludable import ExcludableItems
from ormar.queryset import OrderAction
class SqlJoin:
def __init__( # noqa: CFQ002
self,
used_aliases: list,
select_from: sqlalchemy.sql.Select,
columns: list[sqlalchemy.Column],
excludable: "ExcludableItems",
order_columns: Optional[list["OrderAction"]],
sorted_orders: dict,
main_model: type["Model"],
relation_name: str,
relation_str: str,
related_models: Any = None,
own_alias: str = "",
source_model: Optional[type["Model"]] = None,
already_sorted: Optional[dict] = None,
) -> None:
self.relation_name = relation_name
self.related_models = related_models or []
self.select_from = select_from
self.columns = columns
self.excludable = excludable
self.order_columns = order_columns
self.sorted_orders = sorted_orders
self.already_sorted = already_sorted or dict()
self.main_model = main_model
self.own_alias = own_alias
self.used_aliases = used_aliases
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
self.relation_str = relation_str
self.source_model = source_model
@property
def next_model(self) -> type["Model"]:
if not self._next_model: # pragma: nocover
raise RelationshipInstanceError(
"Cannot link to related table if relation.to model is not set."
)
return self._next_model
@next_model.setter
def next_model(self, value: type["Model"]) -> None:
self._next_model = value
@property
def next_alias(self) -> str:
if not self._next_alias: # pragma: nocover
raise RelationshipInstanceError("Alias for given relation not found.")
return self._next_alias
@next_alias.setter
def next_alias(self, value: str) -> None:
self._next_alias = value
@property
def alias_manager(self) -> AliasManager:
"""
Shortcut for ormar's model AliasManager stored on OrmarConfig.
:return: alias manager from model's OrmarConfig
:rtype: AliasManager
"""
return self.main_model.ormar_config.alias_manager
@property
def to_table(self) -> sqlalchemy.Table:
"""
Shortcut to table name of the next model
:return: name of the target table
:rtype: str
"""
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,
) -> TextClause:
"""
Receives aliases and names of both ends of the join and combines them
into one text clause used in joins.
:param previous_alias: alias of previous table
:type previous_alias: str
:param from_table_name: from table name
:type from_table_name: str
:param from_column_name: from column name
:type from_column_name: str
:param to_table_name: to table name
:type to_table_name: str
:param to_column_name: to column name
:type to_column_name: str
:return: clause combining all strings
:rtype: sqlalchemy.text
"""
dialect = self.main_model.ormar_config.database.dialect
quoter = dialect.identifier_preparer.quote
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}"
)
return text(f"{left_part}={right_part}")
def build_join(self) -> tuple[list, sqlalchemy.sql.Select, list, dict]:
"""
Main external access point for building a join.
Splits the join definition, updates fields and exclude_fields if needed,
handles switching to through models for m2m relations, returns updated lists of
used_aliases and sort_orders.
:return: list of used aliases, select from, list of aliased columns, sort orders
:rtype: tuple[list[str], Join, list[TextClause], dict]
"""
if self.target_field.is_multi:
self._process_m2m_through_table()
self.next_model = self.target_field.to
self._forward_join()
self._process_following_joins()
return (self.used_aliases, self.select_from, self.columns, self.sorted_orders)
def _forward_join(self) -> None:
"""
Process actual join.
Registers complex relation join on encountering of the duplicated alias.
"""
self.next_alias = self.alias_manager.resolve_relation_alias(
from_model=self.target_field.owner, relation_name=self.relation_name
)
if self.next_alias not in self.used_aliases:
self._process_join()
else:
if "__" in self.relation_str and self.source_model:
relation_key = f"{self.source_model.get_name()}_{self.relation_str}"
if relation_key not in self.alias_manager:
self.next_alias = self.alias_manager.add_alias(
alias_key=relation_key
)
else:
self.next_alias = self.alias_manager[relation_key]
self._process_join()
def _process_following_joins(self) -> None:
"""
Iterates through nested models to create subsequent joins.
"""
for related_name in self.related_models:
remainder = None
if (
isinstance(self.related_models, dict)
and self.related_models[related_name]
):
remainder = self.related_models[related_name]
self._process_deeper_join(related_name=related_name, remainder=remainder)
def _process_deeper_join(self, related_name: str, remainder: Any) -> None:
"""
Creates nested recurrent instance of SqlJoin for each nested join table,
updating needed return params here as a side effect.
Updated are:
* self.used_aliases,
* self.select_from,
* self.columns,
* self.sorted_orders,
:param related_name: name of the relation to follow
:type related_name: str
:param remainder: deeper tables if there are more nested joins
:type remainder: Any
"""
sql_join = SqlJoin(
used_aliases=self.used_aliases,
select_from=self.select_from,
columns=self.columns,
excludable=self.excludable,
order_columns=self.order_columns,
sorted_orders=self.sorted_orders,
main_model=self.next_model,
relation_name=related_name,
related_models=remainder,
relation_str="__".join([self.relation_str, related_name]),
own_alias=self.next_alias,
source_model=self.source_model or self.main_model,
already_sorted=self.already_sorted,
)
(
self.used_aliases,
self.select_from,
self.columns,
self.sorted_orders,
) = sql_join.build_join()
def _process_m2m_through_table(self) -> None:
"""
Process Through table of the ManyToMany relation so that source table is
linked to the through table (one additional join)
Replaces needed parameters like:
* self.next_model,
* self.next_alias,
* self.relation_name,
* self.own_alias,
* self.target_field
To point to through model
"""
new_part = self._process_m2m_related_name_change()
self.next_model = self.target_field.through
self._forward_join()
self.relation_name = new_part
self.own_alias = self.next_alias
self.target_field = self.next_model.ormar_config.model_fields[
self.relation_name
]
def _process_m2m_related_name_change(self, reverse: bool = False) -> str:
"""
Extracts relation name to link join through the Through model declared on
relation field.
Changes the same names in order_by queries if they are present.
:param reverse: flag if it's on_clause lookup - use reverse fields
:type reverse: bool
:return: new relation name switched to through model field
:rtype: str
"""
target_field = self.target_field
is_primary_self_ref = (
target_field.self_reference
and self.relation_name == target_field.self_reference_primary
)
if (is_primary_self_ref and not reverse) or (
not is_primary_self_ref and reverse
):
new_part = target_field.default_source_field_name() # type: ignore
else:
new_part = target_field.default_target_field_name() # type: ignore
return new_part
def _process_join(self) -> None: # noqa: CFQ002
"""
Resolves to and from column names and table names.
Produces on_clause.
Performs actual join updating select_from parameter.
Adds aliases of required column to list of columns to include in query.
Updates the used aliases list directly.
Process order_by causes for non m2m relations.
"""
to_key, from_key = self._get_to_and_from_keys()
on_clause = self._on_clause(
previous_alias=self.own_alias,
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,
)
target_table = self.alias_manager.prefixed_table_name(
self.next_alias, self.to_table
)
self.select_from = sqlalchemy.sql.outerjoin(
self.select_from, # type: ignore
target_table,
on_clause,
)
self._get_order_bys()
self_related_fields = self.next_model.own_table_columns(
model=self.next_model,
excludable=self.excludable,
alias=self.next_alias,
use_alias=True,
)
self.columns.extend(
self.alias_manager.prefixed_columns( # type: ignore
self.next_alias,
target_table, # type: ignore
self_related_fields,
)
)
self.used_aliases.append(self.next_alias)
def _set_default_primary_key_order_by(self) -> None:
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
)
self.sorted_orders[clause] = clause.get_text_clause()
def _verify_allowed_order_field(self, order_by: str) -> None:
"""
Verifies if proper field string is used.
:param order_by: string with order by definition
:type order_by: str
"""
parts = order_by.split("__")
if len(parts) > 2 or parts[0] != self.target_field.through.get_name():
raise ModelDefinitionError(
"You can order the relation only by related or link table columns!"
)
def _get_alias_and_model(self, order_by: str) -> tuple[str, type["Model"]]:
"""
Returns proper model and alias to be applied in the clause.
:param order_by: string with order by definition
:type order_by: str
:return: alias and model to be used in clause
:rtype: tuple[str, type["Model"]]
"""
if self.target_field.is_multi and "__" in order_by:
self._verify_allowed_order_field(order_by=order_by)
alias = self.next_alias
model = self.target_field.owner
elif self.target_field.is_multi:
alias = self.alias_manager.resolve_relation_alias(
from_model=self.target_field.through,
relation_name=cast(
"ManyToManyField", self.target_field
).default_target_field_name(),
)
model = self.target_field.to
else:
alias = self.alias_manager.resolve_relation_alias(
from_model=self.target_field.owner, relation_name=self.target_field.name
)
model = self.target_field.to
return alias, model
def _get_order_bys(self) -> None: # noqa: CCR001
"""
Triggers construction of order bys if they are given.
Otherwise by default each table is sorted by a primary key column asc.
"""
alias = self.next_alias
current_table_sorted = False
if f"{alias}_{self.next_model.get_name()}" in self.already_sorted:
current_table_sorted = True
if self.order_columns:
for condition in self.order_columns:
if condition.check_if_filter_apply(
target_model=self.next_model, alias=alias
):
current_table_sorted = True
self.sorted_orders[condition] = condition.get_text_clause()
self.already_sorted[
f"{self.next_alias}_{self.next_model.get_name()}"
] = condition
if self.target_field.orders_by and not current_table_sorted:
current_table_sorted = True
for order_by in self.target_field.orders_by:
alias, model = self._get_alias_and_model(order_by=order_by)
clause = ormar.OrderAction(
order_str=order_by, model_cls=model, alias=alias
)
self.sorted_orders[clause] = clause.get_text_clause()
self.already_sorted[f"{alias}_{model.get_name()}"] = clause
if not current_table_sorted and not self.target_field.is_multi:
self._set_default_primary_key_order_by()
def _get_to_and_from_keys(self) -> tuple[str, str]:
"""
Based on the relation type, name of the relation and previous models and parts
stored in JoinParameters it resolves the current to and from keys, which are
different for ManyToMany relation, ForeignKey and reverse related of relations.
:return: to key and from key
:rtype: tuple[str, str]
"""
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.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.ormar_config.pkname
)
else:
to_key = self.target_field.to.get_column_alias(
self.target_field.to.ormar_config.pkname
)
from_key = self.main_model.get_column_alias(self.relation_name)
return to_key, from_key
ormar-orm-ormar-a7e7fde/ormar/queryset/queries/ 0000775 0000000 0000000 00000000000 15211567312 0022010 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/queryset/queries/__init__.py 0000664 0000000 0000000 00000000726 15211567312 0024126 0 ustar 00root root 0000000 0000000 from ormar.queryset.queries.filter_query import FilterQuery
from ormar.queryset.queries.limit_query import LimitQuery
from ormar.queryset.queries.offset_query import OffsetQuery
from ormar.queryset.queries.order_query import OrderQuery
from ormar.queryset.queries.prefetch_query import PrefetchQuery
from ormar.queryset.queries.query import Query
__all__ = [
"FilterQuery",
"LimitQuery",
"OffsetQuery",
"OrderQuery",
"PrefetchQuery",
"Query",
]
ormar-orm-ormar-a7e7fde/ormar/queryset/queries/filter_query.py 0000664 0000000 0000000 00000002407 15211567312 0025077 0 ustar 00root root 0000000 0000000 from typing import Any, Union
import sqlalchemy
from sqlalchemy import ColumnElement, Select, TextClause
from ormar.queryset.actions.filter_action import FilterAction
class FilterQuery:
"""
Modifies the select query with given list of where/filter clauses.
"""
def __init__(
self, filter_clauses: list[FilterAction], exclude: bool = False
) -> None:
self.exclude = exclude
self.filter_clauses = filter_clauses
def apply(
self,
expr: Select[Any],
) -> Select[Any]:
"""
Applies all filter clauses if set.
:param expr: query to modify
:type expr: sqlalchemy.sql.selectable.Select
:return: modified query
:rtype: sqlalchemy.sql.selectable.Select
"""
if self.filter_clauses:
if len(self.filter_clauses) == 1:
clause: Union[TextClause, ColumnElement[Any]] = self.filter_clauses[
0
].get_text_clause()
else:
clause = sqlalchemy.sql.and_(
*[x.get_text_clause() for x in self.filter_clauses]
)
clause = sqlalchemy.sql.not_(clause) if self.exclude else clause
expr = expr.where(clause)
return expr
ormar-orm-ormar-a7e7fde/ormar/queryset/queries/limit_query.py 0000664 0000000 0000000 00000001211 15211567312 0024720 0 ustar 00root root 0000000 0000000 from typing import Optional
import sqlalchemy
class LimitQuery:
"""
Modifies the select query with limit clause.
"""
def __init__(self, limit_count: Optional[int]) -> None:
self.limit_count = limit_count
def apply(self, expr: sqlalchemy.sql.Select) -> sqlalchemy.sql.Select:
"""
Applies the limit clause.
:param expr: query to modify
:type expr: sqlalchemy.sql.selectable.Select
:return: modified query
:rtype: sqlalchemy.sql.selectable.Select
"""
if self.limit_count is not None:
expr = expr.limit(self.limit_count)
return expr
ormar-orm-ormar-a7e7fde/ormar/queryset/queries/offset_query.py 0000664 0000000 0000000 00000001203 15211567312 0025071 0 ustar 00root root 0000000 0000000 from typing import Optional
import sqlalchemy
class OffsetQuery:
"""
Modifies the select query with offset if set
"""
def __init__(self, query_offset: Optional[int]) -> None:
self.query_offset = query_offset
def apply(self, expr: sqlalchemy.sql.Select) -> sqlalchemy.sql.Select:
"""
Applies the offset clause.
:param expr: query to modify
:type expr: sqlalchemy.sql.selectable.Select
:return: modified query
:rtype: sqlalchemy.sql.selectable.Select
"""
if self.query_offset:
expr = expr.offset(self.query_offset)
return expr
ormar-orm-ormar-a7e7fde/ormar/queryset/queries/order_query.py 0000664 0000000 0000000 00000001334 15211567312 0024723 0 ustar 00root root 0000000 0000000 import sqlalchemy
class OrderQuery:
"""
Modifies the select query with given list of order_by clauses.
"""
def __init__(self, sorted_orders: dict) -> None:
self.sorted_orders = sorted_orders
def apply(self, expr: sqlalchemy.sql.Select) -> sqlalchemy.sql.Select:
"""
Applies all order_by clauses if set.
:param expr: query to modify
:type expr: sqlalchemy.sql.selectable.Select
:return: modified query
:rtype: sqlalchemy.sql.selectable.Select
"""
if self.sorted_orders:
for order in list(self.sorted_orders.values()):
if order is not None:
expr = expr.order_by(order)
return expr
ormar-orm-ormar-a7e7fde/ormar/queryset/queries/prefetch_query.py 0000664 0000000 0000000 00000050407 15211567312 0025415 0 ustar 00root root 0000000 0000000 import abc
import logging
from abc import abstractmethod
from typing import TYPE_CHECKING, Any, Sequence, Union, cast
import ormar_rust_utils
import ormar # noqa: I100, I202
from ormar.queryset.clause import QueryClause
from ormar.queryset.queries.query import Query
from ormar.queryset.utils import translate_list_to_dict
if TYPE_CHECKING: # pragma: no cover
from ormar import ForeignKeyField, Model
from ormar.models.excludable import ExcludableItems
from ormar.queryset import FilterAction, OrderAction
logger = logging.getLogger(__name__)
UniqueList = ormar_rust_utils.UniqueList
class Node(abc.ABC):
"""
Base Node use to build a query tree and divide job into already loaded models
and the ones that still need to be fetched from database
"""
def __init__(self, relation_field: "ForeignKeyField", parent: "Node") -> None:
self.parent = parent
self.children: list["Node"] = []
if self.parent:
self.parent.children.append(self)
self.relation_field = relation_field
self.table_prefix = ""
self.rows: list = []
self.models: list["Model"] = []
self.use_alias: bool = False
@property
def target_name(self) -> str:
"""
Return the name of the relation that is used to
fetch excludes/includes from the excludable mixin
as well as specifying the target to join in m2m relations
:return: name of the relation
:rtype: str
"""
if (
self.relation_field.self_reference
and self.relation_field.self_reference_primary == self.relation_field.name
):
return self.relation_field.default_source_field_name()
else:
return self.relation_field.default_target_field_name()
@abstractmethod
def extract_related_ids(self, column_name: str) -> list: # pragma: no cover
pass
@abstractmethod
def reload_tree(self) -> None: # pragma: no cover
pass
@abstractmethod
async def load_data(self) -> None: # pragma: no cover
pass
def get_filter_for_prefetch(self) -> list["FilterAction"]:
"""
Populates where clause with condition to return only models within the
set of extracted ids.
If there are no ids for relation the empty list is returned.
:return: list of filter clauses based on original models
:rtype: list[sqlalchemy.sql.elements.TextClause]
"""
column_name = self.relation_field.get_model_relation_fields(
self.parent.use_alias
)
ids = self.parent.extract_related_ids(column_name=column_name)
if ids:
return self._prepare_filter_clauses(ids=ids)
return []
def _prepare_filter_clauses(self, ids: list) -> list["FilterAction"]:
"""
Gets the list of ids and construct a list of filter queries on
extracted appropriate column names
:param ids: list of ids that should be used to fetch data
:type ids: list
:return: list of filter actions to use in query
:rtype: list["FilterAction"]
"""
clause_target = self.relation_field.get_filter_clause_target()
filter_column = self.relation_field.get_related_field_alias()
qryclause = QueryClause(
model_cls=clause_target,
select_related=[],
filter_clauses=[],
)
kwargs = {f"{cast(str, filter_column)}__in": ids}
filter_clauses, _ = qryclause.prepare_filter(_own_only=False, **kwargs)
return filter_clauses
class AlreadyLoadedNode(Node):
"""
Node that was already loaded in select statement
"""
def __init__(self, relation_field: "ForeignKeyField", parent: "Node") -> None:
super().__init__(relation_field=relation_field, parent=parent)
self.use_alias = False
self._extract_own_models()
def _extract_own_models(self) -> None:
"""
Extract own models that were already fetched and attached to root node
"""
for model in self.parent.models:
child_models = getattr(model, self.relation_field.name)
if isinstance(child_models, list):
self.models.extend(child_models)
elif child_models:
self.models.append(child_models)
async def load_data(self) -> None:
"""
Triggers a data load in the child nodes
"""
for child in self.children:
await child.load_data()
def reload_tree(self) -> None:
"""
After data was loaded we reload whole tree from the bottom
to include freshly loaded nodes
"""
for child in self.children:
child.reload_tree()
def extract_related_ids(self, column_name: str) -> list:
"""
Extracts the selected column(s) values from own models.
Those values are used to construct filter clauses and populate child models.
:param column_name: names of the column(s) that holds the relation info
:type column_name: Union[str, list[str]]
:return: list of extracted values of relation columns
:rtype: list
"""
list_of_ids = UniqueList()
for model in self.models:
child = getattr(model, column_name)
if isinstance(child, ormar.Model):
list_of_ids.append(child.pk)
elif child is not None:
list_of_ids.append(child)
return list_of_ids
class RootNode(AlreadyLoadedNode):
"""
Root model Node from which both main and prefetch query originated
"""
def __init__(self, models: list["Model"]) -> None:
self.models = models
self.use_alias = False
self.children = []
def reload_tree(self) -> None:
for child in self.children:
child.reload_tree()
class LoadNode(Node):
"""
Nodes that actually need to be fetched from database in the prefetch query
"""
def __init__(
self,
relation_field: "ForeignKeyField",
excludable: "ExcludableItems",
orders_by: list["OrderAction"],
parent: "Node",
source_model: type["Model"],
) -> None:
super().__init__(relation_field=relation_field, parent=parent)
self.excludable = excludable
self.exclude_prefix: str = ""
self.orders_by = orders_by
self.use_alias = True
self.grouped_models: dict[Any, list["Model"]] = dict()
self.source_model = source_model
async def load_data(self) -> None:
"""
Ensures that at least primary key columns from current model are included in
the query.
Gets the filter values from the parent model and runs the query.
Triggers a data load in child tasks.
"""
self._update_excludable_with_related_pks()
if self.relation_field.is_multi:
query_target = self.relation_field.through
select_related = [self.target_name]
else:
query_target = self.relation_field.to
select_related = []
filter_clauses = self.get_filter_for_prefetch()
if filter_clauses:
qry = Query(
model_cls=query_target,
select_related=select_related,
filter_clauses=filter_clauses,
exclude_clauses=[],
offset=None,
limit_count=None,
excludable=self.excludable,
order_bys=self._extract_own_order_bys(),
limit_raw_sql=False,
)
expr = qry.build_select_expression()
logger.debug(
expr.compile(
dialect=self.source_model.ormar_config.database.dialect,
compile_kwargs={"literal_binds": True},
)
)
async with query_target.ormar_config.database.get_query_executor() as exctr:
self.rows = await exctr.fetch_all(expr)
for child in self.children:
await child.load_data()
def _update_excludable_with_related_pks(self) -> None:
"""
Makes sure that excludable is populated with own model primary keys values
if the excludable has the exclude/include clauses
"""
related_field_names = self.relation_field.get_related_field_name()
alias_manager = self.relation_field.to.ormar_config.alias_manager
relation_key = self._build_relation_key()
self.exclude_prefix = alias_manager.resolve_relation_alias_after_complex(
source_model=self.source_model,
relation_str=relation_key,
relation_field=self.relation_field,
)
if self.relation_field.is_multi:
self.table_prefix = self.exclude_prefix
target_model = self.relation_field.to
model_excludable = self.excludable.get(
model_cls=target_model, alias=self.exclude_prefix
)
# includes nested pks if not included already
for related_name in related_field_names:
if model_excludable.include and not model_excludable.is_included(
related_name
):
model_excludable.set_values({related_name}, slot="include")
def _build_relation_string(self) -> str:
node: Union[LoadNode, Node] = self
relation = node.relation_field.name
while not isinstance(node.parent, RootNode):
relation = node.parent.relation_field.name + "__" + relation
node = node.parent
return relation
def _build_relation_key(self) -> str:
relation_key = self._build_relation_string()
return relation_key
def _extract_own_order_bys(self) -> list["OrderAction"]:
"""
Extracts list of order actions related to current model.
Since same model can happen multiple times in a tree we check not only the
match on given model but also that path from relation tree matches the
path in order action.
:return: list of order actions related to current model
:rtype: list[OrderAction]
"""
own_order_bys = []
own_path = self._get_full_tree_path()
for order_by in self.orders_by:
if (
order_by.target_model == self.relation_field.to
and order_by.related_str.endswith(f"{own_path}")
):
order_by.is_source_model_order = True
order_by.table_prefix = self.table_prefix
own_order_bys.append(order_by)
return own_order_bys
def _get_full_tree_path(self) -> str:
"""
Iterates the nodes to extract path from root node.
:return: path from root node
:rtype: str
"""
node: Node = self
relation_str = node.relation_field.name
while not isinstance(node.parent, RootNode):
node = node.parent
relation_str = f"{node.relation_field.name}__{relation_str}"
return relation_str
def extract_related_ids(self, column_name: str) -> list:
"""
Extracts the selected column(s) values from own models.
Those values are used to construct filter clauses and populate child models.
:param column_names: names of the column(s) that holds the relation info
:type column_names: Union[str, list[str]]
:return: list of extracted values of relation columns
:rtype: list
"""
column_name = self._prefix_column_names_with_table_prefix(
column_name=column_name
)
return self._extract_simple_relation_keys(column_name=column_name)
def _prefix_column_names_with_table_prefix(self, column_name: str) -> str:
return (f"{self.table_prefix}_" if self.table_prefix else "") + column_name
def _extract_simple_relation_keys(self, column_name: str) -> list:
"""
Extracts simple relation keys values.
:param column_name: names of the column(s) that holds the relation info
:type column_name: str
:return: list of extracted values of relation columns
:rtype: list
"""
list_of_ids = UniqueList()
for row in self.rows:
if row[column_name]:
list_of_ids.append(row[column_name])
return list_of_ids
def reload_tree(self) -> None:
"""
Instantiates models from loaded database rows.
Groups those instances by relation key for easy extract per parent.
Triggers same for child nodes and then populates
the parent node with own related models
"""
if self.rows:
self._instantiate_models()
self._group_models_by_relation_key()
for child in self.children:
child.reload_tree()
self._populate_parent_models()
def _instantiate_models(self) -> None:
"""
Iterates the rows and initializes instances of ormar.Models.
Each model is instantiated only once (they can be duplicates for m2m relation
when multiple parent models refer to same child model since the query have to
also include the through model - hence full rows are unique, but related
models without through models can be not unique).
"""
fields_to_exclude = self.relation_field.to.get_names_to_exclude(
excludable=self.excludable, alias=self.exclude_prefix
)
# ``self.table_prefix`` and ``self.exclude_prefix`` can differ, so the
# plan only amortizes the column mapping work — exclude set is reused
# from above. Build once before the loop.
row_plan = self.relation_field.to.build_row_extraction_plan(
self.table_prefix, self.excludable
)
parsed_rows: dict[tuple, "Model"] = {}
for row in self.rows:
item = self.relation_field.to.apply_row_plan(row_plan, row, {})
hashable_item = self._hash_item(item)
instance = parsed_rows.setdefault(
hashable_item,
self.relation_field.to._construct_with_excluded(
fields_to_exclude, **item
),
)
self.models.append(instance)
def _hash_item(self, item: Union[dict, list]) -> tuple:
"""
Converts model dictionary or list into a hashable tuple to allow its use
as a dictionary key - used to ensure unique instances of related models.
:param item: instance dictionary or list
:type item: dict or list
:return: tuple out of model dictionary or list
:rtype: tuple
"""
return ormar_rust_utils.hash_item(item)
def _group_models_by_relation_key(self) -> None:
"""
Groups own models by relation keys so it's easy later to extract those models
when iterating parent models. Note that order is important as it reflects
order by issued by the user.
"""
relation_key = self.relation_field.get_related_field_alias()
for index, row in enumerate(self.rows):
key = row[relation_key]
current_group = self.grouped_models.setdefault(key, [])
current_group.append(self.models[index])
def _populate_parent_models(self) -> None:
"""
Populate parent node models with own child models from grouped dictionary
"""
relation_key = self._get_relation_key_linking_models()
for model in self.parent.models:
children = self._get_own_models_related_to_parent(
model=model, relation_key=relation_key
)
for child in children:
setattr(model, self.relation_field.name, child)
def _get_relation_key_linking_models(self) -> tuple[str, str]:
"""
Extract names and aliases of relation columns to use
in linking between own models and parent models
:return: tuple of name and alias of relation columns
:rtype: list[tuple[str, str]]
"""
column_name = self.relation_field.get_model_relation_fields(False)
column_alias = self.relation_field.get_model_relation_fields(True)
return column_name, column_alias
def _get_own_models_related_to_parent(
self, model: "Model", relation_key: tuple[str, str]
) -> list["Model"]:
"""
Extracts related column values from parent and based on this key gets the
own grouped models.
:param model: parent model from parent node
:type model: Model
:param relation_key: name and aliases linking relations
:type relation_key: list[tuple[str, str]]
:return: list of own models to set on parent
:rtype: list[Model]
"""
column_name, column_alias = relation_key
model_value = getattr(model, column_name)
if isinstance(model_value, ormar.Model):
model_value = model_value.pk
return self.grouped_models.get(model_value, [])
class PrefetchQuery:
"""
Query used to fetch related models in subsequent queries.
Each model is fetched only ones by the name of the relation.
That means that for each prefetch_related entry next query is issued to database.
"""
def __init__( # noqa: CFQ002
self,
model_cls: type["Model"],
excludable: "ExcludableItems",
prefetch_related: list,
select_related: list,
orders_by: list["OrderAction"],
) -> None:
self.model = model_cls
self.excludable = excludable
self.select_dict = translate_list_to_dict(select_related, default={})
self.prefetch_dict = translate_list_to_dict(prefetch_related, default={})
self.orders_by = orders_by
self.load_tasks: list[Node] = []
async def prefetch_related(self, models: Sequence["Model"]) -> Sequence["Model"]:
"""
Main entry point for prefetch_query.
Receives list of already initialized parent models with all children from
select_related already populated. Receives also list of row sql result rows
as it's quicker to extract ids that way instead of calling each model.
Returns list with related models already prefetched and set.
:param models: list of already instantiated models from main query
:type models: Sequence[Model]
:param rows: row sql result of the main query before the prefetch
:type rows: list[sqlalchemy.engine.result.RowProxy]
:return: list of models with children prefetched
:rtype: list[Model]
"""
parent_task = RootNode(models=cast(list["Model"], models))
self._build_load_tree(
prefetch_dict=self.prefetch_dict,
select_dict=self.select_dict,
parent=parent_task,
model=self.model,
)
await parent_task.load_data()
parent_task.reload_tree()
return parent_task.models
def _build_load_tree(
self,
select_dict: dict,
prefetch_dict: dict,
parent: Node,
model: type["Model"],
) -> None:
"""
Build a tree of already loaded nodes and nodes that need
to be loaded through the prefetch query.
:param select_dict: dictionary wth select query structure
:type select_dict: dict
:param prefetch_dict: dictionary with prefetch query structure
:type prefetch_dict: dict
:param parent: parent Node
:type parent: Node
:param model: currently processed model
:type model: Model
"""
for related in prefetch_dict.keys():
relation_field = cast(
"ForeignKeyField", model.ormar_config.model_fields[related]
)
if related in select_dict:
task: Node = AlreadyLoadedNode(
relation_field=relation_field, parent=parent
)
else:
task = LoadNode(
relation_field=relation_field,
excludable=self.excludable,
orders_by=self.orders_by,
parent=parent,
source_model=self.model,
)
if prefetch_dict:
self._build_load_tree(
select_dict=select_dict.get(related, {}),
prefetch_dict=prefetch_dict.get(related, {}),
parent=task,
model=model.ormar_config.model_fields[related].to,
)
ormar-orm-ormar-a7e7fde/ormar/queryset/queries/query.py 0000664 0000000 0000000 00000023047 15211567312 0023535 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Any, Optional, Union, cast
import sqlalchemy
from sqlalchemy import Column, Select, Table, TextClause
from sqlalchemy.sql import Join
from sqlalchemy.sql.roles import FromClauseRole
import ormar # noqa I100
from ormar.models.helpers.models import group_related_list
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.models.excludable import ExcludableItems
from ormar.queryset import OrderAction
class Query:
def __init__( # noqa CFQ002
self,
model_cls: type["Model"],
filter_clauses: list[FilterAction],
exclude_clauses: list[FilterAction],
select_related: list,
limit_count: Optional[int],
offset: Optional[int],
excludable: "ExcludableItems",
order_bys: Optional[list["OrderAction"]],
limit_raw_sql: bool,
) -> None:
self.query_offset = offset
self.limit_count = limit_count
self._select_related = select_related[:]
self.filter_clauses = filter_clauses[:]
self.exclude_clauses = exclude_clauses[:]
self.excludable = excludable
self.model_cls = model_cls
self.table = self.model_cls.ormar_config.table
self.used_aliases: list[str] = []
self.select_from: Union[Join, Table, list[str]] = []
self.columns: list[Column] = []
self.order_columns = order_bys
self.sorted_orders: dict[OrderAction, TextClause] = {}
self._init_sorted_orders()
self.limit_raw_sql = limit_raw_sql
def _init_sorted_orders(self) -> None:
"""
Initialize empty order_by dict to be populated later during the query call
"""
if self.order_columns:
for clause in self.order_columns:
self.sorted_orders[clause] = None # type: ignore
def apply_order_bys_for_primary_model(self) -> None: # noqa: CCR001
"""
Applies order_by queries on main model when it's used as a subquery.
That way the subquery with limit and offset only on main model has proper
sorting applied and correct models are fetched.
"""
current_table_sorted = False
if self.order_columns:
for clause in self.order_columns:
if clause.is_source_model_order:
current_table_sorted = True
self.sorted_orders[clause] = clause.get_text_clause()
if not current_table_sorted:
self._apply_default_model_sorting()
def _apply_default_model_sorting(self) -> None:
"""
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 clause in ormar.OrderAction.from_model_defaults(self.model_cls):
self.sorted_orders[clause] = clause.get_text_clause()
def _pagination_query_required(self) -> bool:
"""
Checks if limit or offset are set, the flag limit_sql_raw is not set
and query has select_related applied. Otherwise we can limit/offset normally
at the end of whole query.
:return: result of the check
:rtype: bool
"""
return bool(
(self.limit_count or self.query_offset)
and not self.limit_raw_sql
and self._select_related
)
def build_select_expression(self) -> sqlalchemy.sql.Select:
"""
Main entry point from outside (after proper initialization).
Extracts columns list to fetch,
construct all required joins for select related,
then applies all conditional and sort clauses.
Returns ready to run query with all joins and clauses.
:return: ready to run query with all joins and clauses.
:rtype: sqlalchemy.sql.selectable.Select
"""
self_related_fields = self.model_cls.own_table_columns(
model=self.model_cls, excludable=self.excludable, use_alias=True
)
self.columns = self.model_cls.ormar_config.alias_manager.prefixed_columns( # type: ignore
"", self.table, self_related_fields
)
self.apply_order_bys_for_primary_model()
self.select_from = self.table
related_models = group_related_list(self._select_related)
for related in related_models:
remainder = None
if isinstance(related_models, dict) and related_models[related]:
remainder = related_models[related]
sql_join = SqlJoin(
used_aliases=self.used_aliases,
select_from=self.select_from, # type: ignore
columns=self.columns,
excludable=self.excludable,
order_columns=self.order_columns,
sorted_orders=self.sorted_orders,
main_model=self.model_cls,
relation_name=related,
relation_str=related,
related_models=remainder,
)
(
self.used_aliases,
self.select_from,
self.columns,
self.sorted_orders,
) = sql_join.build_join() # type: ignore
if self._pagination_query_required():
limit_qry, on_clause = self._build_pagination_condition()
self.select_from = sqlalchemy.sql.join(
cast("FromClauseRole", self.select_from), limit_qry, on_clause
)
expr = sqlalchemy.sql.select(*self.columns)
expr = expr.select_from(cast("FromClauseRole", self.select_from))
expr = self._apply_expression_modifiers(expr)
# print("\n", expr.compile(compile_kwargs={"literal_binds": True}))
self._reset_query_parameters()
return expr
def _build_pagination_condition(
self,
) -> tuple[
sqlalchemy.sql.expression.TextClause, sqlalchemy.sql.expression.TextClause
]:
"""
In order to apply limit and offset on main table in join only
(otherwise you can get only partially constructed main model
if number of children exceeds the applied limit and select_related is used)
Used also to change first and get() without argument behaviour.
Needed only if limit or offset are set, the flag limit_sql_raw is not set
and query has select_related applied. Otherwise we can limit/offset normally
at the end of whole 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.ormar_config.pkname)
pk_aliased_name = f"{self.table.name}.{pk_alias}"
qry_text = sqlalchemy.text(f"{pk_aliased_name}")
maxes = {}
for order in list(self.sorted_orders.keys()):
if order is not None and order.get_field_name_text() != pk_aliased_name:
aliased_col = order.get_field_name_text()
# maxes[aliased_col] = order.get_text_clause()
maxes[aliased_col] = order.get_min_or_max()
elif order.get_field_name_text() == pk_aliased_name:
maxes[pk_aliased_name] = order.get_text_clause()
limit_qry: Select[Any] = sqlalchemy.sql.select(qry_text)
limit_qry = limit_qry.select_from(self.select_from) # type: ignore
limit_qry = FilterQuery(filter_clauses=self.filter_clauses).apply(limit_qry)
limit_qry = FilterQuery(
filter_clauses=self.exclude_clauses, exclude=True
).apply(limit_qry)
limit_qry = limit_qry.group_by(qry_text)
for order_by in maxes.values():
limit_qry = limit_qry.order_by(order_by)
limit_qry = LimitQuery(limit_count=self.limit_count).apply(limit_qry)
limit_qry = OffsetQuery(query_offset=self.query_offset).apply(limit_qry)
limit_qry = limit_qry.alias("limit_query") # type: ignore
on_clause = sqlalchemy.text(
f"limit_query.{pk_alias}={self.table.name}.{pk_alias}"
)
return limit_qry, on_clause # type: ignore
def _apply_expression_modifiers(
self, expr: sqlalchemy.sql.Select
) -> sqlalchemy.sql.Select:
"""
Receives the select query (might be join) and applies:
* Filter clauses
* Exclude filter clauses
* Limit clauses
* Offset clauses
* Order by clauses
Returns complete ready to run query.
:param expr: select expression before clauses
:type expr: sqlalchemy.sql.selectable.Select
:return: expression with all present clauses applied
:rtype: sqlalchemy.sql.selectable.Select
"""
expr = FilterQuery(filter_clauses=self.filter_clauses).apply(expr)
expr = FilterQuery(filter_clauses=self.exclude_clauses, exclude=True).apply(
expr
)
if not self._pagination_query_required():
expr = LimitQuery(limit_count=self.limit_count).apply(expr)
expr = OffsetQuery(query_offset=self.query_offset).apply(expr)
expr = OrderQuery(sorted_orders=self.sorted_orders).apply(expr)
return expr
def _reset_query_parameters(self) -> None:
"""
Although it should be created each time before the call we reset the key params
anyway.
"""
self.select_from = []
self.columns = []
self.used_aliases = []
ormar-orm-ormar-a7e7fde/ormar/queryset/queryset.py 0000664 0000000 0000000 00000167225 15211567312 0022603 0 ustar 00root root 0000000 0000000 import asyncio
from typing import (
TYPE_CHECKING,
Any,
AsyncGenerator,
Generic,
Iterable,
Optional,
Sequence,
TypeVar,
Union,
cast,
)
import sqlalchemy
from sqlalchemy import bindparam
import ormar # noqa I100
from ormar import MultipleMatches, NoMatch
from ormar.exceptions import (
ModelListEmptyError,
ModelPersistenceError,
QueryDefinitionError,
)
from ormar.queryset import FieldAccessor, FilterQuery, SelectAction
from ormar.queryset.actions.order_action import OrderAction
from ormar.queryset.clause import FilterGroup, QueryClause
from ormar.queryset.queries.prefetch_query import PrefetchQuery
from ormar.queryset.queries.query import Query
from ormar.queryset.reverse_alias_resolver import ReverseAliasResolver
from ormar.queryset.utils import (
extract_access_chains,
get_relationship_alias_model_and_str,
normalize_slice,
)
if TYPE_CHECKING: # pragma no cover
from ormar import Model
from ormar.models import T
from ormar.models.excludable import ExcludableItems, Slot
from ormar.models.ormar_config import OrmarConfig
else:
T = TypeVar("T", bound="Model")
class QuerySet(Generic[T]):
"""
Main class to perform database queries, exposed on each model as objects attribute.
"""
def __init__( # noqa CFQ002
self,
model_cls: Optional[type["T"]] = 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,
reverse_result: bool = False,
) -> None:
self.proxy_source_model = proxy_source_model
self.model_cls = model_cls
self.filter_clauses = [] if filter_clauses is None else filter_clauses
self.exclude_clauses = [] if exclude_clauses is None else exclude_clauses
self._select_related = [] if select_related is None else select_related
self._prefetch_related = [] if prefetch_related is None else prefetch_related
self.limit_count = limit_count
self.query_offset = offset
self._excludable = excludable or ormar.ExcludableItems()
self.order_bys = order_bys or []
self.limit_sql_raw = limit_raw_sql
self._reverse_result = reverse_result
@property
def model_config(self) -> "OrmarConfig":
"""
Shortcut to model class OrmarConfig set on QuerySet model.
: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.ormar_config
@property
def model(self) -> type["T"]:
"""
Shortcut to model class set on QuerySet.
:return: model class
:rtype: type[Model]
"""
if not self.model_cls: # pragma nocover
raise ValueError("Model class of QuerySet is not initialized")
return self.model_cls
def rebuild_self( # noqa: CFQ002
self,
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,
reverse_result: Optional[bool] = None,
) -> "QuerySet":
"""
Method that returns new instance of queryset based on passed params,
all not passed params are taken from current values.
"""
overwrites = {
"select_related": "_select_related",
"offset": "query_offset",
"excludable": "_excludable",
"prefetch_related": "_prefetch_related",
"limit_raw_sql": "limit_sql_raw",
"reverse_result": "_reverse_result",
}
passed_args = locals()
def replace_if_none(arg_name: str) -> Any:
if passed_args.get(arg_name) is None:
return getattr(self, overwrites.get(arg_name, arg_name))
return passed_args.get(arg_name)
return self.__class__(
model_cls=self.model_cls,
filter_clauses=replace_if_none("filter_clauses"),
exclude_clauses=replace_if_none("exclude_clauses"),
select_related=replace_if_none("select_related"),
limit_count=replace_if_none("limit_count"),
offset=replace_if_none("offset"),
excludable=replace_if_none("excludable"),
order_bys=replace_if_none("order_bys"),
prefetch_related=replace_if_none("prefetch_related"),
limit_raw_sql=replace_if_none("limit_raw_sql"),
proxy_source_model=replace_if_none("proxy_source_model"),
reverse_result=replace_if_none("reverse_result"),
)
async def _prefetch_related_models(
self, models: list["T"], rows: list
) -> list["T"]:
"""
Performs prefetch query for selected models names.
:param models: list of already parsed main Models from main query
:type models: list[Model]
:param rows: database rows from main query
:type rows: list[sqlalchemy.engine.result.RowProxy]
:return: list of models with prefetch models populated
:rtype: list[Model]
"""
query = PrefetchQuery(
model_cls=self.model,
excludable=self._excludable,
prefetch_related=self._prefetch_related,
select_related=self._select_related,
orders_by=self.order_bys,
)
return await query.prefetch_related(models=models) # type: ignore
async def _process_query_result_rows(
self, rows: list, plan_cache: Optional[dict] = None
) -> list["T"]:
"""
Process database rows and initialize ormar Model from each of the rows.
:param rows: list of database rows from query result
:type rows: list[sqlalchemy.engine.result.RowProxy]
:param plan_cache: optional row-extraction plan cache; ``iterate``
passes a single dict shared across all chunks so amortization
survives the per-chunk ``_process_query_result_rows`` boundary
:type plan_cache: Optional[dict]
:return: list of models
:rtype: list[Model]
"""
if plan_cache is None:
plan_cache = {}
result_rows = []
for i, row in enumerate(rows):
result_rows.append(
self.model.from_row(
row=row,
select_related=self._select_related,
excludable=self._excludable,
source_model=self.model,
proxy_source_model=self.proxy_source_model,
plan_cache=plan_cache,
)
)
if i % 100 == 99: # pragma: no cover
await asyncio.sleep(0)
if result_rows:
return self.model.merge_instances_list( # type: ignore[return-value]
result_rows, # type: ignore[arg-type]
excludable=self._excludable,
)
return cast(list["T"], result_rows)
def _resolve_filter_groups(
self, groups: Any
) -> tuple[list[FilterGroup], list[str]]:
"""
Resolves filter groups to populate FilterAction params in group tree.
:param groups: tuple of FilterGroups
:type groups: Any
:return: list of resolver groups
:rtype: tuple[list[FilterGroup], list[str]]
"""
filter_groups = []
select_related = self._select_related
if groups:
for group in groups:
if not isinstance(group, FilterGroup):
raise QueryDefinitionError(
"Only ormar.and_ and ormar.or_ "
"can be passed as filter positional"
" arguments,"
"other values need to be passed by"
"keyword arguments"
)
_, select_related = group.resolve(
model_cls=self.model,
select_related=self._select_related,
filter_clauses=self.filter_clauses,
)
filter_groups.append(group)
return filter_groups, select_related
@staticmethod
def check_single_result_rows_count(rows: Sequence[Optional["T"]]) -> None:
"""
Verifies if the result has one and only one row.
:param rows: one element list of Models
:type rows: list[Model]
"""
if not rows or rows[0] is None:
raise NoMatch()
if len(rows) > 1:
raise MultipleMatches()
@property
def table(self) -> sqlalchemy.Table:
"""
Shortcut to models table from OrmarConfig.
:return: database table
:rtype: sqlalchemy.Table
"""
return self.model_config.table
def build_select_expression(
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.
If any of the params is not passed the QuerySet own value is used.
:param limit: number to limit the query
:type limit: int
:param offset: number to offset by
:type offset: int
:param order_bys: list of order-by fields names
:type order_bys: list
:return: built sqlalchemy select expression
:rtype: sqlalchemy.sql.selectable.Select
"""
qry = Query(
model_cls=self.model,
select_related=self._select_related,
filter_clauses=self.filter_clauses,
exclude_clauses=self.exclude_clauses,
offset=offset or self.query_offset,
excludable=self._excludable,
order_bys=order_bys or self.order_bys,
limit_raw_sql=self.limit_sql_raw,
limit_count=limit if limit is not None else self.limit_count,
)
exp = qry.build_select_expression()
# print("\n", exp.compile(compile_kwargs={"literal_binds": True}))
return exp
def filter( # noqa: A003
self, *args: Any, _exclude: bool = False, **kwargs: Any
) -> "QuerySet[T]":
"""
Allows you to filter by any `Model` attribute/field
as well as to fetch instances, with a filter across an FK relationship.
You can use special filter suffix to change the filter operands:
* exact - like `album__name__exact='Malibu'` (exact match)
* iexact - like `album__name__iexact='malibu'` (exact match case insensitive)
* contains - like `album__name__contains='Mal'` (sql like)
* icontains - like `album__name__icontains='mal'` (sql like case insensitive)
* in - like `album__name__in=['Malibu', 'Barclay']` (sql in)
* isnull - like `album__name__isnull=True` (sql is null)
(isnotnull `album__name__isnull=False` (sql is not null))
* gt - like `position__gt=3` (sql >)
* gte - like `position__gte=3` (sql >=)
* lt - like `position__lt=3` (sql <)
* lte - like `position__lte=3` (sql <=)
* startswith - like `album__name__startswith='Mal'` (exact start match)
* istartswith - like `album__name__istartswith='mal'` (case insensitive)
* endswith - like `album__name__endswith='ibu'` (exact end match)
* iendswith - like `album__name__iendswith='IBU'` (case insensitive)
Note that you can also use python style filters - check the docs!
:param _exclude: flag if it should be exclude or filter
:type _exclude: bool
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: filtered QuerySet
:rtype: QuerySet
"""
filter_groups, select_related = self._resolve_filter_groups(groups=args)
qryclause = QueryClause(
model_cls=self.model,
select_related=select_related,
filter_clauses=self.filter_clauses,
)
filter_clauses, select_related = qryclause.prepare_filter(**kwargs)
filter_clauses = filter_clauses + filter_groups # type: ignore
if _exclude:
exclude_clauses = filter_clauses
filter_clauses = self.filter_clauses
else:
exclude_clauses = self.exclude_clauses
filter_clauses = filter_clauses
return self.rebuild_self(
filter_clauses=filter_clauses,
exclude_clauses=exclude_clauses,
select_related=select_related,
)
def exclude(self, *args: Any, **kwargs: Any) -> "QuerySet[T]": # noqa: A003
"""
Works exactly the same as filter and all modifiers (suffixes) are the same,
but returns a *not* condition.
So if you use `filter(name='John')` which is `where name = 'John'` in SQL,
the `exclude(name='John')` equals to `where name <> 'John'`
Note that all conditions are joined so if you pass multiple values it
becomes a union of conditions.
`exclude(name='John', age>=35)` will become
`where not (name='John' and age>=35)`
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: filtered QuerySet
:rtype: QuerySet
"""
return self.filter(_exclude=True, *args, **kwargs)
def select_related(self, related: Union[list, str, FieldAccessor]) -> "QuerySet[T]":
"""
Allows to prefetch related models during the same query.
**With `select_related` always only one query is run against the database**,
meaning that one (sometimes complicated) join is generated and later nested
models are processed in python.
To fetch related model use `ForeignKey` names.
To chain related `Models` relation use double underscores between names.
:param related: list of relation field names, can be linked by '__' to nest
:type related: Union[list, str]
:return: QuerySet
:rtype: QuerySet
"""
if not isinstance(related, list):
related = [related]
related = cast(list, extract_access_chains(related))
related = sorted(list(set(list(self._select_related) + related)))
return self.rebuild_self(select_related=related)
def select_all(self, follow: bool = False) -> "QuerySet[T]":
"""
By default adds only directly related models.
If follow=True is set it adds also related models of related models.
To not get stuck in an infinite loop as related models also keep a relation
to parent model visited models set is kept.
That way already visited models that are nested are loaded, but the load do not
follow them inside. So Model A -> Model B -> Model C -> Model A -> Model X
will load second Model A but will never follow into Model X.
Nested relations of those kind need to be loaded manually.
:param follow: flag to trigger deep save -
by default only directly related models are saved
with follow=True also related models of related models are saved
:type follow: bool
:return: reloaded Model
:rtype: Model
"""
relations = list(self.model.extract_related_names())
if follow:
relations = self.model._iterate_related_models()
return self.rebuild_self(select_related=relations)
def prefetch_related(
self, related: Union[list, str, FieldAccessor]
) -> "QuerySet[T]":
"""
Allows to prefetch related models during query - but opposite to
`select_related` each subsequent model is fetched in a separate database query.
**With `prefetch_related` always one query per Model is run against the
database**, meaning that you will have multiple queries executed one
after another.
To fetch related model use `ForeignKey` names.
To chain related `Models` relation use double underscores between names.
:param related: list of relation field names, can be linked by '__' to nest
:type related: Union[list, str]
:return: QuerySet
:rtype: QuerySet
"""
if not isinstance(related, list):
related = [related]
related = cast(list, extract_access_chains(related))
related = list(set(list(self._prefetch_related) + related))
return self.rebuild_self(prefetch_related=related)
def flatten_fields(
self,
columns: Union[list, str, set, tuple, dict, FieldAccessor],
) -> "QuerySet[T]":
"""
Render selected related models as their primary-key value on
``model_dump()`` instead of the default nested dict.
Accepts the same input forms as :py:meth:`fields` / :py:meth:`exclude_fields`
plus ``FieldAccessor`` / list of accessors. Missing relations are
auto-loaded: single-valued foreign keys go into ``select_related``,
many-to-many and reverse relations into ``prefetch_related``.
Chained calls merge. A flatten directive conflicts with any include or
exclude sub-field selection on the same relation (scalar output has no
place for children).
:param columns: relations to flatten on serialization
:type columns: Union[list, str, set, tuple, dict, FieldAccessor]
:return: new QuerySet with the flatten spec applied
:rtype: QuerySet[T]
"""
normalized = extract_access_chains(columns)
excludable = ormar.ExcludableItems.from_excludable(self._excludable)
excludable.build(
items=normalized,
model_cls=self.model_cls, # type: ignore
slot="flatten",
)
excludable.validate_flatten_vs_excludable(
source_model=self.model_cls # type: ignore
)
select_to_add, prefetch_to_add = self._classify_flatten_paths(
excludable._flatten_paths
)
new_select = sorted(set(self._select_related) | select_to_add)
new_prefetch = sorted(set(self._prefetch_related) | prefetch_to_add)
return self.rebuild_self(
excludable=excludable,
select_related=new_select,
prefetch_related=new_prefetch,
)
def _classify_flatten_paths(
self, paths: Iterable[tuple[str, ...]]
) -> tuple[set[str], set[str]]:
"""
Split flatten tuple paths into ``select_related`` (single-valued FK,
incl. self-ref) and ``prefetch_related`` (m2m / reverse) buckets so
the caller can auto-add whichever the user hasn't loaded already.
:param paths: pre-split tuple paths collected during flatten build
:type paths: Iterable[tuple[str, ...]]
:return: (paths for select_related, paths for prefetch_related) — each
as dunder strings for downstream ``select_related`` /
``prefetch_related`` call sites
:rtype: tuple[set[str], set[str]]
"""
select_paths: set[str] = set()
prefetch_paths: set[str] = set()
source_model = self.model
for parts in paths:
parent_model: type["Model"] = source_model
if len(parts) > 1:
_, parent_model, _, _ = get_relationship_alias_model_and_str(
source_model=source_model,
related_parts=list(parts[:-1]),
)
field = parent_model.ormar_config.model_fields[parts[-1]]
bucket = (
prefetch_paths
if getattr(field, "is_multi", False) or getattr(field, "virtual", False)
else select_paths
)
bucket.add("__".join(parts))
return select_paths, prefetch_paths
def fields(
self, columns: Union[list, str, set, dict], slot: "Slot" = "include"
) -> "QuerySet[T]":
"""
With `fields()` you can select subset of model columns to limit the data load.
Note that `fields()` and `exclude_fields()` works both for main models
(on normal queries like `get`, `all` etc.)
as well as `select_related` and `prefetch_related`
models (with nested notation).
You can select specified fields by passing a `str, list[str], set[str] or
dict` with nested definition.
To include related models use notation
`{related_name}__{column}[__{optional_next} etc.]`.
`fields()` can be called several times, building up the columns to select.
If you include related models into `select_related()` call but you won't specify
columns for those models in fields - implies a list of all fields for
those nested models.
Mandatory fields cannot be excluded as it will raise `ValidationError`,
to exclude a field it has to be nullable.
Pk column cannot be excluded - it's always auto added even if
not explicitly included.
You can also pass fields to include as dictionary or set.
To mark a field as included in a dictionary use it's name as key
and ellipsis as value.
To traverse nested models use nested dictionaries.
To include fields at last level instead of nested dictionary a set can be used.
To include whole nested model specify model related field name and ellipsis.
:param slot: which Excludable slot to write into ("include" or "exclude")
:type slot: Slot
:param columns: columns to include
:type columns: Union[list, str, set, dict]
:return: QuerySet
:rtype: QuerySet
"""
excludable = ormar.ExcludableItems.from_excludable(self._excludable)
excludable.build(
items=columns,
model_cls=self.model_cls, # type: ignore
slot=slot,
)
if excludable._flatten_paths:
excludable.validate_flatten_vs_excludable(
source_model=self.model_cls # type: ignore
)
return self.rebuild_self(excludable=excludable)
def exclude_fields(self, columns: Union[list, str, set, dict]) -> "QuerySet[T]":
"""
With `exclude_fields()` you can select subset of model columns that will
be excluded to limit the data load.
It's the opposite of `fields()` method so check documentation above
to see what options are available.
Especially check above how you can pass also nested dictionaries
and sets as a mask to exclude fields from whole hierarchy.
Note that `fields()` and `exclude_fields()` works both for main models
(on normal queries like `get`, `all` etc.)
as well as `select_related` and `prefetch_related` models
(with nested notation).
Mandatory fields cannot be excluded as it will raise `ValidationError`,
to exclude a field it has to be nullable.
Pk column cannot be excluded - it's always auto added even
if explicitly excluded.
:param columns: columns to exclude
:type columns: Union[list, str, set, dict]
:return: QuerySet
:rtype: QuerySet
"""
return self.fields(columns=columns, slot="exclude")
def order_by(self, columns: Union[list, str, OrderAction]) -> "QuerySet[T]":
"""
With `order_by()` you can order the results from database based on your
choice of fields.
You can provide a string with field name or list of strings with fields names.
Ordering in sql will be applied in order of names you provide in order_by.
By default if you do not provide ordering `ormar` explicitly orders by
all primary keys
If you are sorting by nested models that causes that the result rows are
unsorted by the main model `ormar` will combine those children rows into
one main model.
The main model will never duplicate in the result
To order by main model field just provide a field name
To sort on nested models separate field names with dunder '__'.
You can sort this way across all relation types -> `ForeignKey`,
reverse virtual FK and `ManyToMany` fields.
To sort in descending order provide a hyphen in front of the field name
:param columns: columns by which models should be sorted
:type columns: Union[list, str]
:return: QuerySet
:rtype: QuerySet
"""
if not isinstance(columns, list):
columns = [columns]
orders_by = [
(
OrderAction(order_str=x, model_cls=self.model_cls) # type: ignore
if not isinstance(x, OrderAction)
else x
)
for x in columns
]
order_bys = self.order_bys + [x for x in orders_by if x not in self.order_bys]
return self.rebuild_self(order_bys=order_bys)
async def values(
self,
fields: Union[list, str, set, dict, None] = None,
exclude_through: bool = False,
_as_dict: bool = True,
_flatten: bool = False,
) -> list:
"""
Return a list of dictionaries with column values in order of the fields
passed or all fields from queried models.
To filter for given row use filter/exclude methods before values,
to limit number of rows use limit/offset or paginate before values.
Note that it always return a list even for one row from database.
:param exclude_through: flag if through models should be excluded
:type exclude_through: bool
:param _flatten: internal parameter to flatten one element tuples
:type _flatten: bool
:param _as_dict: internal parameter if return dict or tuples
:type _as_dict: bool
:param fields: field name or list of field names to extract from db
:type fields: Union[list, str, set, dict]
"""
if fields:
return await self.fields(columns=fields).values(
_as_dict=_as_dict, _flatten=_flatten, exclude_through=exclude_through
)
excludable = self._excludable.with_projection_exclusions(
source_model=self.model,
select_related=self._select_related,
)
projected = (
self
if excludable is self._excludable
else self.rebuild_self(excludable=excludable)
)
expr = projected.build_select_expression()
async with self.model_config.database.get_query_executor() as executor:
rows = await executor.fetch_all(expr)
if not rows:
return []
alias_resolver = ReverseAliasResolver(
select_related=self._select_related,
excludable=excludable,
model_cls=self.model_cls, # type: ignore
exclude_through=exclude_through,
)
column_map = alias_resolver.resolve_columns(columns_names=list(rows[0].keys())) # type: ignore
result = [
{column_map.get(k): v for k, v in dict(x).items() if k in column_map}
for x in rows
]
if _as_dict:
return result
if _flatten and self._excludable.include_entry_count() != 1:
raise QueryDefinitionError(
"You cannot flatten values_list if more than one field is selected!"
)
tuple_result = [tuple(x.values()) for x in result]
return tuple_result if not _flatten else [x[0] for x in tuple_result]
async def values_list(
self,
fields: Union[list, str, set, dict, None] = None,
flatten: bool = False,
exclude_through: bool = False,
) -> list:
"""
Return a list of tuples with column values in order of the fields passed or
all fields from queried models.
When one field is passed you can flatten the list of tuples into list of values
of that single field.
To filter for given row use filter/exclude methods before values,
to limit number of rows use limit/offset or paginate before values.
Note that it always return a list even for one row from database.
:param exclude_through: flag if through models should be excluded
:type exclude_through: bool
:param fields: field name or list of field names to extract from db
:type fields: Union[str, list[str]]
:param flatten: when one field is passed you can flatten the list of tuples
:type flatten: bool
"""
return await self.values(
fields=fields,
exclude_through=exclude_through,
_as_dict=False,
_flatten=flatten,
)
async def exists(self) -> bool:
"""
Returns a bool value to confirm if there are rows matching the given criteria
(applied with `filter` and `exclude` if set).
:return: result of the check
:rtype: bool
"""
expr = self.build_select_expression()
expr = sqlalchemy.exists(expr).select()
async with self.model_config.database.get_query_executor() as executor:
result = await executor.fetch_val(expr)
return bool(result)
async def count(self, distinct: bool = True) -> int:
"""
Returns number of rows matching the given criteria
(applied with `filter` and `exclude` if set before).
If `distinct` is `True` (the default), this will return
the number of primary rows selected. If `False`,
the count will be the total number of rows returned
(including extra rows for `one-to-many` or `many-to-many`
left `select_related` table joins).
`False` is the legacy (buggy) behavior for workflows that depend on it.
:param distinct: flag if the primary table rows should be distinct or not
:return: number of rows
:rtype: int
"""
expr = self.build_select_expression().alias("subquery_for_count")
expr = sqlalchemy.func.count().select().select_from(expr) # type: ignore
if distinct:
pk_column_name = self.model.get_column_alias(self.model_config.pkname)
expr_distinct = expr.group_by(pk_column_name).alias("subquery_for_group") # type: ignore
expr = sqlalchemy.func.count().select().select_from(expr_distinct) # type: ignore
async with self.model_config.database.get_query_executor() as executor:
result = await executor.fetch_val(expr) # type: ignore
return int(result) if result is not None else 0
async def _query_aggr_function(self, func_name: str, columns: list) -> Any:
func = getattr(sqlalchemy.func, func_name)
select_actions = [
SelectAction(select_str=column, model_cls=self.model) for column in columns
]
if func_name in ["sum", "avg"]:
if any(not x.is_numeric for x in select_actions):
raise QueryDefinitionError(
"You can use sum and svg only with numeric types of columns"
)
if any(x.field_name not in x.target_model.model_fields for x in select_actions):
raise QueryDefinitionError(
"You can use aggregate functions only on "
"existing columns of the target model"
)
select_columns = [x.apply_func(func, use_label=True) for x in select_actions]
expr = self.build_select_expression().alias(f"subquery_for_{func_name}")
expr = sqlalchemy.select(*select_columns).select_from(expr) # type: ignore
# print("\n", expr.compile(compile_kwargs={"literal_binds": True}))
async with self.model_config.database.get_query_executor() as executor:
result = await executor.fetch_one(expr) # type: ignore
return dict(result) if len(result) > 1 else result[columns[0]] # type: ignore
async def max(self, columns: Union[str, list[str]]) -> Any: # noqa: A003
"""
Returns max value of columns for rows matching the given criteria
(applied with `filter` and `exclude` if set before).
:return: max value of column(s)
:rtype: Any
"""
if not isinstance(columns, list):
columns = [columns]
return await self._query_aggr_function(func_name="max", columns=columns)
async def min(self, columns: Union[str, list[str]]) -> Any: # noqa: A003
"""
Returns min value of columns for rows matching the given criteria
(applied with `filter` and `exclude` if set before).
:return: min value of column(s)
:rtype: Any
"""
if not isinstance(columns, list):
columns = [columns]
return await self._query_aggr_function(func_name="min", columns=columns)
async def sum(self, columns: Union[str, list[str]]) -> Any: # noqa: A003
"""
Returns sum value of columns for rows matching the given criteria
(applied with `filter` and `exclude` if set before).
:return: sum value of columns
:rtype: int
"""
if not isinstance(columns, list):
columns = [columns]
return await self._query_aggr_function(func_name="sum", columns=columns)
async def avg(self, columns: Union[str, list[str]]) -> Any:
"""
Returns avg value of columns for rows matching the given criteria
(applied with `filter` and `exclude` if set before).
:return: avg value of columns
:rtype: Union[int, float, list]
"""
if not isinstance(columns, list):
columns = [columns]
return await self._query_aggr_function(func_name="avg", columns=columns)
async def update(self, each: bool = False, **kwargs: Any) -> int:
"""
Updates the model table after applying the filters from kwargs.
You have to either pass a filter to narrow down a query or explicitly pass
each=True flag to affect whole table.
:param each: flag if whole table should be affected if no filter is passed
:type each: bool
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: number of updated rows
:rtype: int
"""
if not each and not (self.filter_clauses or self.exclude_clauses):
raise QueryDefinitionError(
"You cannot update without filtering the queryset first. "
"If you want to update all rows use update(each=True, **kwargs)"
)
self_fields = self.model.extract_db_own_fields().union(
self.model.extract_related_names()
)
updates = {k: v for k, v in kwargs.items() if k in self_fields}
updates = self.model.populate_onupdate_value(updates)
updates = self.model.validate_enums(updates)
updates = self.model.translate_columns_to_aliases(updates)
expr = FilterQuery(filter_clauses=self.filter_clauses).apply(
self.table.update().values(**updates) # type: ignore
)
expr = FilterQuery(filter_clauses=self.exclude_clauses, exclude=True).apply(
expr
)
async with self.model_config.database.get_query_executor() as executor:
return await executor.execute(expr)
async def delete(self, *args: Any, each: bool = False, **kwargs: Any) -> int:
"""
Deletes from the model table after applying the filters from kwargs.
You have to either pass a filter to narrow down a query or explicitly pass
each=True flag to affect whole table.
:param each: flag if whole table should be affected if no filter is passed
:type each: bool
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: number of deleted rows
:rtype:int
"""
if kwargs or args:
return await self.filter(*args, **kwargs).delete()
if not each and not (self.filter_clauses or self.exclude_clauses):
raise QueryDefinitionError(
"You cannot delete without filtering the queryset first. "
"If you want to delete all rows use delete(each=True)"
)
expr = FilterQuery(filter_clauses=self.filter_clauses).apply(
self.table.delete() # type: ignore
)
expr = FilterQuery(filter_clauses=self.exclude_clauses, exclude=True).apply(
expr
)
async with self.model_config.database.get_query_executor() as executor:
return await executor.execute(expr)
def paginate(self, page: int, page_size: int = 20) -> "QuerySet[T]":
"""
You can paginate the result which is a combination of offset and limit clauses.
Limit is set to page size and offset is set to (page-1) * page_size.
:param page_size: numbers of items per page
:type page_size: int
:param page: page number
:type page: int
:return: QuerySet
:rtype: QuerySet
"""
if page < 1 or page_size < 1:
raise QueryDefinitionError("Page size and page have to be greater than 0.")
limit_count = page_size
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: Optional[bool] = None
) -> "QuerySet[T]":
"""
You can limit the results to desired number of parent models.
To limit the actual number of database query rows instead of number of main
models use the `limit_raw_sql` parameter flag, and set it to `True`.
:param limit_raw_sql: flag if raw sql should be limited
:type limit_raw_sql: bool
:param limit_count: number of models to limit
:type limit_count: int
:return: QuerySet
:rtype: QuerySet
"""
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: Optional[bool] = None
) -> "QuerySet[T]":
"""
You can also offset the results by desired number of main models.
To offset the actual number of database query rows instead of number of main
models use the `limit_raw_sql` parameter flag, and set it to `True`.
:param limit_raw_sql: flag if raw sql should be offset
:type limit_raw_sql: bool
:param offset: numbers of models to offset
:type offset: int
:return: QuerySet
:rtype: QuerySet
"""
limit_raw_sql = self.limit_sql_raw if limit_raw_sql is None else limit_raw_sql
return self.rebuild_self(offset=offset, limit_raw_sql=limit_raw_sql)
def _single_row_order_bys(self, reverse: bool) -> list["OrderAction"]:
"""
Builds the ``order_bys`` list used by :meth:`first` and :meth:`last`.
A pk-based tiebreaker is prepended only when the user has not
supplied an ordering on the source model (otherwise the user's
choice must win). For :meth:`last`, both the pk tiebreaker and the
user's order_bys are flipped so that ``LIMIT 1`` pulls the tail of
the user's natural order.
:param reverse: whether to flip directions for the ``last()`` case
:type reverse: bool
:return: combined list of OrderAction objects
:rtype: list[OrderAction]
"""
pk_default = OrderAction(
order_str=self.model.ormar_config.pkname,
model_cls=self.model_cls, # type: ignore
)
user_orders = self.order_bys
if reverse:
pk_default = pk_default.flipped()
user_orders = [ob.flipped() for ob in user_orders]
has_source_order = any(x.is_source_model_order for x in self.order_bys)
prefix: list["OrderAction"] = [] if has_source_order else [pk_default]
return prefix + list(user_orders)
async def _fetch_single(self, order_bys: list["OrderAction"]) -> "T":
"""
Runs a ``LIMIT 1`` select with the given ``order_bys``, merges results,
runs any configured prefetch, and enforces the single-row contract
(raises ``NoMatch`` / ``MultipleMatches``). Shared implementation
backing :meth:`first` and :meth:`last`.
:param order_bys: fully-resolved order list to apply to the query
:type order_bys: list[OrderAction]
:return: the single fetched model
:rtype: Model
"""
expr = self.build_select_expression(limit=1, order_bys=order_bys)
async with self.model_config.database.get_query_executor() as executor:
rows = await executor.fetch_all(expr)
processed_rows = await self._process_query_result_rows(rows)
if self._prefetch_related and processed_rows:
processed_rows = await self._prefetch_related_models(processed_rows, rows)
self.check_single_result_rows_count(processed_rows)
return processed_rows[0] # type: ignore
async def first(self, *args: Any, **kwargs: Any) -> "T":
"""
Gets the first row from the db ordered by primary key column ascending.
Passing args and/or kwargs is a shortcut and equals to calling
``filter(*args, **kwargs).first()``.
The query is always executed with ``LIMIT 1`` so at most one row is
fetched; use :meth:`get` if you want to assert that the criteria
match exactly one row.
:raises NoMatch: if no rows match the criteria
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model
:rtype: Model
"""
if kwargs or args:
return await self.filter(*args, **kwargs).first()
return await self._fetch_single(self._single_row_order_bys(reverse=False))
async def first_or_none(self, *args: Any, **kwargs: Any) -> Optional["T"]:
"""
Gets the first row from the db ordered by primary key column ascending.
Behaves like :meth:`first` but returns ``None`` instead of raising
``NoMatch`` when no row matches.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model or None
:rtype: Optional[Model]
"""
try:
return await self.first(*args, **kwargs)
except ormar.NoMatch:
return None
async def last(self, *args: Any, **kwargs: Any) -> "T":
"""
Gets the last row from the db ordered by primary key column descending.
Complementary to :meth:`first`: the default pk ordering is flipped
and the top row is returned. Passing args and/or kwargs is a shortcut
and equals to calling ``filter(*args, **kwargs).last()``.
The query is always executed with ``LIMIT 1`` so at most one row is
fetched; use :meth:`get` if you want to assert that the criteria
match exactly one row.
:raises NoMatch: if no rows match the criteria
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model
:rtype: Model
"""
if kwargs or args:
return await self.filter(*args, **kwargs).last()
return await self._fetch_single(self._single_row_order_bys(reverse=True))
async def last_or_none(self, *args: Any, **kwargs: Any) -> Optional["T"]:
"""
Gets the last row from the db ordered by primary key column descending.
Behaves like :meth:`last` but returns ``None`` instead of raising
``NoMatch`` when no row matches.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model or None
:rtype: Optional[Model]
"""
try:
return await self.last(*args, **kwargs)
except ormar.NoMatch:
return None
async def get_or_none(self, *args: Any, **kwargs: Any) -> Optional["T"]:
"""
Gets a single row from the db matching the criteria set by args/kwargs.
Behaves like :meth:`get` but returns ``None`` instead of raising
``NoMatch`` when no row matches. ``MultipleMatches`` is still raised
when criteria are provided and more than one row matches.
:raises MultipleMatches: when criteria are set (via ``filter``/``exclude``
or args/kwargs) and more than one row matches them.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model or None
:rtype: Optional[Model]
"""
try:
return await self.get(*args, **kwargs)
except ormar.NoMatch:
return None
async def get(self, *args: Any, **kwargs: Any) -> "T": # noqa: CCR001
"""
Gets a single row from the db matching the criteria set by args/kwargs.
Passing args/kwargs is a shortcut for ``filter(*args, **kwargs).get()``.
When criteria are set (either through args/kwargs here or via a
chained :meth:`filter`/:meth:`exclude`) the query fetches every
matching row and asserts that exactly one comes back — ``NoMatch``
is raised when none match and ``MultipleMatches`` when more than one
matches.
When no criteria are set, ``get()`` falls back to returning the last
row of the table ordered by primary key descending (``LIMIT 1``); in
that mode ``MultipleMatches`` cannot be raised.
:raises NoMatch: if no rows match the criteria
:raises MultipleMatches: when criteria are set and more than one row
matches them.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model
:rtype: Model
"""
if kwargs or args:
return await self.filter(*args, **kwargs).get()
if not self.filter_clauses:
expr = self.build_select_expression(
limit=1,
order_bys=(
[
OrderAction(
order_str=f"-{self.model.ormar_config.pkname}",
model_cls=self.model_cls, # type: ignore
)
]
if not any([x.is_source_model_order for x in self.order_bys])
else []
)
+ self.order_bys,
)
else:
expr = self.build_select_expression()
async with self.model_config.database.get_query_executor() as executor:
rows = await executor.fetch_all(expr)
processed_rows = await self._process_query_result_rows(rows)
if self._prefetch_related and processed_rows:
processed_rows = await self._prefetch_related_models(processed_rows, rows)
self.check_single_result_rows_count(processed_rows)
return processed_rows[0] # type: ignore
async def get_or_create(
self,
_defaults: Optional[dict[str, Any]] = None,
*args: Any,
**kwargs: Any,
) -> tuple["T", bool]:
"""
Combination of create and get methods.
Tries to get a row meeting the criteria for kwargs
and if `NoMatch` exception is raised
it creates a new one with given kwargs and _defaults.
If a concurrent caller wins the race and creates a matching row
between this call's ``get`` and ``create``, the ``create`` raises
``sqlalchemy.exc.IntegrityError`` and ``get`` is retried once.
If the retry still finds no row, the original ``IntegrityError`` is
re-raised because the violation isn't a race but a legitimate
constraint conflict (e.g. a different unique key).
Passing a criteria is actually calling filter(*args, **kwargs) method described
below.
:param kwargs: fields names and proper value types
:type kwargs: Any
:param _defaults: default values for creating object
:type _defaults: Optional[dict[str, Any]]
:return: model instance and a boolean
:rtype: tuple("T", bool)
"""
try:
return await self.get(*args, **kwargs), False
except NoMatch:
pass
_defaults = _defaults or {}
try:
return await self.create(**{**kwargs, **_defaults}), True
except sqlalchemy.exc.IntegrityError as integrity_error:
try:
return await self.get(*args, **kwargs), False
except NoMatch:
raise integrity_error from None
async def update_or_create(self, **kwargs: Any) -> "T":
"""
Updates the model, or in case there is no match in database creates a new one.
If a pk is supplied but no row exists with that pk, a new row is
created with the supplied pk and field values rather than raising
``NoMatch``.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: updated or created model
:rtype: Model
"""
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:
return await self.create(**kwargs)
try:
model = await self.get(pk=kwargs[pk_name])
except NoMatch:
return await self.create(**kwargs)
return await model.update(**kwargs)
async def all(self, *args: Any, **kwargs: Any) -> list["T"]: # noqa: A003
"""
Returns all rows from a database for given model for set filter options.
Passing args and/or kwargs is a shortcut and equals to calling
`filter(*args, **kwargs).all()`.
If there are no rows meeting the criteria an empty list is returned.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: list of returned models
:rtype: list[Model]
"""
if kwargs or args:
return await self.filter(*args, **kwargs).all()
expr = self.build_select_expression()
async with self.model_config.database.get_query_executor() as executor:
rows = await executor.fetch_all(expr)
result_rows = await self._process_query_result_rows(rows)
if self._prefetch_related and result_rows:
result_rows = await self._prefetch_related_models(result_rows, rows)
if self._reverse_result:
result_rows.reverse()
return result_rows
async def iterate( # noqa: A003
self,
*args: Any,
**kwargs: Any,
) -> AsyncGenerator["T", None]:
"""
Return async iterable generator for all rows from a database for given model.
Passing args and/or kwargs is a shortcut and equals to calling
`filter(*args, **kwargs).iterate()`.
If there are no rows meeting the criteria an empty async generator is returned.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: asynchronous iterable generator of returned models
:rtype: AsyncGenerator[Model]
"""
if self._prefetch_related:
raise QueryDefinitionError(
"Prefetch related queries are not supported in iterators"
)
if kwargs or args:
async for result in self.filter(*args, **kwargs).iterate():
yield result
return
expr = self.build_select_expression()
rows: list = []
last_primary_key = None
pk_alias = self.model.get_column_alias(self.model_config.pkname)
# Single shared cache across all yielded chunks so 1-row chunks
# (the common iterate case) still amortize the plan build.
plan_cache: dict = {}
# Server-side cursor (asyncpg/aiomysql) requires an open transaction,
# which AUTOCOMMIT does not provide.
async with self.model_config.database.get_query_executor(
transactional=True
) as executor:
async for row in executor.iterate(expr):
current_primary_key = row[pk_alias]
if last_primary_key == current_primary_key or last_primary_key is None:
last_primary_key = current_primary_key
rows.append(row)
continue
yield (await self._process_query_result_rows(rows, plan_cache))[0]
last_primary_key = current_primary_key
rows = [row]
if rows:
yield (await self._process_query_result_rows(rows, plan_cache))[0]
async def create(self, **kwargs: Any) -> "T":
"""
Creates the model instance, saves it in a database and returns the updates model
(with pk populated if not passed and autoincrement is set).
The allowed kwargs are `Model` fields names and proper value types.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: created model
:rtype: Model
"""
instance = self.model(**kwargs)
instance = await instance.save()
return instance
async def bulk_create(self, objects: list["T"]) -> None:
"""
Performs a bulk create in one database session to speed up the process.
Allows you to create multiple objects at once.
A valid list of `Model` objects needs to be passed.
Bulk operations do not send signals.
:param objects: list of ormar models already initialized and ready to save.
:type objects: list[Model]
"""
if not objects:
raise ModelListEmptyError("Bulk create objects are empty!")
ready_objects = []
for i, obj in enumerate(objects):
ready_objects.append(obj.prepare_model_to_save(obj.model_dump()))
if i % 100 == 99: # pragma: no cover
await asyncio.sleep(0)
# don't use execute_many, as in databases it's executed in a loop
# instead of using execute_many from drivers
expr = self.table.insert().values(ready_objects)
async with self.model_config.database.get_query_executor() as executor:
await executor.execute(expr)
for obj in objects:
obj.set_save_status(True)
async def bulk_update( # noqa: CCR001
self, objects: list["T"], columns: Optional[list[str]] = None
) -> None:
"""
Performs bulk update in one database session to speed up the process.
Allows you to update multiple instance at once.
All `Models` passed need to have primary key column populated.
You can also select which fields to update by passing `columns` list
as a list of string names.
Bulk operations do not send signals.
:param objects: list of ormar models
:type objects: list[Model]
:param columns: list of columns to update
:type columns: list[str]
"""
if not objects:
raise ModelListEmptyError("Bulk update objects are empty!")
ready_objects = []
pk_name = self.model_config.pkname
if not columns:
columns = list(
self.model.extract_db_own_fields().union(
self.model.extract_related_names()
)
)
if pk_name not in columns:
columns.append(pk_name)
onupdate_fields = self.model._onupdate_fields
for field_name in onupdate_fields:
if field_name not in columns:
columns.append(field_name)
columns = [self.model.get_column_alias(k) for k in columns]
for i, obj in enumerate(objects):
explicit_fields = obj.__setattr_fields__
new_kwargs = obj.model_dump()
if new_kwargs.get(pk_name) is None:
raise ModelPersistenceError(
"You cannot update unsaved objects. "
f"{self.model.__name__} has to have {pk_name} filled."
)
new_kwargs = obj.populate_onupdate_value(
new_kwargs, explicit_fields=explicit_fields
)
obj.update_from_dict(
{
field_name: new_kwargs[field_name]
for field_name in onupdate_fields - explicit_fields
}
)
new_kwargs = obj.prepare_model_to_update(new_kwargs)
ready_objects.append(
{"new_" + k: v for k, v in new_kwargs.items() if k in columns}
)
if i % 100 == 99: # pragma: no cover
await asyncio.sleep(0)
pk_column: sqlalchemy.Column = self.model_config.table.c[
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_config.table.c]
expr = self.table.update().where(
pk_column == bindparam("new_" + pk_column_name)
)
expr = expr.values(
**{
k: bindparam("new_" + k)
for k in columns
if k != pk_column_name and k in table_columns
}
)
# databases bind params only where query is passed as string
# otherwise it just passes all data to values and results in unconsumed columns
expr = str(expr) # type: ignore
# Multi-row execute_many: run in an explicit transaction so all rows
# share a single COMMIT instead of one per row under AUTOCOMMIT.
async with self.model_config.database.get_query_executor(
transactional=True
) as executor:
await executor.execute_many(expr, ready_objects)
for obj in objects:
obj.set_save_status(True)
obj.__setattr_fields__.clear()
await cast(
type["Model"], self.model_cls
).ormar_config.signals.post_bulk_update.send(
sender=self.model_cls, # type: ignore
instances=objects,
)
def __getitem__(self, key: Union[int, slice]) -> "QuerySet[T]":
"""
Returns a new ``QuerySet`` with LIMIT/OFFSET derived from a Python
integer index or slice.
Negative indices and negative slice bounds are translated into a
reversed-order query plus an in-memory list reversal on ``.all()``,
so that ``Model.objects[-N:]`` returns the last ``N`` rows in the
original ordering. Slice shapes that would require a ``COUNT(*)``
round-trip (a bare ``[:-N]``, or mixed positive/negative bounds) are
rejected with a ``QueryDefinitionError``; use ``.count()`` combined
with ``.offset()``/``.limit()`` instead.
Each call replaces the previous pagination state rather than composing
with it — subsequent slicing on an already-sliced queryset is not
guaranteed to compose in a Python-list-like way.
:param key: integer index or slice
:type key: int | slice
:raises QueryDefinitionError: when ``key`` is not an ``int``/``slice``
or when the slice cannot be expressed without a count
:return: new QuerySet with updated pagination
:rtype: QuerySet
"""
bounds = normalize_slice(key)
order_bys = self.order_bys
if self._reverse_result != bounds.reverse:
if not order_bys:
order_bys = OrderAction.from_model_defaults(self.model)
order_bys = [ob.flipped() for ob in order_bys]
return self.rebuild_self(
limit_count=bounds.limit,
offset=bounds.offset,
order_bys=order_bys,
reverse_result=bounds.reverse,
)
ormar-orm-ormar-a7e7fde/ormar/queryset/reverse_alias_resolver.py 0000664 0000000 0000000 00000021757 15211567312 0025466 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, cast
if TYPE_CHECKING: # pragma: no cover
from ormar import ForeignKeyField, Model
from ormar.models.excludable import Excludable, ExcludableItems
class ReverseAliasResolver:
"""
Class is used to reverse resolve table aliases into relation strings
to parse raw data columns and replace table prefixes with full relation string
"""
def __init__(
self,
model_cls: type["Model"],
excludable: "ExcludableItems",
select_related: list[str],
exclude_through: bool = False,
) -> None:
self.select_related = select_related
self.model_cls = model_cls
self.reversed_aliases = (
self.model_cls.ormar_config.alias_manager.reversed_aliases
)
self.excludable = excludable
self.exclude_through = exclude_through
self._fields: dict[str, "ForeignKeyField"] = dict()
self._prefixes: dict[str, str] = dict()
self._previous_prefixes: list[str] = [""]
self._resolved_names: dict[str, str] = dict()
def resolve_columns(self, columns_names: list[str]) -> dict:
"""
Takes raw query prefixed column and resolves the prefixes to
relation strings (relation names connected with dunders).
:param columns_names: list of column names with prefixes from query
:type columns_names: list[str]
:return: dictionary of prefix: resolved names
:rtype: Union[None, dict[str, str]]
"""
self._create_prefixes_map()
for column_name in columns_names:
column_parts = column_name.split("_")
potential_prefix = column_parts[0]
if potential_prefix in self.reversed_aliases:
self._resolve_column_with_prefix(
column_name=column_name, prefix=potential_prefix
)
else:
allowed_columns = self.model_cls.own_table_columns(
model=self.model_cls,
excludable=self.excludable,
add_pk_columns=False,
)
if column_name in allowed_columns:
self._resolved_names[column_name] = column_name
return self._resolved_names
def _resolve_column_with_prefix(self, column_name: str, prefix: str) -> None:
"""
Takes the prefixed column, checks if field should be excluded, and if not
it proceeds to replace prefix of a table with full relation string.
Sample: translates: "xsd12df_name" -> into: "posts__user__name"
:param column_name: prefixed name of the column
:type column_name: str
:param prefix: extracted prefix
:type prefix: str
"""
relation = self.reversed_aliases.get(prefix, None)
assert relation is not None, "Prefix not found in reversed aliases"
relation_str = self._prefixes.get(relation, None)
field = self._fields.get(relation, None)
if relation_str is None or field is None:
return
is_through = field.is_multi and field.through.get_name() in relation_str
if self._check_if_field_is_excluded(
prefix=prefix, field=field, is_through=is_through
):
return
target_model = field.through if is_through else field.to
allowed_columns = target_model.own_table_columns(
model=target_model,
excludable=self.excludable,
alias=prefix,
add_pk_columns=False,
)
new_column_name = column_name.replace(f"{prefix}_", "")
if new_column_name in allowed_columns:
self._resolved_names[column_name] = column_name.replace(
f"{prefix}_", f"{relation_str}__"
)
def _check_if_field_is_excluded(
self, prefix: str, field: "ForeignKeyField", is_through: bool
) -> bool:
"""
Checks if given relation is excluded in current query.
Note that in contrary to other queryset methods here you can exclude the
in-between models but keep the end columns, which does not make sense
when parsing the raw data into models.
So in relation category -> category_x_post -> post -> user you can exclude
category_x_post and post models but can keep the user one. (in ormar model
context that is not possible as if you would exclude through and post model
there would be no way to reach user model).
Exclusions happen on a model before the current one, so we need to move back
in chain of model by one or by two (m2m relations have through model in between)
:param prefix: table alias
:type prefix: str
:param field: field with relation
:type field: ForeignKeyField
:param is_through: flag if current table is a through table
:type is_through: bool
:return: result of the check
:rtype: bool
"""
shift, field_name = 1, field.name
if is_through:
field_name = field.through.get_name()
elif field.is_multi:
shift = 2
previous_excludable = self._get_previous_excludable(
prefix=prefix, field=field, shift=shift
)
return previous_excludable.is_excluded(field_name)
def _get_previous_excludable(
self, prefix: str, field: "ForeignKeyField", shift: int = 1
) -> "Excludable":
"""
Returns excludable related to model previous in chain of models.
Used to check if current model should be excluded.
:param prefix: prefix of a current table
:type prefix: str
:param field: field with relation
:type field: ForeignKeyField
:param shift: how many model back to go - for m2m it's 2 due to through models
:type shift: int
:return: excludable for previous model
:rtype: Excludable
"""
if prefix not in self._previous_prefixes:
self._previous_prefixes.append(prefix)
previous_prefix_ind = self._previous_prefixes.index(prefix)
previous_prefix = (
self._previous_prefixes[previous_prefix_ind - shift]
if previous_prefix_ind > (shift - 1)
else ""
)
return self.excludable.get(field.owner, alias=previous_prefix)
def _create_prefixes_map(self) -> None:
"""
Creates a map of alias manager aliases keys to relation strings.
I.e in alias manager you can have alias user_roles: xas12ad
This method will create entry user_roles: roles, where roles is a name of
relation on user model.
Will also keep the relation field in separate dictionary so we can later
extract field names and owner models.
"""
for related in self.select_related:
model_cls = self.model_cls
related_split = related.split("__")
related_str = ""
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.ormar_config.model_fields[relation]
field = cast("ForeignKeyField", field)
prefix_name = self._handle_through_fields_and_prefix(
model_cls=model_cls,
field=field,
previous_related_str=previous_related_str,
relation=relation,
)
self._prefixes[prefix_name] = new_related_str
self._fields[prefix_name] = field
model_cls = field.to
related_str = new_related_str
def _handle_through_fields_and_prefix(
self,
model_cls: type["Model"],
field: "ForeignKeyField",
previous_related_str: str,
relation: str,
) -> str:
"""
Registers through models for m2m relations and switches prefix for
the one linking from through model to target model.
For other relations returns current model name + relation name as prefix.
Nested relations are a chain of relation names with __ in between.
:param model_cls: model of current relation
:type model_cls: type["Model"]
:param field: field with relation
:type field: ForeignKeyField
:param previous_related_str: concatenated chain linked with "__"
:type previous_related_str: str
:param relation: name of the current relation in chain
:type relation: str
:return: name of prefix to populate
:rtype: str
"""
prefix_name = f"{model_cls.get_name()}_{relation}"
if field.is_multi:
through_name = field.through.get_name()
if not self.exclude_through:
self._fields[prefix_name] = field
new_through_str = previous_related_str + through_name
self._prefixes[prefix_name] = new_through_str
prefix_name = f"{through_name}_{field.default_target_field_name()}"
return prefix_name
ormar-orm-ormar-a7e7fde/ormar/queryset/utils.py 0000664 0000000 0000000 00000043771 15211567312 0022061 0 ustar 00root root 0000000 0000000 import collections.abc
import copy
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Iterable, Optional, Union
import ormar_rust_utils
from ormar.exceptions import QueryDefinitionError
if TYPE_CHECKING: # pragma no cover
from ormar import BaseField, Model
PathParts = tuple[str, ...]
def build_flatten_map(paths: Iterable[PathParts]) -> dict:
"""
Build a nested-Ellipsis dict from pre-split flatten path tuples — the
runtime representation threaded through ``model_dump`` recursion.
Example::
paths = [
("author", "name"),
("author", "email"),
("category", "title"),
]
build_flatten_map(paths) == {
"author": {"name": ..., "email": ...},
"category": {"title": ...},
}
:param paths: iterable of tuple paths (each path already split on ``__``)
:type paths: Iterable[PathParts]
:return: nested dict where leaves are ``...``
:rtype: dict
"""
result: dict = {}
for parts in paths:
current = result
last = len(parts) - 1
for i, part in enumerate(parts):
if i == last:
current[part] = ...
else:
current = current.setdefault(part, {})
return result
def extract_access_chains(value: Any) -> Any:
"""
Unwrap ``FieldAccessor`` inputs (or lists that contain them) into their
underlying dunder-path strings so downstream parsers see a uniform shape.
Anything that isn't an accessor (or list of accessors) is returned
unchanged — sets, tuples, dicts, and plain strings all pass through.
:param value: user input for a relation-spec method (``select_related``,
``prefetch_related``, ``flatten_fields``)
:type value: Any
:return: a dunder string, a list with each accessor replaced by its chain,
or the original value unchanged
:rtype: Any
"""
# Late import to avoid circular dependency with queryset package.
from ormar.queryset.field_accessor import FieldAccessor
if isinstance(value, FieldAccessor):
return [value._access_chain]
if isinstance(value, list):
return [
item._access_chain if isinstance(item, FieldAccessor) else item
for item in value
]
return value
@dataclass(frozen=True)
class SliceBounds:
"""
Normalized pagination parameters produced by :func:`normalize_slice`.
``reverse=True`` signals that the ORDER BY directions should be flipped
at query build time and the fetched list reversed in memory so the
caller still sees rows in the original ordering. This is how negative
indices and negative slice bounds are emulated without a ``COUNT(*)``
round-trip.
:ivar limit: row count for ``LIMIT``; ``None`` means "no limit"
:vartype limit: Optional[int]
:ivar offset: row count for ``OFFSET``; always non-negative
:vartype offset: int
:ivar reverse: whether the query ordering must be flipped and the
result list reversed in memory
:vartype reverse: bool
"""
limit: Optional[int]
offset: int
reverse: bool
def normalize_slice(key: Union[int, slice]) -> SliceBounds:
"""
Top-level dispatcher: turns a Python integer index or slice into a
:class:`SliceBounds` suitable for ``QuerySet``.
Delegates to a dedicated helper for each shape. Any shape that would
require a ``COUNT(*)`` round-trip (``step != 1``, a bare ``[:-N]``, or
mixed-sign bounds) raises ``QueryDefinitionError``.
Examples::
5 → SliceBounds(limit=1, offset=5, reverse=False)
slice(2, 8) → SliceBounds(limit=6, offset=2, reverse=False)
slice(-3, None) → SliceBounds(limit=3, offset=0, reverse=True)
:param key: integer or slice passed to ``QuerySet.__getitem__``
:type key: int | slice
:return: normalized slice parameters
:rtype: SliceBounds
"""
if isinstance(key, int):
return _int_to_limit_offset(key)
if isinstance(key, slice):
return _slice_to_limit_offset(key)
raise QueryDefinitionError(
f"QuerySet indices must be integers or slices, not {type(key).__name__}."
)
def _int_to_limit_offset(key: int) -> SliceBounds:
"""
Handles ``Model.objects[N]`` and ``Model.objects[-N]`` — a single-row
pick from the head or tail of the result. Negative indices are emulated
by flipping the ORDER BY and offsetting ``|N|-1`` rows from the end.
Examples::
[0] → SliceBounds(limit=1, offset=0, reverse=False)
[5] → SliceBounds(limit=1, offset=5, reverse=False)
[-1] → SliceBounds(limit=1, offset=0, reverse=True)
[-3] → SliceBounds(limit=1, offset=2, reverse=True)
:param key: non-negative index from the head, or negative from the tail
:type key: int
:return: ``SliceBounds(limit=1, offset=..., reverse=...)``
:rtype: SliceBounds
"""
if key >= 0:
return SliceBounds(limit=1, offset=key, reverse=False)
return SliceBounds(limit=1, offset=-key - 1, reverse=True)
def _slice_to_limit_offset(key: slice) -> SliceBounds:
"""
Dispatches a ``slice`` object to the shape-specific helper: open on both
ends, open-start, open-stop, or fully bounded. Validates ``step`` up
front because it is the only constraint that applies to every shape.
Examples::
[:] → SliceBounds(limit=None, offset=0, reverse=False)
[:8] → _slice_head(8)
[3:] → _slice_tail(3)
[2:8] → _slice_range(2, 8)
[::2] → raises QueryDefinitionError (step != 1)
:param key: Python slice passed to ``__getitem__``
:type key: slice
:return: normalized slice parameters
:rtype: SliceBounds
"""
if key.step is not None and key.step != 1:
raise QueryDefinitionError(f"Slice step {key.step} is not supported, only 1.")
start, stop = key.start, key.stop
if start is None and stop is None:
return SliceBounds(limit=None, offset=0, reverse=False)
if start is None:
return _slice_head(stop)
if stop is None:
return _slice_tail(start)
return _slice_range(start, stop)
def _slice_head(stop: int) -> SliceBounds:
"""
Handles ``Model.objects[:N]`` — the first ``N`` rows of the result set.
Negative ``stop`` would mean "everything except the last ``|N|`` rows",
which needs a ``COUNT(*)`` to resolve, so it is rejected.
Examples::
[:5] → SliceBounds(limit=5, offset=0, reverse=False)
[:0] → SliceBounds(limit=0, offset=0, reverse=False)
[:-2] → raises QueryDefinitionError
:param stop: upper bound from the slice
:type stop: int
:return: ``SliceBounds(limit=stop, offset=0, reverse=False)``
:rtype: SliceBounds
"""
if stop < 0:
raise QueryDefinitionError(
"Negative slice stop without a start requires an explicit count; "
"use .count() with .offset()/.limit() instead."
)
return SliceBounds(limit=stop, offset=0, reverse=False)
def _slice_tail(start: int) -> SliceBounds:
"""
Handles ``Model.objects[N:]`` and ``Model.objects[-N:]`` — everything
from ``N`` onwards, or the last ``|N|`` rows. The negative case is
emulated by flipping the ORDER BY and taking the first ``|N|`` rows.
Examples::
[3:] → SliceBounds(limit=None, offset=3, reverse=False)
[-5:] → SliceBounds(limit=5, offset=0, reverse=True)
[-1:] → SliceBounds(limit=1, offset=0, reverse=True)
:param start: lower bound from the slice
:type start: int
:return: normalized slice parameters; ``limit`` is ``None`` for the
positive case (no upper bound)
:rtype: SliceBounds
"""
if start < 0:
return SliceBounds(limit=-start, offset=0, reverse=True)
return SliceBounds(limit=None, offset=start, reverse=False)
def _slice_range(start: int, stop: int) -> SliceBounds:
"""
Handles ``Model.objects[A:B]`` with both bounds set. Mixed-sign bounds
(e.g. ``[3:-2]``) need a ``COUNT(*)`` to resolve so they are rejected;
otherwise the sign of the bounds picks the forward or reverse variant.
Examples::
[2:8] → _forward_range(2, 8)
[-5:-2] → _reverse_range(-5, -2)
[3:-2] → raises QueryDefinitionError (mixed sign)
:param start: lower bound from the slice
:type start: int
:param stop: upper bound from the slice
:type stop: int
:return: normalized slice parameters
:rtype: SliceBounds
"""
start_neg = start < 0
stop_neg = stop < 0
if start_neg != stop_neg:
raise QueryDefinitionError(
"Mixed positive and negative slice bounds are not supported "
"without an explicit count; use .count() with "
".offset()/.limit() instead."
)
if start_neg:
return _reverse_range(start, stop)
return _forward_range(start, stop)
def _forward_range(start: int, stop: int) -> SliceBounds:
"""
Handles ``[A:B]`` with ``A, B >= 0``. Out-of-order bounds (``A > B``)
clamp to an empty result to match Python list semantics, rather than
emitting a nonsensical negative ``LIMIT``.
Examples::
[2:8] → SliceBounds(limit=6, offset=2, reverse=False)
[0:3] → SliceBounds(limit=3, offset=0, reverse=False)
[8:2] → SliceBounds(limit=0, offset=8, reverse=False) # empty
:param start: non-negative lower bound
:type start: int
:param stop: non-negative upper bound
:type stop: int
:return: ``SliceBounds(limit=max(stop-start, 0), offset=start,
reverse=False)``
:rtype: SliceBounds
"""
return SliceBounds(limit=max(stop - start, 0), offset=start, reverse=False)
def _reverse_range(start: int, stop: int) -> SliceBounds:
"""
Handles ``[-A:-B]`` (both bounds negative). Out-of-order bounds
(``start >= stop``, e.g. ``[-2:-5]``) clamp to empty; otherwise the
slice is emulated by flipping the ORDER BY, offsetting ``|stop|`` rows
from the end, and limiting to ``|start| - |stop|`` rows.
Examples::
[-5:-2] → SliceBounds(limit=3, offset=2, reverse=True)
[-3:-1] → SliceBounds(limit=2, offset=1, reverse=True)
[-2:-5] → SliceBounds(limit=0, offset=0, reverse=True) # empty
:param start: negative lower bound
:type start: int
:param stop: negative upper bound
:type stop: int
:return: ``SliceBounds(..., reverse=True)``
:rtype: SliceBounds
"""
if start >= stop:
return SliceBounds(limit=0, offset=0, reverse=True)
return SliceBounds(limit=stop - start, offset=-stop, reverse=True)
def translate_list_to_dict( # noqa: CCR001
list_to_trans: Union[list, set], default: Any = ...
) -> dict:
"""
Splits the list of strings by '__' and converts them to dictionary with nested
models grouped by parent model. That way each model appears only once in the whole
dictionary and children are grouped under parent name.
Default required key ise Ellipsis like in pydantic.
:param list_to_trans: input list
:type list_to_trans: Union[list, set]
:param default: value to use as a default value
:type default: Any
:param is_order: flag if change affects order_by clauses are they require special
default value with sort order.
:type is_order: bool
:return: converted to dictionary input list
:rtype: dict
"""
return ormar_rust_utils.translate_list_to_dict(list(list_to_trans), default)
def convert_set_to_required_dict(set_to_convert: set) -> dict:
"""
Converts set to dictionary of required keys.
Required key is Ellipsis.
:param set_to_convert: set to convert to dict
:type set_to_convert: set
:return: set converted to dict of ellipsis
:rtype: dict
"""
new_dict = dict()
for key in set_to_convert:
new_dict[key] = Ellipsis
return new_dict
def update(current_dict: Any, updating_dict: Any) -> dict: # noqa: CCR001
"""
Update one dict with another but with regard for nested keys.
That way nested sets are unionised, dicts updated and
only other values are overwritten.
:param current_dict: dict to update
:type current_dict: dict[str, ellipsis]
:param updating_dict: dict with values to update
:type updating_dict: dict
:return: combination of both dicts
:rtype: dict
"""
if current_dict is Ellipsis:
current_dict = dict()
for key, value in updating_dict.items():
if isinstance(value, collections.abc.Mapping):
old_key = current_dict.get(key, {})
if isinstance(old_key, set):
old_key = convert_set_to_required_dict(old_key)
current_dict[key] = update(old_key, value)
elif isinstance(value, set) and isinstance(current_dict.get(key), set):
current_dict[key] = current_dict.get(key).union(value)
else:
current_dict[key] = value
return current_dict
def subtract_dict(current_dict: Any, updating_dict: Any) -> dict: # noqa: CCR001
"""
Update one dict with another but with regard for nested keys.
That way nested sets are unionised, dicts updated and
only other values are overwritten.
:param current_dict: dict to update
:type current_dict: dict[str, ellipsis]
:param updating_dict: dict with values to update
:type updating_dict: dict
:return: combination of both dicts
:rtype: dict
"""
for key, value in updating_dict.items():
old_key = current_dict.get(key, {})
new_value: Optional[Union[dict, set]] = None
if not old_key:
continue
if isinstance(value, set) and isinstance(old_key, set):
new_value = old_key.difference(value)
elif isinstance(value, (set, collections.abc.Mapping)) and isinstance(
old_key, (set, collections.abc.Mapping)
):
value = (
convert_set_to_required_dict(value)
if not isinstance(value, collections.abc.Mapping)
else value
)
old_key = (
convert_set_to_required_dict(old_key)
if not isinstance(old_key, collections.abc.Mapping)
else old_key
)
new_value = subtract_dict(old_key, value)
if new_value:
current_dict[key] = new_value
else:
current_dict.pop(key, None)
return current_dict
def update_dict_from_list(curr_dict: dict, list_to_update: Union[list, set]) -> dict:
"""
Converts the list into dictionary and later performs special update, where
nested keys that are sets or dicts are combined and not overwritten.
:param curr_dict: dict to update
:type curr_dict: dict
:param list_to_update: list with values to update the dict
:type list_to_update: list[str]
:return: updated dict
:rtype: dict
"""
updated_dict = copy.copy(curr_dict)
dict_to_update = translate_list_to_dict(list_to_update)
update(updated_dict, dict_to_update)
return updated_dict
def get_relationship_alias_model_and_str(
source_model: type["Model"], related_parts: list
) -> tuple[str, type["Model"], str, bool]:
"""
Walks the relation to retrieve the actual model on which the clause should be
constructed, extracts alias based on last relation leading to target model.
:param related_parts: list of related names extracted from string
:type related_parts: Union[list, list[str]]
:param source_model: model from which relation starts
:type source_model: type[Model]
:return: table prefix, target model and relation string
:rtype: tuple[str, type["Model"], str]
"""
table_prefix = ""
is_through = False
target_model = source_model
previous_model = target_model
previous_models = [target_model]
manager = target_model.ormar_config.alias_manager
for relation in related_parts[:]:
related_field = target_model.ormar_config.model_fields[relation]
if related_field.is_through:
previous_model, relation, is_through = _process_through_field(
related_parts=related_parts,
relation=relation,
related_field=related_field,
previous_model=previous_model,
previous_models=previous_models,
)
if related_field.is_multi:
previous_model = related_field.through
relation = related_field.default_target_field_name() # type: ignore
table_prefix = manager.resolve_relation_alias(
from_model=previous_model, relation_name=relation
)
target_model = related_field.to
previous_model = target_model
if not is_through:
previous_models.append(previous_model)
relation_str = "__".join(related_parts)
return table_prefix, target_model, relation_str, is_through
def _process_through_field(
related_parts: list,
relation: Optional[str],
related_field: "BaseField",
previous_model: type["Model"],
previous_models: list[type["Model"]],
) -> tuple[type["Model"], Optional[str], bool]:
"""
Helper processing through models as they need to be treated differently.
:param related_parts: split relation string
:type related_parts: list[str]
:param relation: relation name
:type relation: str
:param related_field: field with relation declaration
:type related_field: "ForeignKeyField"
:param previous_model: model from which relation is coming
:type previous_model: type["Model"]
:param previous_models: list of already visited models in relation chain
:type previous_models: list[type["Model"]]
:return: previous_model, relation, is_through
:rtype: tuple[type["Model"], str, bool]
"""
is_through = True
related_parts.remove(relation)
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:
previous_model = through_field.to
relation = through_field.related_name
else:
relation = related_field.related_name
return previous_model, relation, is_through
ormar-orm-ormar-a7e7fde/ormar/relations/ 0000775 0000000 0000000 00000000000 15211567312 0020452 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/relations/__init__.py 0000664 0000000 0000000 00000000777 15211567312 0022576 0 ustar 00root root 0000000 0000000 """
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
from ormar.relations.utils import get_relations_sides_and_names
__all__ = [
"AliasManager",
"Relation",
"RelationsManager",
"RelationType",
"get_relations_sides_and_names",
]
ormar-orm-ormar-a7e7fde/ormar/relations/alias_manager.py 0000664 0000000 0000000 00000016410 15211567312 0023611 0 ustar 00root root 0000000 0000000 import string
import uuid
from random import choices
from typing import TYPE_CHECKING, Any, Optional, Union
import sqlalchemy
from sqlalchemy import Label
from sqlalchemy.sql.selectable import NamedFromClause
if TYPE_CHECKING: # pragma: no cover
from ormar import Model
from ormar.fields import ForeignKeyField
from ormar.models import ModelRow
def get_table_alias() -> str:
"""
Creates a random string that is used to alias tables in joins.
It's necessary that each relation has it's own aliases cause you can link
to the same target tables from multiple fields on one model as well as from
multiple different models in one join.
:return: randomly generated alias
:rtype: str
"""
alias = "".join(choices(string.ascii_uppercase, k=2)) + uuid.uuid4().hex[:4]
return alias.lower()
class AliasManager:
"""
Keep all aliases of relations between different tables.
One global instance is shared between all models.
"""
def __init__(self) -> None:
self._aliases_new: dict[str, str] = dict()
self._reversed_aliases: dict[str, str] = dict()
self._prefixed_tables: dict[str, NamedFromClause] = dict()
def __contains__(self, item: str) -> bool:
return self._aliases_new.__contains__(item)
def __getitem__(self, key: str) -> Any:
return self._aliases_new.__getitem__(key)
@property
def reversed_aliases(self) -> dict:
"""
Returns swapped key-value pairs from aliases where alias is the key.
:return: dictionary of prefix to relation
:rtype: dict
"""
if self._reversed_aliases:
return self._reversed_aliases
reversed_aliases = {v: k for k, v in self._aliases_new.items()}
self._reversed_aliases = reversed_aliases
return self._reversed_aliases
@staticmethod
def prefixed_columns(
alias: str, table: sqlalchemy.Table, fields: Optional[list] = None
) -> list[Label[Any]]:
"""
Creates a list of aliases sqlalchemy text clauses from
string alias and sqlalchemy.Table.
Optional list of fields to include can be passed to extract only those columns.
List has to have sqlalchemy names of columns (ormar aliases) not the ormar ones.
:param alias: alias of given table
:type alias: str
:param table: table from which fields should be aliased
:type table: sqlalchemy.Table
:param fields: fields to include
:type fields: Optional[list[str]]
:return: list of sqlalchemy text clauses with "column name as aliased name"
:rtype: list[text]
"""
alias = f"{alias}_" if alias else ""
aliased_fields = [f"{alias}{x}" for x in fields] if fields else []
all_columns = (
table.columns
if not fields
else [
col
for col in table.columns
if col.name in fields or col.name in aliased_fields
]
)
return [column.label(f"{alias}{column.name}") for column in all_columns]
def prefixed_table_name(
self, alias: str, table: sqlalchemy.Table
) -> NamedFromClause:
"""
Creates text clause with table name with aliased name.
:param alias: alias of given table
:type alias: str
:param table: table
:type table: sqlalchemy.Table
:return: sqlalchemy text clause as "table_name aliased_name"
:rtype: sqlalchemy text clause
"""
full_alias = f"{alias}_{table.name}"
key = f"{full_alias}_{id(table)}"
return self._prefixed_tables.setdefault(key, table.alias(full_alias))
def add_relation_type(
self,
source_model: type["Model"],
relation_name: str,
reverse_name: Optional[str] = None,
) -> None:
"""
Registers the relations defined in ormar models.
Given the relation it registers also the reverse side of this relation.
Used by both ForeignKey and ManyToMany relations.
Each relation is registered as Model name and relation name.
Each alias registered has to be unique.
Aliases are used to construct joins to assure proper links between tables.
That way you can link to the same target tables from multiple fields
on one model as well as from multiple different models in one join.
:param source_model: model with relation defined
:type source_model: source Model
:param relation_name: name of the relation to define
:type relation_name: str
:param reverse_name: name of related_name fo given relation for m2m relations
:type reverse_name: Optional[str]
:return: none
:rtype: None
"""
parent_key = f"{source_model.get_name()}_{relation_name}"
if parent_key not in self._aliases_new:
self.add_alias(parent_key)
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:
self.add_alias(child_key)
def add_alias(self, alias_key: str) -> str:
"""
Adds alias to the dictionary of aliases under given key.
:param alias_key: key of relation to generate alias for
:type alias_key: str
:return: generated alias
:rtype: str
"""
alias = get_table_alias()
self._aliases_new[alias_key] = alias
return alias
def resolve_relation_alias(
self, from_model: Union[type["Model"], type["ModelRow"]], relation_name: str
) -> str:
"""
Given model and relation name returns the alias for this relation.
:param from_model: model with relation defined
:type from_model: source Model
:param relation_name: name of the relation field
:type relation_name: str
:return: alias of the relation
:rtype: str
"""
alias = self._aliases_new.get(f"{from_model.get_name()}_{relation_name}", "")
return alias
def resolve_relation_alias_after_complex(
self,
source_model: Union[type["Model"], type["ModelRow"]],
relation_str: str,
relation_field: "ForeignKeyField",
) -> str:
"""
Given source model and relation string returns the alias for this complex
relation if it exists, otherwise fallback to normal relation from a relation
field definition.
:param relation_field: field with direct relation definition
:type relation_field: "ForeignKeyField"
:param source_model: model with query starts
:type source_model: source Model
:param relation_str: string with relation joins defined
:type relation_str: str
:return: alias of the relation
:rtype: str
"""
alias = ""
if relation_str and "__" in relation_str:
alias = self.resolve_relation_alias(
from_model=source_model, relation_name=relation_str
)
if not alias:
alias = self.resolve_relation_alias(
from_model=relation_field.get_source_model(),
relation_name=relation_field.get_relation_name(),
)
return alias
ormar-orm-ormar-a7e7fde/ormar/relations/querysetproxy.py 0000664 0000000 0000000 00000111573 15211567312 0024017 0 ustar 00root root 0000000 0000000 from _weakref import CallableProxyType
from typing import ( # noqa: I100, I201
TYPE_CHECKING,
Any,
AsyncGenerator,
Generic,
MutableSequence,
Optional,
Sequence,
TypeVar,
Union,
cast,
)
import sqlalchemy.exc
import ormar # noqa: I100, I202
from ormar.exceptions import ModelPersistenceError, NoMatch, QueryDefinitionError
if TYPE_CHECKING: # pragma no cover
from ormar import OrderAction, RelationType
from ormar.models import Model, T
from ormar.queryset import QuerySet
from ormar.relations import Relation
else:
T = TypeVar("T", bound="Model")
class QuerysetProxy(Generic[T]):
"""
Exposes QuerySet methods on relations, but also handles creating and removing
of through Models for m2m relations.
"""
if TYPE_CHECKING: # pragma no cover
relation: "Relation"
def __init__(
self,
relation: "Relation",
to: type["T"],
type_: "RelationType",
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.ormar_config.model_fields[
self.relation.field_name
].get_related_name()
self.to: type[T] = to
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()
if self.type_ == ormar.RelationType.MULTIPLE
else ""
)
@property
def queryset(self) -> "QuerySet[T]":
"""
Returns queryset if it's set, AttributeError otherwise.
:return: QuerySet
:rtype: QuerySet
"""
if not self._queryset:
raise AttributeError # pragma: no cover
return self._queryset
@queryset.setter
def queryset(self, value: "QuerySet") -> None:
"""
set's the queryset. Initialized in RelationProxy.
:param value: QuerySet
:type value: QuerySet
"""
self._queryset = value
def _assign_child_to_parent(self, child: Optional["T"]) -> None:
"""
Registers child in parents RelationManager.
:param child: child to register on parent side.
:type child: Model
"""
if child:
owner = self._owner
rel_name = self.relation.field_name
setattr(owner, rel_name, child)
def _register_related(self, child: Union["T", Sequence[Optional["T"]]]) -> None:
"""
Registers child/ children in parents RelationManager.
:param child: child or list of children models to register.
:type child: Union[Model,list[Model]]
"""
if isinstance(child, list):
for subchild in child:
self._assign_child_to_parent(subchild)
else:
assert isinstance(child, ormar.Model)
child = cast("T", child)
self._assign_child_to_parent(child)
def _clean_items_on_load(self) -> None:
"""
Cleans the current list of the related models.
"""
if isinstance(self.relation.related_models, MutableSequence):
for item in self.relation.related_models[:]:
self.relation.remove(item)
async def create_through_instance(self, child: "T", **kwargs: Any) -> None:
"""
Crete a through model instance in the database for m2m relations.
:param kwargs: dict of additional keyword arguments for through instance
:type kwargs: Any
:param child: child model instance
:type child: Model
"""
model_cls = self.relation.through
owner_column = self.related_field.default_target_field_name() # type: ignore
child_column = self.related_field.default_source_field_name() # type: ignore
rel_kwargs = {owner_column: self._owner.pk, child_column: child.pk}
final_kwargs = {**rel_kwargs, **kwargs}
if child.pk is None:
raise ModelPersistenceError(
f"You cannot save {child.get_name()} "
f"model without primary key set! \n"
f"Save the child model first."
)
await model_cls(**final_kwargs).save()
async def update_through_instance(self, child: "T", **kwargs: Any) -> None:
"""
Updates a through model instance in the database for m2m relations.
:param kwargs: dict of additional keyword arguments for through instance
:type kwargs: Any
:param child: child model instance
:type child: Model
"""
model_cls = self.relation.through
owner_column = self.related_field.default_target_field_name() # type: ignore
child_column = self.related_field.default_source_field_name() # type: ignore
rel_kwargs = {owner_column: self._owner.pk, child_column: child.pk}
through_model = await model_cls.objects.get(**rel_kwargs)
await through_model.update(**kwargs)
async def upsert_through_instance(self, child: "T", **kwargs: Any) -> None:
"""
Updates a through model instance in the database for m2m relations if
it already exists, else creates one.
:param kwargs: dict of additional keyword arguments for through instance
:type kwargs: Any
:param child: child model instance
:type child: Model
"""
try:
await self.update_through_instance(child=child, **kwargs)
except NoMatch:
await self.create_through_instance(child=child, **kwargs)
async def delete_through_instance(self, child: "T") -> None:
"""
Removes through model instance from the database for m2m relations.
:param child: child model instance
:type child: Model
"""
queryset = ormar.QuerySet(model_cls=self.relation.through) # type: ignore
owner_column = self.related_field.default_target_field_name() # type: ignore
child_column = self.related_field.default_source_field_name() # type: ignore
kwargs = {owner_column: self._owner, child_column: child}
link_instance = await queryset.filter(**kwargs).get() # type: ignore
await link_instance.delete()
async def exists(self) -> bool:
"""
Returns a bool value to confirm if there are rows matching the given criteria
(applied with `filter` and `exclude` if set).
Actual call delegated to QuerySet.
:return: result of the check
:rtype: bool
"""
return await self.queryset.exists()
async def count(self, distinct: bool = True) -> int:
"""
Returns number of rows matching the given criteria
(applied with `filter` and `exclude` if set before).
If `distinct` is `True` (the default), this will return
the number of primary rows selected. If `False`,
the count will be the total number of rows returned
(including extra rows for `one-to-many` or `many-to-many`
left `select_related` table joins).
`False` is the legacy (buggy) behavior for workflows that depend on it.
Actual call delegated to QuerySet.
:param distinct: flag if the primary table rows should be distinct or not
:return: number of rows
:rtype: int
"""
return await self.queryset.count(distinct=distinct)
async def max(self, columns: Union[str, list[str]]) -> Any: # noqa: A003
"""
Returns max value of columns for rows matching the given criteria
(applied with `filter` and `exclude` if set before).
:return: max value of column(s)
:rtype: Any
"""
return await self.queryset.max(columns=columns)
async def min(self, columns: Union[str, list[str]]) -> Any: # noqa: A003
"""
Returns min value of columns for rows matching the given criteria
(applied with `filter` and `exclude` if set before).
:return: min value of column(s)
:rtype: Any
"""
return await self.queryset.min(columns=columns)
async def sum(self, columns: Union[str, list[str]]) -> Any: # noqa: A003
"""
Returns sum value of columns for rows matching the given criteria
(applied with `filter` and `exclude` if set before).
:return: sum value of columns
:rtype: int
"""
return await self.queryset.sum(columns=columns)
async def avg(self, columns: Union[str, list[str]]) -> Any:
"""
Returns avg value of columns for rows matching the given criteria
(applied with `filter` and `exclude` if set before).
:return: avg value of columns
:rtype: Union[int, float, list]
"""
return await self.queryset.avg(columns=columns)
async def clear(self, keep_reversed: bool = True) -> int:
"""
Removes all related models from given relation.
Removes all through models for m2m relation.
For reverse FK relations keep_reversed flag marks if the reversed models
should be kept or deleted from the database too (False means that models
will be deleted, and not only removed from relation).
:param keep_reversed: flag if reverse models in reverse FK should be deleted
or not, keep_reversed=False deletes them from database.
:type keep_reversed: bool
:return: number of deleted models
:rtype: int
"""
if self.type_ == ormar.RelationType.MULTIPLE:
queryset = ormar.QuerySet(model_cls=self.relation.through) # type: ignore
owner_column = self._owner.get_name()
else:
queryset = ormar.QuerySet(model_cls=self.relation.to) # type: ignore
owner_column = self.related_field_name
kwargs = {owner_column: self._owner}
self._clean_items_on_load()
if keep_reversed and self.type_ == ormar.RelationType.REVERSE:
update_kwrgs = {f"{owner_column}": None}
return await queryset.filter(_exclude=False, **kwargs).update(
each=False, **update_kwrgs
)
return await queryset.delete(**kwargs) # type: ignore
async def values(
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
passed or all fields from queried models.
To filter for given row use filter/exclude methods before values,
to limit number of rows use limit/offset or paginate before values.
Note that it always return a list even for one row from database.
:param exclude_through: flag if through models should be excluded
:type exclude_through: bool
:param fields: field name or list of field names to extract from db
:type fields: Union[list, str, set, dict]
"""
return await self.queryset.values(
fields=fields, exclude_through=exclude_through
)
async def values_list(
self,
fields: Union[list, str, set, dict, None] = None,
flatten: bool = False,
exclude_through: bool = False,
) -> list:
"""
Return a list of tuples with column values in order of the fields passed or
all fields from queried models.
When one field is passed you can flatten the list of tuples into list of values
of that single field.
To filter for given row use filter/exclude methods before values,
to limit number of rows use limit/offset or paginate before values.
Note that it always return a list even for one row from database.
:param exclude_through: flag if through models should be excluded
:type exclude_through: bool
:param fields: field name or list of field names to extract from db
:type fields: Union[str, list[str]]
:param flatten: when one field is passed you can flatten the list of tuples
:type flatten: bool
"""
return await self.queryset.values(
fields=fields,
exclude_through=exclude_through,
_as_dict=False,
_flatten=flatten,
)
async def first(self, *args: Any, **kwargs: Any) -> "T":
"""
Gets the first row from the db ordered by primary key column ascending.
Actual call delegated to :meth:`QuerySet.first`, which always issues
``LIMIT 1`` and therefore never raises ``MultipleMatches``. Use
:meth:`get` instead if you want to assert a single match.
Passing args and/or kwargs is a shortcut and equals to calling
``filter(*args, **kwargs).first()``.
List of related models is cleared before the call.
:raises NoMatch: if no rows match the criteria
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model
:rtype: Model
"""
first = await self.queryset.first(*args, **kwargs)
self._clean_items_on_load()
self._register_related(first)
return first
async def first_or_none(self, *args: Any, **kwargs: Any) -> Optional["T"]:
"""
Gets the first row from the db ordered by primary key column ascending.
Behaves like :meth:`first` but returns ``None`` instead of raising
``NoMatch`` when no row matches.
Passing args and/or kwargs is a shortcut and equals to calling
``filter(*args, **kwargs).first_or_none()``.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model or None
:rtype: Optional[Model]
"""
first = await self.queryset.first_or_none(*args, **kwargs)
self._clean_items_on_load()
if first is not None:
self._register_related(first)
return first
async def last(self, *args: Any, **kwargs: Any) -> "T":
"""
Gets the last row from the db ordered by primary key column descending.
Actual call delegated to :meth:`QuerySet.last`, which always issues
``LIMIT 1`` and therefore never raises ``MultipleMatches``. Use
:meth:`get` instead if you want to assert a single match.
Passing args and/or kwargs is a shortcut and equals to calling
``filter(*args, **kwargs).last()``.
List of related models is cleared before the call.
:raises NoMatch: if no rows match the criteria
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model
:rtype: Model
"""
last = await self.queryset.last(*args, **kwargs)
self._clean_items_on_load()
self._register_related(last)
return last
async def last_or_none(self, *args: Any, **kwargs: Any) -> Optional["T"]:
"""
Gets the last row from the db ordered by primary key column descending.
Behaves like :meth:`last` but returns ``None`` instead of raising
``NoMatch`` when no row matches.
Passing args and/or kwargs is a shortcut and equals to calling
``filter(*args, **kwargs).last_or_none()``.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model or None
:rtype: Optional[Model]
"""
last = await self.queryset.last_or_none(*args, **kwargs)
self._clean_items_on_load()
if last is not None:
self._register_related(last)
return last
async def get_or_none(self, *args: Any, **kwargs: Any) -> Optional["T"]:
"""
Gets a single row from the db matching the criteria set by args/kwargs.
Behaves like :meth:`get` but returns ``None`` instead of raising
``NoMatch`` when no row matches. ``MultipleMatches`` is still raised
when criteria are provided and more than one row matches.
Passing args and/or kwargs is a shortcut and equals to calling
``filter(*args, **kwargs).get_or_none()``.
:raises MultipleMatches: when criteria are set (via ``filter``/``exclude``
or args/kwargs) and more than one row matches them.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model or None
:rtype: Optional[Model]
"""
try:
get = await self.queryset.get(*args, **kwargs)
except ormar.NoMatch:
return None
self._clean_items_on_load()
self._register_related(get)
return get
async def get(self, *args: Any, **kwargs: Any) -> "T":
"""
Gets a single row from the db matching the criteria set by args/kwargs.
Passing args and/or kwargs is a shortcut and equals to calling
``filter(*args, **kwargs).get()``. Actual call is delegated to
:meth:`QuerySet.get`; see its docstring for the full contract.
When criteria are set (here or via chained :meth:`filter`/:meth:`exclude`)
the query asserts that exactly one row matches: ``NoMatch`` is raised
when none match and ``MultipleMatches`` when more than one matches.
When no criteria are set, ``get()`` returns the last row of the table
ordered by primary key descending.
List of related models is cleared before the call.
:raises NoMatch: if no rows match the criteria
:raises MultipleMatches: when criteria are set and more than one row
matches them.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: returned model
:rtype: Model
"""
get = await self.queryset.get(*args, **kwargs)
self._clean_items_on_load()
self._register_related(get)
return get
async def all(self, *args: Any, **kwargs: Any) -> list["T"]: # noqa: A003
"""
Returns all rows from a database for given model for set filter options.
Passing args and/or kwargs is a shortcut and equals to calling
`filter(*args, **kwargs).all()`.
If there are no rows meeting the criteria an empty list is returned.
Actual call delegated to QuerySet.
List of related models is cleared before the call.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: list of returned models
:rtype: list[Model]
"""
all_items = await self.queryset.all(*args, **kwargs)
self._clean_items_on_load()
self._register_related(all_items)
return all_items
async def iterate( # noqa: A003
self,
*args: Any,
**kwargs: Any,
) -> AsyncGenerator["T", None]:
"""
Return async iterable generator for all rows from a database for given model.
Passing args and/or kwargs is a shortcut and equals to calling
`filter(*args, **kwargs).iterate()`.
If there are no rows meeting the criteria an empty async generator is returned.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: asynchronous iterable generator of returned models
:rtype: AsyncGenerator[Model]
"""
async for item in self.queryset.iterate(*args, **kwargs):
yield item
async def create(self, **kwargs: Any) -> "T":
"""
Creates the model instance, saves it in a database and returns the updates model
(with pk populated if not passed and autoincrement is set).
The allowed kwargs are `Model` fields names and proper value types.
For m2m relation the through model is created automatically.
Actual call delegated to QuerySet.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: created model
:rtype: Model
"""
through_kwargs = kwargs.pop(self.through_model_name, {})
if self.type_ == ormar.RelationType.REVERSE:
kwargs[self.related_field_name] = self._owner
created = await self.queryset.create(**kwargs)
self._register_related(created)
if self.type_ == ormar.RelationType.MULTIPLE:
await self.create_through_instance(created, **through_kwargs)
return created
async def update(self, each: bool = False, **kwargs: Any) -> int:
"""
Updates the model table after applying the filters from kwargs.
You have to either pass a filter to narrow down a query or explicitly pass
each=True flag to affect whole table.
:param each: flag if whole table should be affected if no filter is passed
:type each: bool
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: number of updated rows
:rtype: int
"""
# queryset proxy always have one filter for pk of parent model
if (
not each
and (len(self.queryset.filter_clauses) + len(self.queryset.exclude_clauses))
== 1
):
raise QueryDefinitionError(
"You cannot update without filtering the queryset first. "
"If you want to update all rows use update(each=True, **kwargs)"
)
through_kwargs = kwargs.pop(self.through_model_name, {})
children = await self.queryset.all()
for child in children:
await child.update(**kwargs) # type: ignore
if self.type_ == ormar.RelationType.MULTIPLE and through_kwargs:
await self.update_through_instance(
child=child,
**through_kwargs, # type: ignore
)
return len(children)
async def get_or_create(
self,
_defaults: Optional[dict[str, Any]] = None,
*args: Any,
**kwargs: Any,
) -> tuple["T", bool]:
"""
Combination of create and get methods.
Tries to get a row meeting the criteria for kwargs
and if `NoMatch` exception is raised
it creates a new one with given kwargs and _defaults.
If a concurrent caller wins the race and creates a matching row
between this call's ``get`` and ``create``, the ``create`` raises
``sqlalchemy.exc.IntegrityError`` and ``get`` is retried once.
If the retry still finds no row, the original ``IntegrityError`` is
re-raised because the violation isn't a race but a legitimate
constraint conflict (e.g. a different unique key).
:param kwargs: fields names and proper value types
:type kwargs: Any
:param _defaults: default values for creating object
:type _defaults: Optional[dict[str, Any]]
:return: model instance and a boolean
:rtype: tuple("T", bool)
"""
try:
return await self.get(*args, **kwargs), False
except NoMatch:
pass
_defaults = _defaults or {}
try:
return await self.create(**{**kwargs, **_defaults}), True
except sqlalchemy.exc.IntegrityError as integrity_error:
try:
return await self.get(*args, **kwargs), False
except NoMatch:
raise integrity_error from None
async def update_or_create(self, **kwargs: Any) -> "T":
"""
Updates the model, or in case there is no match in database creates a new one.
If a pk is supplied but no row exists with that pk, a new row is
created with the supplied pk and field values rather than raising
``NoMatch``.
Actual call delegated to QuerySet.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: updated or created model
:rtype: Model
"""
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:
return await self.create(**kwargs)
try:
model = await self.queryset.get(pk=kwargs[pk_name])
except NoMatch:
return await self.create(**kwargs)
return await model.update(**kwargs)
def filter( # noqa: A003, A001
self, *args: Any, **kwargs: Any
) -> "QuerysetProxy[T]":
"""
Allows you to filter by any `Model` attribute/field
as well as to fetch instances, with a filter across an FK relationship.
You can use special filter suffix to change the filter operands:
* exact - like `album__name__exact='Malibu'` (exact match)
* iexact - like `album__name__iexact='malibu'` (exact match case insensitive)
* contains - like `album__name__contains='Mal'` (sql like)
* icontains - like `album__name__icontains='mal'` (sql like case insensitive)
* in - like `album__name__in=['Malibu', 'Barclay']` (sql in)
* isnull - like `album__name__isnull=True` (sql is null)
(isnotnull `album__name__isnull=False` (sql is not null))
* gt - like `position__gt=3` (sql >)
* gte - like `position__gte=3` (sql >=)
* lt - like `position__lt=3` (sql <)
* lte - like `position__lte=3` (sql <=)
* startswith - like `album__name__startswith='Mal'` (exact start match)
* istartswith - like `album__name__istartswith='mal'` (case insensitive)
* endswith - like `album__name__endswith='ibu'` (exact end match)
* iendswith - like `album__name__iendswith='IBU'` (case insensitive)
Actual call delegated to QuerySet.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: filtered QuerysetProxy
:rtype: QuerysetProxy
"""
queryset = self.queryset.filter(*args, **kwargs)
return self.__class__(
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
def exclude(self, *args: Any, **kwargs: Any) -> "QuerysetProxy[T]": # noqa: A003, A001
"""
Works exactly the same as filter and all modifiers (suffixes) are the same,
but returns a *not* condition.
So if you use `filter(name='John')` which is `where name = 'John'` in SQL,
the `exclude(name='John')` equals to `where name <> 'John'`
Note that all conditions are joined so if you pass multiple values it
becomes a union of conditions.
`exclude(name='John', age>=35)` will become
`where not (name='John' and age>=35)`
Actual call delegated to QuerySet.
:param kwargs: fields names and proper value types
:type kwargs: Any
:return: filtered QuerysetProxy
:rtype: QuerysetProxy
"""
queryset = self.queryset.exclude(*args, **kwargs)
return self.__class__(
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
def select_all(self, follow: bool = False) -> "QuerysetProxy[T]":
"""
By default adds only directly related models.
If follow=True is set it adds also related models of related models.
To not get stuck in an infinite loop as related models also keep a relation
to parent model visited models set is kept.
That way already visited models that are nested are loaded, but the load do not
follow them inside. So Model A -> Model B -> Model C -> Model A -> Model X
will load second Model A but will never follow into Model X.
Nested relations of those kind need to be loaded manually.
:param follow: flag to trigger deep save -
by default only directly related models are saved
with follow=True also related models of related models are saved
:type follow: bool
:return: reloaded Model
:rtype: Model
"""
queryset = self.queryset.select_all(follow=follow)
return self.__class__(
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
def select_related(self, related: Union[list, str]) -> "QuerysetProxy[T]":
"""
Allows to prefetch related models during the same query.
**With `select_related` always only one query is run against the database**,
meaning that one (sometimes complicated) join is generated and later nested
models are processed in python.
To fetch related model use `ForeignKey` names.
To chain related `Models` relation use double underscores between names.
Actual call delegated to QuerySet.
:param related: list of relation field names, can be linked by '__' to nest
:type related: Union[list, str]
:return: QuerysetProxy
:rtype: QuerysetProxy
"""
queryset = self.queryset.select_related(related)
return self.__class__(
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
def prefetch_related(self, related: Union[list, str]) -> "QuerysetProxy[T]":
"""
Allows to prefetch related models during query - but opposite to
`select_related` each subsequent model is fetched in a separate database query.
**With `prefetch_related` always one query per Model is run against the
database**, meaning that you will have multiple queries executed one
after another.
To fetch related model use `ForeignKey` names.
To chain related `Models` relation use double underscores between names.
Actual call delegated to QuerySet.
:param related: list of relation field names, can be linked by '__' to nest
:type related: Union[list, str]
:return: QuerysetProxy
:rtype: QuerysetProxy
"""
queryset = self.queryset.prefetch_related(related)
return self.__class__(
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
def paginate(self, page: int, page_size: int = 20) -> "QuerysetProxy[T]":
"""
You can paginate the result which is a combination of offset and limit clauses.
Limit is set to page size and offset is set to (page-1) * page_size.
Actual call delegated to QuerySet.
:param page_size: numbers of items per page
:type page_size: int
:param page: page number
:type page: int
:return: QuerySet
:rtype: QuerySet
"""
queryset = self.queryset.paginate(page=page, page_size=page_size)
return self.__class__(
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
def limit(self, limit_count: int) -> "QuerysetProxy[T]":
"""
You can limit the results to desired number of parent models.
Actual call delegated to QuerySet.
:param limit_count: number of models to limit
:type limit_count: int
:return: QuerysetProxy
:rtype: QuerysetProxy
"""
queryset = self.queryset.limit(limit_count)
return self.__class__(
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
def offset(self, offset: int) -> "QuerysetProxy[T]":
"""
You can also offset the results by desired number of main models.
Actual call delegated to QuerySet.
:param offset: numbers of models to offset
:type offset: int
:return: QuerysetProxy
:rtype: QuerysetProxy
"""
queryset = self.queryset.offset(offset)
return self.__class__(
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
def fields(self, columns: Union[list, str, set, dict]) -> "QuerysetProxy[T]":
"""
With `fields()` you can select subset of model columns to limit the data load.
Note that `fields()` and `exclude_fields()` works both for main models
(on normal queries like `get`, `all` etc.)
as well as `select_related` and `prefetch_related`
models (with nested notation).
You can select specified fields by passing a `str, list[str], set[str] or
dict` with nested definition.
To include related models use notation
`{related_name}__{column}[__{optional_next} etc.]`.
`fields()` can be called several times, building up the columns to select.
If you include related models into `select_related()` call but you won't specify
columns for those models in fields - implies a list of all fields for
those nested models.
Mandatory fields cannot be excluded as it will raise `ValidationError`,
to exclude a field it has to be nullable.
Pk column cannot be excluded - it's always auto added even if
not explicitly included.
You can also pass fields to include as dictionary or set.
To mark a field as included in a dictionary use it's name as key
and ellipsis as value.
To traverse nested models use nested dictionaries.
To include fields at last level instead of nested dictionary a set can be used.
To include whole nested model specify model related field name and ellipsis.
Actual call delegated to QuerySet.
:param columns: columns to include
:type columns: Union[list, str, set, dict]
:return: QuerysetProxy
:rtype: QuerysetProxy
"""
queryset = self.queryset.fields(columns)
return self.__class__(
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
def exclude_fields(
self, columns: Union[list, str, set, dict]
) -> "QuerysetProxy[T]":
"""
With `exclude_fields()` you can select subset of model columns that will
be excluded to limit the data load.
It's the opposite of `fields()` method so check documentation above
to see what options are available.
Especially check above how you can pass also nested dictionaries
and sets as a mask to exclude fields from whole hierarchy.
Note that `fields()` and `exclude_fields()` works both for main models
(on normal queries like `get`, `all` etc.)
as well as `select_related` and `prefetch_related` models
(with nested notation).
Mandatory fields cannot be excluded as it will raise `ValidationError`,
to exclude a field it has to be nullable.
Pk column cannot be excluded - it's always auto added even
if explicitly excluded.
Actual call delegated to QuerySet.
:param columns: columns to exclude
:type columns: Union[list, str, set, dict]
:return: QuerysetProxy
:rtype: QuerysetProxy
"""
queryset = self.queryset.exclude_fields(columns=columns)
return self.__class__(
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
def order_by(self, columns: Union[list, str, "OrderAction"]) -> "QuerysetProxy[T]":
"""
With `order_by()` you can order the results from database based on your
choice of fields.
You can provide a string with field name or list of strings with fields names.
Ordering in sql will be applied in order of names you provide in order_by.
By default if you do not provide ordering `ormar` explicitly orders by
all primary keys
If you are sorting by nested models that causes that the result rows are
unsorted by the main model `ormar` will combine those children rows into
one main model.
The main model will never duplicate in the result
To order by main model field just provide a field name
To sort on nested models separate field names with dunder '__'.
You can sort this way across all relation types -> `ForeignKey`,
reverse virtual FK and `ManyToMany` fields.
To sort in descending order provide a hyphen in front of the field name
Actual call delegated to QuerySet.
:param columns: columns by which models should be sorted
:type columns: Union[list, str]
:return: QuerysetProxy
:rtype: QuerysetProxy
"""
queryset = self.queryset.order_by(columns)
return self.__class__(
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
def __getitem__(self, key: Union[int, slice]) -> "QuerysetProxy[T]":
"""
Slices the underlying QuerySet using Python integer or slice syntax.
Negative indices and slices are supported the same way as on
``QuerySet.__getitem__`` — they are translated into a reversed-order
query plus an in-memory list reversal on ``.all()``.
Actual call delegated to QuerySet.
:param key: integer index or slice
:type key: int | slice
:return: QuerysetProxy with updated pagination
:rtype: QuerysetProxy
"""
queryset = self.queryset[key]
return self.__class__(
relation=self.relation, type_=self.type_, to=self.to, qryset=queryset
)
ormar-orm-ormar-a7e7fde/ormar/relations/relation.py 0000664 0000000 0000000 00000020173 15211567312 0022644 0 ustar 00root root 0000000 0000000 from enum import Enum
from typing import TYPE_CHECKING, Generic, Optional, TypeVar, Union, cast
import ormar # noqa I100
from ormar.exceptions import RelationshipInstanceError # noqa I100
from ormar.relations.relation_proxy import RelationProxy
if TYPE_CHECKING: # pragma no cover
from ormar.models import Model, NewBaseModel, T
from ormar.relations import RelationsManager
else:
T = TypeVar("T", bound="Model")
class RelationType(Enum):
"""
Different types of relations supported by ormar:
* ForeignKey = PRIMARY
* reverse ForeignKey = REVERSE
* ManyToMany = MULTIPLE
"""
PRIMARY = 1
REVERSE = 2
MULTIPLE = 3
THROUGH = 4
class Relation(Generic[T]):
"""
Keeps related Models and handles adding/removing of the children.
"""
def __init__(
self,
manager: "RelationsManager",
type_: RelationType,
field_name: str,
to: type["T"],
through: Optional[type["Model"]] = None,
) -> None:
"""
Initialize the Relation and keep the related models either as instances of
passed Model, or as a RelationProxy which is basically a list of models with
some special behavior, as it exposes QuerySetProxy and allows querying the
related models already pre filtered by parent model.
:param manager: reference to relation manager
:type manager: RelationsManager
:param type_: type of the relation
:type type_: RelationType
:param field_name: name of the relation field
:type field_name: str
:param to: model to which relation leads to
:type to: type[Model]
:param through: model through which relation goes for m2m relations
:type through: type[Model]
"""
self.manager = manager
self._owner: "Model" = manager.owner
self._type: RelationType = type_
self._to_remove: set = set()
self.to: type["T"] = to
self._through = through
self.field_name: str = field_name
# ``RelationProxy`` is built lazily on the first reverse/m2m use
# (``add`` / ``get`` / ``_clean_related``) to avoid the per-model
# allocation when the relation is never read.
self.related_models: Optional[Union[RelationProxy, "Model"]] = None
def _ensure_proxy(self) -> RelationProxy:
"""
Materialize and cache the ``RelationProxy`` for reverse/m2m relations
on first use. Safe to call multiple times — subsequent calls return
the cached proxy.
:return: the relation's ``RelationProxy``
:rtype: RelationProxy
"""
proxy = self.related_models
if not isinstance(proxy, RelationProxy):
proxy = RelationProxy(
relation=self,
type_=self._type,
to=self.to,
field_name=self.field_name,
)
self.related_models = proxy
return proxy
def clear(self) -> None:
if self._type in (RelationType.PRIMARY, RelationType.THROUGH):
self.related_models = None
self._owner.__dict__[self.field_name] = None
elif self.related_models is not None:
related_models = cast("RelationProxy", self.related_models)
related_models._clear()
self._owner.__dict__[self.field_name] = None
@property
def through(self) -> type["Model"]:
if not self._through: # pragma: no cover
raise RelationshipInstanceError("Relation does not have through model!")
return self._through
def _clean_related(self) -> None:
"""
Removes dead weakrefs from RelationProxy.
"""
cleaned_data = [
x
for i, x in enumerate(self.related_models) # type: ignore
if i not in self._to_remove
]
proxy = RelationProxy(
relation=self,
type_=self._type,
to=self.to,
field_name=self.field_name,
data_=cleaned_data,
)
self.related_models = proxy
self._owner.__dict__[self.field_name] = proxy
self._to_remove = set()
def _find_existing(
self, child: Union["NewBaseModel", type["NewBaseModel"]]
) -> Optional[int]:
"""
Find child model in RelationProxy if exists.
:param child: child model to find
:type child: Model
:return: index of child in RelationProxy
:rtype: Optional[ind]
"""
if not isinstance(self.related_models, RelationProxy): # pragma nocover
raise ValueError("Cannot find existing models in parent relation type")
if child not in self.related_models:
return None
else:
# We need to clear the weakrefs that don't point to anything anymore
# There's an assumption here that if some of the related models
# went out of scope, then they all did, so we can just check the first one
try:
self.related_models[0].__repr__.__self__
return self.related_models.index(child)
except ReferenceError:
missing = self.related_models._get_list_of_missing_weakrefs()
self._to_remove.update(missing)
return self.related_models.index(child)
def add(self, child: "Model") -> None:
"""
Adds child Model to relation, either sets child as related model or adds
it to the list in RelationProxy depending on relation type.
For reverse / many-to-many relations the ``RelationProxy`` itself is
stored under ``_owner.__dict__[relation_name]``, so a single O(1)
membership check on the proxy's hash cache covers both the relation
bookkeeping and the pydantic-visible ``__dict__`` slot — no parallel
list, no second linear scan.
:param child: model to add to relation
:type child: Model
"""
relation_name = self.field_name
if self._type in (RelationType.PRIMARY, RelationType.THROUGH):
self.related_models = child
self._owner.__dict__[relation_name] = child
return
proxy = self._ensure_proxy()
# ``_find_existing`` is the membership check *plus* a dead-weakref
# probe — when a hash collision lands on a stale entry we need that
# probe to populate ``_to_remove`` so the next ``get()`` triggers
# ``_clean_related``.
if self._find_existing(child) is None:
proxy.append(child)
self._owner.__dict__[relation_name] = proxy
def remove(self, child: Union["NewBaseModel", type["NewBaseModel"]]) -> None:
"""
Removes child Model from relation, either sets None as related model or removes
it from the list in RelationProxy depending on relation type.
:param child: model to remove from relation
:type child: Model
"""
relation_name = self.field_name
if self._type == RelationType.PRIMARY:
if self.related_models == child:
self.related_models = None
del self._owner.__dict__[relation_name]
else:
position = self._find_existing(child)
if position is not None:
self.related_models.pop(position) # type: ignore
def get(self) -> Optional[Union[list["Model"], "Model"]]:
"""
Return the related model or models from RelationProxy.
For reverse / many-to-many relations the ``RelationProxy`` is
materialized on first read so callers always see a list-like
return value (even when no children have been registered yet).
:return: related model/models if set
:rtype: Optional[Union[list[Model], Model]]
"""
if self._to_remove:
self._clean_related()
if self.related_models is None and self._type in (
RelationType.REVERSE,
RelationType.MULTIPLE,
):
return self._ensure_proxy()
return self.related_models
def __repr__(self) -> str: # pragma no cover
if self._to_remove:
self._clean_related()
return str(self.related_models)
ormar-orm-ormar-a7e7fde/ormar/relations/relation_manager.py 0000664 0000000 0000000 00000014655 15211567312 0024346 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Optional, Sequence, 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.fields import BaseField, ForeignKeyField
from ormar.models import Model, NewBaseModel
class RelationsManager:
"""
Manages relations on a Model, each Model has it's own instance.
"""
def __init__(
self,
related_fields: Optional[list["ForeignKeyField"]] = None,
owner: Optional["Model"] = None,
) -> None:
self.owner = proxy(owner)
self._related_fields = related_fields or []
# ``_field_map`` lets ``_get`` build a ``Relation`` lazily by name.
# Holding only the field reference (not a constructed Relation) is
# what skips the per-FK Relation/RelationProxy/QuerysetProxy
# allocation tree on every ``Model.__init__``.
self._field_map: dict[str, "ForeignKeyField"] = {
field.name: field for field in self._related_fields
}
self._related_names = list(self._field_map)
self._relations: dict[str, Relation] = dict()
def __contains__(self, item: str) -> bool:
"""
Checks if relation with given name is already registered.
:param item: name of attribute
:type item: str
:return: result of the check
:rtype: bool
"""
return item in self._related_names
def clear(self) -> None:
for relation in self._relations.values():
relation.clear()
def get(self, name: str) -> Optional[Union["Model", Sequence["Model"]]]:
"""
Returns the related model/models if relation is set.
Actual call is delegated to Relation instance registered under relation name.
:param name: name of the relation
:type name: str
:return: related model or list of related models if set
:rtype: Optional[Union[Model, list[Model]]
"""
relation = self._get(name)
if relation is not None:
return relation.get()
return None # pragma nocover
@staticmethod
def add(parent: "Model", child: "Model", field: "ForeignKeyField") -> None:
"""
Adds relation on both sides -> meaning on both child and parent models.
One side of the relation is always weakref proxy to avoid circular refs.
Based on the side from which relation is added and relation name actual names
of parent and child relations are established. The related models are registered
on both ends.
:param parent: parent model on which relation should be registered
:type parent: Model
:param child: child model to register
:type child: Model
:param field: field with relation definition
:type field: ForeignKeyField
"""
parent, child, child_name, to_name = get_relations_sides_and_names(
field, parent, child
)
# print('adding parent', parent.get_name(), child.get_name(), child_name)
parent_relation = parent._orm._get(child_name)
if parent_relation:
parent_relation.add(child) # type: ignore
# print('adding child', child.get_name(), parent.get_name(), to_name)
child_relation = child._orm._get(to_name)
if child_relation:
child_relation.add(parent)
def remove(
self, name: str, child: Union["NewBaseModel", type["NewBaseModel"]]
) -> None:
"""
Removes given child from relation with given name.
Since you can have many relations between two models you need to pass a name
of relation from which you want to remove the child.
:param name: name of the relation
:type name: str
:param child: child to remove from relation
:type child: Union[Model, type[Model]]
"""
relation = self._get(name)
if relation:
relation.remove(child)
@staticmethod
def remove_parent(
item: Union["NewBaseModel", type["NewBaseModel"]], parent: "Model", name: str
) -> None:
"""
Removes given parent from relation with given name.
Since you can have many relations between two models you need to pass a name
of relation from which you want to remove the parent.
:param item: model with parent registered
:type item: Union[Model, type[Model]]
:param parent: parent Model
:type parent: Model
:param name: name of the relation
:type name: str
"""
relation_name = item.ormar_config.model_fields[name].get_related_name()
item._orm.remove(name, parent)
parent._orm.remove(relation_name, item)
def _get(self, name: str) -> Optional[Relation]:
"""
Return the ``Relation`` for ``name``, building it on first access.
Relations are constructed lazily so that ``Model.__init__`` does
not allocate a ``Relation`` (and, transitively, ``RelationProxy`` /
``QuerysetProxy``) for every declared FK on every instance — most
of which are never read on row-materialization paths.
:param name: name of the relation
:type name: str
:return: existing or freshly constructed Relation, or None if the
name does not correspond to a declared relation
:rtype: ormar.relations.relation.Relation
"""
relation = self._relations.get(name)
if relation is not None:
return relation
field = self._field_map.get(name)
if field is None:
return None
relation = Relation(
manager=self,
type_=self._get_relation_type(field),
field_name=field.name,
to=field.to,
through=getattr(field, "through", None),
)
self._relations[name] = relation
return relation
def _get_relation_type(self, field: "BaseField") -> RelationType:
"""
Returns type of the relation declared on a field.
:param field: field with relation declaration
:type field: BaseField
:return: type of the relation defined on field
:rtype: RelationType
"""
if field.is_multi:
return RelationType.MULTIPLE
if field.is_through:
return RelationType.THROUGH
return RelationType.PRIMARY if not field.virtual else RelationType.REVERSE
ormar-orm-ormar-a7e7fde/ormar/relations/relation_proxy.py 0000664 0000000 0000000 00000031537 15211567312 0024113 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar
from typing_extensions import SupportsIndex
import ormar
from ormar.exceptions import NoMatch, RelationshipInstanceError
from ormar.relations.querysetproxy import QuerysetProxy
if TYPE_CHECKING: # pragma no cover
from ormar import Model, RelationType
from ormar.models import T
from ormar.queryset import QuerySet
from ormar.relations import Relation
else:
T = TypeVar("T", bound="Model")
class RelationProxy(Generic[T], list[T]):
"""
Proxy of the Relation that is a list with special methods.
"""
def __init__(
self,
relation: "Relation",
type_: "RelationType",
to: type["T"],
field_name: str,
data_: Any = None,
) -> None:
self.relation: "Relation[T]" = relation
self.type_: "RelationType" = type_
self.field_name = field_name
self._owner: "Model" = self.relation.manager.owner
self._to: type["T"] = to
self._queryset_proxy: Optional[QuerysetProxy[T]] = None
self._related_field_name: Optional[str] = None
self._relation_cache: dict[int, int] = {}
validated_data = []
if data_ is not None:
idx = 0
for d in data_:
try:
self._relation_cache[d.__hash__()] = idx
validated_data.append(d)
idx += 1
except ReferenceError:
pass
super().__init__(validated_data or ())
@property
def queryset_proxy(self) -> "QuerysetProxy[T]":
"""
Builds the underlying ``QuerysetProxy`` on first access. Most
``RelationProxy`` instances are constructed during row materialization
and never have any queryset method invoked on them, so deferring this
allocation skips a non-trivial dict/setattr pair per relation.
:return: lazily constructed (and cached) QuerysetProxy
:rtype: QuerysetProxy
"""
proxy = self._queryset_proxy
if proxy is None:
proxy = QuerysetProxy[T](
relation=self.relation, to=self._to, type_=self.type_
)
self._queryset_proxy = proxy
return proxy
@property
def related_field_name(self) -> str:
"""
On first access calculates the name of the related field, later stored in
_related_field_name property.
:return: name of the related field
:rtype: str
"""
if self._related_field_name:
return self._related_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
def __getitem__(self, item: Any) -> "T": # type: ignore
return super().__getitem__(item)
def append(self, item: "T") -> None:
"""
Appends an item to the list in place
:param item: The generic item of the list
:type item: T
"""
idx = len(self)
self._relation_cache[item.__hash__()] = idx
super().append(item)
def update_cache(self, prev_hash: int, new_hash: int) -> None:
"""
Updates the cache from the old hash to the new one.
This maintains the index cache, which allows O(1) indexing and
existence checks
:param prev_hash: The hash to update
:type prev_hash: int
:param prev_hash: The new hash to update to
:type new_hash: int
"""
try:
idx = self._relation_cache.pop(prev_hash)
self._relation_cache[new_hash] = idx
except KeyError:
pass
def index(self, item: T, *args: Any) -> int:
"""
Gets the index of the item in the list
:param item: The item to get the index of
:type item: "T"
"""
return self._relation_cache[item.__hash__()]
def _get_list_of_missing_weakrefs(self) -> set[int]:
"""
Iterates through the list and checks for weakrefs.
:return: The set of missing weakref indices
:rtype: set[int]
"""
to_remove = set()
for ind, relation_child in enumerate(self[:]):
try:
relation_child.__repr__.__self__ # type: ignore
except ReferenceError: # pragma no cover
to_remove.add(ind)
return to_remove
def pop(self, index: SupportsIndex = 0) -> T:
"""
Pops the index off the list and returns it. By default,
it pops off the element at index 0.
This also clears the value from the relation cache.
:param index: The index to pop
:type index: SupportsIndex
:return: The item at the provided index
:rtype: "T"
"""
item = self[index]
# Try to delete it, but do it a long way
# if weakly-referenced thing doesn't exist
try:
self._relation_cache.pop(item.__hash__())
except ReferenceError:
for hash_, idx in self._relation_cache.items():
if idx == index:
self._relation_cache.pop(hash_)
break
index_int = int(index)
for idx in range(index_int + 1, len(self)):
self._relation_cache[self[idx].__hash__()] -= 1
return super().pop(index)
def __contains__(self, item: object) -> bool:
"""
Checks whether the item exists in self. This relies
on the relation cache, which is a hashmap of values
in the list. It runs in O(1) time.
:param item: The item to check if the list contains
:type item: object
"""
try:
return item.__hash__() in self._relation_cache
except ReferenceError:
return False
async def count(self, distinct: bool = True) -> int: # type: ignore[override]
"""
Returns count of related models. Delegates to ``QuerysetProxy.count``.
Defined explicitly to shadow ``list.count`` so attribute lookup resolves
to the relation-aware version without going through ``__getattribute__``
on every attribute access.
:param distinct: flag if the primary table rows should be distinct
:type distinct: bool
:return: number of related models
:rtype: int
"""
self._initialize_queryset()
return await self.queryset_proxy.count(distinct=distinct)
async def clear(self, keep_reversed: bool = True) -> int: # type: ignore[override]
"""
Removes all related models from the relation. Delegates to
``QuerysetProxy.clear``.
Defined explicitly to shadow ``list.clear`` so attribute lookup resolves
to the relation-aware version without going through ``__getattribute__``
on every attribute access.
:param keep_reversed: keep reversed FK rows in the database
:type keep_reversed: bool
:return: number of removed relation entries
:rtype: int
"""
self._initialize_queryset()
return await self.queryset_proxy.clear(keep_reversed=keep_reversed)
def __getattr__(self, item: str) -> Any:
"""
Delegates calls for non existing attributes to QuerySetProxy.
:param item: name of attribute/method
:type item: str
:return: method from QuerySetProxy if exists
:rtype: method
"""
self._initialize_queryset()
return getattr(self.queryset_proxy, item)
def _clear(self) -> None:
self._relation_cache.clear()
super().clear()
def _initialize_queryset(self) -> None:
"""
Initializes the QuerySetProxy if not yet initialized.
"""
if not self._check_if_queryset_is_initialized():
self.queryset_proxy.queryset = self._set_queryset()
def _check_if_queryset_is_initialized(self) -> bool:
"""
Checks if the QuerySetProxy is already set and ready. Reads the
backing ``_queryset_proxy`` slot directly so the check itself does
not force lazy construction of the proxy.
:return: result of the check
:rtype: bool
"""
proxy = self._queryset_proxy
return proxy is not None and proxy._queryset is not None
def _check_if_model_saved(self) -> None:
"""
Verifies if the parent model of the relation has been already saved.
Otherwise QuerySetProxy cannot filter by parent primary key.
"""
pk_value = self._owner.pk
if not pk_value:
raise RelationshipInstanceError(
"You cannot query relationships from unsaved model."
)
def _set_queryset(self) -> "QuerySet[T]":
"""
Creates new QuerySet with relation model and pre filters it with currents
parent model primary key, so all queries by definition are already related
to the parent model only, without need for user to filter them.
:return: initialized QuerySet
:rtype: QuerySet
"""
related_field_name = self.related_field_name
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 = (
ormar.QuerySet(
model_cls=self.relation.to, proxy_source_model=self._owner.__class__
)
.select_related(related_field_name)
.filter(**kwargs)
)
return queryset
async def remove( # type: ignore
self, item: "T", keep_reversed: bool = True
) -> None:
"""
Removes the related from relation with parent.
Through models are automatically deleted for m2m relations.
For reverse FK relations keep_reversed flag marks if the reversed models
should be kept or deleted from the database too (False means that models
will be deleted, and not only removed from relation).
:param item: child to remove from relation
:type item: Model
:param keep_reversed: flag if the reversed model should be kept or deleted too
:type keep_reversed: bool
"""
if item not in self:
raise NoMatch(
f"Object {self._owner.get_name()} has no "
f"{item.get_name()} with given primary key!"
)
await self._owner.signals.pre_relation_remove.send(
sender=self._owner.__class__,
instance=self._owner,
child=item,
relation_name=self.field_name,
)
index_to_remove = self._relation_cache[item.__hash__()]
self.pop(index_to_remove)
relation_name = self.related_field_name
relation = item._orm._get(relation_name)
# if relation is None: # pragma nocover
# raise ValueError(
# f"{self._owner.get_name()} does not have relation {relation_name}"
# )
if relation:
relation.remove(self._owner)
self.relation.remove(item)
if self.type_ == ormar.RelationType.MULTIPLE:
await self.queryset_proxy.delete_through_instance(item)
else:
if keep_reversed:
setattr(item, relation_name, None)
await item.update()
else:
await item.delete()
await self._owner.signals.post_relation_remove.send(
sender=self._owner.__class__,
instance=self._owner,
child=item,
relation_name=self.field_name,
)
async def add(self, item: "T", **kwargs: Any) -> None:
"""
Adds child model to relation.
For ManyToMany relations through instance is automatically created.
:param kwargs: dict of additional keyword arguments for through instance
:type kwargs: Any
:param item: child to add to relation
:type item: Model
"""
new_idx = len(self) if item not in self else self.index(item)
relation_name = self.related_field_name
await self._owner.signals.pre_relation_add.send(
sender=self._owner.__class__,
instance=self._owner,
child=item,
relation_name=self.field_name,
passed_kwargs=kwargs,
)
self._check_if_model_saved()
if self.type_ == ormar.RelationType.MULTIPLE:
await self.queryset_proxy.create_through_instance(item, **kwargs)
setattr(self._owner, self.field_name, item)
else:
setattr(item, relation_name, self._owner)
await item.upsert()
self._relation_cache[item.__hash__()] = new_idx
await self._owner.signals.post_relation_add.send(
sender=self._owner.__class__,
instance=self._owner,
child=item,
relation_name=self.field_name,
passed_kwargs=kwargs,
)
ormar-orm-ormar-a7e7fde/ormar/relations/utils.py 0000664 0000000 0000000 00000002034 15211567312 0022163 0 ustar 00root root 0000000 0000000 from typing import TYPE_CHECKING
from weakref import proxy
from ormar.fields.foreign_key import ForeignKeyField
if TYPE_CHECKING: # pragma no cover
from ormar import Model
def get_relations_sides_and_names(
to_field: ForeignKeyField, parent: "Model", child: "Model"
) -> tuple["Model", "Model", str, str]:
"""
Determines the names of child and parent relations names, as well as
changes one of the sides of the relation into weakref.proxy to model.
:param to_field: field with relation definition
:type to_field: ForeignKeyField
:param parent: parent model
:type parent: Model
:param child: child model
:type child: Model
:return: parent, child, child_name, to_name
:rtype: tuple["Model", "Model", str, str]
"""
to_name = to_field.name
child_name = to_field.get_related_name()
if to_field.virtual:
child_name, to_name = to_name, child_name
child, parent = parent, proxy(child)
else:
child = proxy(child)
return parent, child, child_name, to_name
ormar-orm-ormar-a7e7fde/ormar/signals/ 0000775 0000000 0000000 00000000000 15211567312 0020112 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/signals/__init__.py 0000664 0000000 0000000 00000000402 15211567312 0022217 0 ustar 00root root 0000000 0000000 """
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"]
ormar-orm-ormar-a7e7fde/ormar/signals/signal.py 0000664 0000000 0000000 00000006553 15211567312 0021752 0 ustar 00root root 0000000 0000000 import asyncio
import inspect
from typing import TYPE_CHECKING, Any, Callable, Union
from ormar.exceptions import SignalDefinitionError
if TYPE_CHECKING: # pragma: no cover
from ormar import Model
def callable_accepts_kwargs(func: Callable) -> bool:
"""
Checks if function accepts **kwargs.
:param func: function which signature needs to be checked
:type func: function
:return: result of the check
:rtype: bool
"""
return any(
p
for p in inspect.signature(func).parameters.values()
if p.kind == p.VAR_KEYWORD
)
def make_id(target: Any) -> Union[int, tuple[int, int]]:
"""
Creates id of a function or method to be used as key to store signal
:param target: target which id we want
:type target: Any
:return: id of the target
:rtype: int
"""
if hasattr(target, "__func__"):
return id(target.__self__), id(target.__func__)
return id(target)
class Signal:
"""
Signal that notifies all receiver functions.
In ormar used by models to send pre_save, post_save etc. signals.
"""
def __init__(self) -> None:
self._receivers: dict[Union[int, tuple[int, int]], Callable] = {}
def connect(self, receiver: Callable) -> None:
"""
Connects given receiver function to the signal.
:raises SignalDefinitionError: if receiver is not callable
or not accept **kwargs
:param receiver: receiver function
:type receiver: Callable
"""
if not callable(receiver):
raise SignalDefinitionError("Signal receivers must be callable.")
if not callable_accepts_kwargs(receiver):
raise SignalDefinitionError(
"Signal receivers must accept **kwargs argument."
)
new_receiver_key = make_id(receiver)
if new_receiver_key not in self._receivers:
self._receivers[new_receiver_key] = receiver
def disconnect(self, receiver: Callable) -> bool:
"""
Removes the receiver function from the signal.
:param receiver: receiver function
:type receiver: Callable
:return: flag if receiver was removed
:rtype: bool
"""
new_receiver_key = make_id(receiver)
receiver_func: Union[Callable, None] = self._receivers.pop(
new_receiver_key, None
)
return True if receiver_func is not None else False
async def send(self, sender: type["Model"], **kwargs: Any) -> None:
"""
Notifies all receiver functions with given kwargs
:param sender: model that sends the signal
:type sender: type["Model"]
:param kwargs: arguments passed to receivers
:type kwargs: Any
"""
receivers = [
receiver_func(sender=sender, **kwargs)
for receiver_func in self._receivers.values()
]
await asyncio.gather(*receivers)
class SignalEmitter(dict):
"""
Emitter that registers the signals in internal dictionary.
If signal with given name does not exist it's auto added on access.
"""
def __getattr__(self, item: str) -> Signal:
return self.setdefault(item, Signal())
def __setattr__(self, key: str, value: Signal) -> None:
if not isinstance(value, Signal):
raise SignalDefinitionError(f"{value} is not valid signal")
self[key] = value
ormar-orm-ormar-a7e7fde/ormar/utils/ 0000775 0000000 0000000 00000000000 15211567312 0017612 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/utils/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0021711 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/ormar/warnings.py 0000664 0000000 0000000 00000003525 15211567312 0020661 0 ustar 00root root 0000000 0000000 # Adopted from pydantic
from typing import Optional
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))
ormar-orm-ormar-a7e7fde/poetry.lock 0000664 0000000 0000000 00001002704 15211567312 0017532 0 ustar 00root root 0000000 0000000 # This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand.
[[package]]
name = "aiomysql"
version = "0.3.2"
description = "MySQL driver for asyncio."
optional = true
python-versions = ">=3.9"
groups = ["main"]
markers = "extra == \"mysql\" or extra == \"all\""
files = [
{file = "aiomysql-0.3.2-py3-none-any.whl", hash = "sha256:c82c5ba04137d7afd5c693a258bea8ead2aad77101668044143a991e04632eb2"},
{file = "aiomysql-0.3.2.tar.gz", hash = "sha256:72d15ef5cfc34c03468eb41e1b90adb9fd9347b0b589114bd23ead569a02ac1a"},
]
[package.dependencies]
PyMySQL = ">=1.0"
[package.extras]
rsa = ["PyMySQL[rsa] (>=1.0)"]
sa = ["sqlalchemy (>=1.3,<1.4)"]
[[package]]
name = "aiopg"
version = "1.4.0"
description = "Postgres integration with asyncio."
optional = true
python-versions = ">=3.7"
groups = ["main"]
markers = "extra == \"aiopg\" or extra == \"all\""
files = [
{file = "aiopg-1.4.0-py3-none-any.whl", hash = "sha256:aea46e8aff30b039cfa818e6db4752c97656e893fc75e5a5dc57355a9e9dedbd"},
{file = "aiopg-1.4.0.tar.gz", hash = "sha256:116253bef86b4d954116716d181e9a0294037f266718b2e1c9766af995639d71"},
]
[package.dependencies]
async-timeout = ">=3.0,<5.0"
psycopg2-binary = ">=2.9.5"
[package.extras]
sa = ["sqlalchemy[postgresql-psycopg2binary] (>=1.3,<1.5)"]
[[package]]
name = "aiosqlite"
version = "0.22.1"
description = "asyncio bridge to the standard sqlite3 module"
optional = true
python-versions = ">=3.9"
groups = ["main"]
markers = "extra == \"sqlite\" or extra == \"all\""
files = [
{file = "aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb"},
{file = "aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650"},
]
[package.extras]
dev = ["attribution (==1.8.0)", "black (==25.11.0)", "build (>=1.2)", "coverage[toml] (==7.10.7)", "flake8 (==7.3.0)", "flake8-bugbear (==24.12.12)", "flit (==3.12.0)", "mypy (==1.19.0)", "ufmt (==2.8.0)", "usort (==1.0.8.post1)"]
docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.2)"]
[[package]]
name = "annotated-doc"
version = "0.0.4"
description = "Document parameters, class attributes, return types, and variables inline, with Annotated."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"},
{file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"},
]
[[package]]
name = "annotated-types"
version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "anyio"
version = "4.5.2"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"},
{file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"},
]
[package.dependencies]
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
idna = ">=2.8"
sniffio = ">=1.1"
typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
[package.extras]
doc = ["Sphinx (>=7.4,<8.0)", "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", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21.0b1) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""]
trio = ["trio (>=0.26.1)"]
[[package]]
name = "asgi-lifespan"
version = "2.1.0"
description = "Programmatic startup/shutdown of ASGI apps."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "asgi-lifespan-2.1.0.tar.gz", hash = "sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308"},
{file = "asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f"},
]
[package.dependencies]
sniffio = "*"
[[package]]
name = "ast-serialize"
version = "0.3.0"
description = "Python bindings for mypy AST serialization"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ast_serialize-0.3.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a867927df59f76a18dc1d874a0b2c079b42c58972dca637905576deb0912e14"},
{file = "ast_serialize-0.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a6fb063bf040abf8321e7b8113a0554eda445ffc508aa51287f8808886a5ae22"},
{file = "ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5075cd8482573d743586779e5f9b652a015e37d4e95132d7e5a9bc5c8f483d8f"},
{file = "ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41560b27794f4553b0f77811e9fb325b77db4a2b39018d437e09932275306e66"},
{file = "ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b967c01ca74909c5d90e0fe4393401e2cc5da5ebd9a6262a19e45ffd3757dec8"},
{file = "ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:424ebb8f46cd993f7cec4009d119312d8433dd90e6b0df0499cd2c91bdcc5af9"},
{file = "ast_serialize-0.3.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14b1d566b56e2ee70b11fec1de7e0b94ec7cd83717ec7d189967841a361190e"},
{file = "ast_serialize-0.3.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ba30b18735f047ec11103d1ab92f4789cf1fea1e0dc89b04a2f5a0632fd79de"},
{file = "ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e6ea0754cb7b0f682ebb005ffb0d18f8d17993490d9c289863cd69cacc4ab8df"},
{file = "ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:a0c5aa1073a5ba7b2abaa4b54abe8b8d75c4d1e2d54a2ff70b0ca6222fea5728"},
{file = "ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4e52650d834c1ea7791969a361de2c54c13b2fb4c519ec79445fa8b9021a147d"},
{file = "ast_serialize-0.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15bd6af3f136c61dae27805eb6b8f3269e85a545c4c27ffe9e530ead78d2b36d"},
{file = "ast_serialize-0.3.0-cp314-cp314t-win32.whl", hash = "sha256:d188bfe37b674b49708497683051d4b571366a668799c9b8e8a94513694969d9"},
{file = "ast_serialize-0.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5832c2fdf8f8a6cf682b4cfcf677f5eaf39b4ddbc490f5480cfccdd1e7ce8fa1"},
{file = "ast_serialize-0.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:670f177188d128fb7f9f15b5ad0e1b553d22c34e3f584dcb83eb8077600437f0"},
{file = "ast_serialize-0.3.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ec2fafa5e4313cc8feed96e436ebe19ac7bc6fa41fbc2827e826c48b9e4c3a9"},
{file = "ast_serialize-0.3.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef6d3c08b7b4cd29b48410338e134764a00e76d25841eb02c1084e868c888ecc"},
{file = "ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d841424f41b886e98044abc80769c14a956e6e5ccd5fb5b0d9f5ead72be18a4"},
{file = "ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d21453734ad39367ede5d37efe4f59f830ce1c09f432fc72a90e368f77a4a3e7"},
{file = "ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5e110cdce2a347e1dd987529c88ef54d26f67848dce3eba1b3b2cc2cf085c94"},
{file = "ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6e23a98e57560a055f5c4b68700a0fd5ce483d2814c23140b3638c7f5d1e61"},
{file = "ast_serialize-0.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c9e763d70293d65ce1e1ea8c943140c68d0953f0268c7ee0998f2e07f77dd0"},
{file = "ast_serialize-0.3.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4388a1796c228f1ce5c391426f7d21a0003ad3b47f677dbeded9bd1a85c7209f"},
{file = "ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5283cdcc0c64c3d8b9b688dc6aaa012d9c0cf1380a7f774a6bae6a1c01b3205a"},
{file = "ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ef88cc5842a5d7a6ac09dc0d5fc2c98f5d276c1f076f866d55047ce886785b"},
{file = "ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cc14bf402bdc0978594ecce783793de2c7470cd4f5cd7eb286ca97ed8ff7cba9"},
{file = "ast_serialize-0.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11eae0cf1b7b3e0678133cc2daa974ea972caf02eb4b3aa062af6fa9acd52c57"},
{file = "ast_serialize-0.3.0-cp39-abi3-win32.whl", hash = "sha256:2db3dd99de5e6a5a11d7dda73de8750eb6e5baaf25245adf7bdcfe64b6108ae2"},
{file = "ast_serialize-0.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:a2cd125adccf7969470621905d302750cd25951f22ea430d9a25b7be031e5549"},
{file = "ast_serialize-0.3.0-cp39-abi3-win_arm64.whl", hash = "sha256:0dd00da29985f15f50dc35728b7e1e7c84507bccfea1d9914738530f1c72238a"},
{file = "ast_serialize-0.3.0.tar.gz", hash = "sha256:1bc3ca09a63a021376527c4e938deedd11d11d675ce850e6f9c7487f5889992b"},
]
[[package]]
name = "async-timeout"
version = "4.0.3"
description = "Timeout context manager for asyncio programs"
optional = true
python-versions = ">=3.7"
groups = ["main"]
markers = "extra == \"aiopg\" or extra == \"all\" or (extra == \"postgresql\" or extra == \"postgres\" or extra == \"all\" or extra == \"aiopg\") and python_version < \"3.11.0\""
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
]
[[package]]
name = "asyncpg"
version = "0.31.0"
description = "An asyncio PostgreSQL driver"
optional = true
python-versions = ">=3.9.0"
groups = ["main"]
markers = "extra == \"postgresql\" or extra == \"postgres\" or extra == \"all\""
files = [
{file = "asyncpg-0.31.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831712dd3cf117eec68575a9b50da711893fd63ebe277fc155ecae1c6c9f0f61"},
{file = "asyncpg-0.31.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b17c89312c2f4ccea222a3a6571f7df65d4ba2c0e803339bfc7bed46a96d3be"},
{file = "asyncpg-0.31.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3faa62f997db0c9add34504a68ac2c342cfee4d57a0c3062fcf0d86c7f9cb1e8"},
{file = "asyncpg-0.31.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea599d45c361dfbf398cb67da7fd052affa556a401482d3ff1ee99bd68808a1"},
{file = "asyncpg-0.31.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:795416369c3d284e1837461909f58418ad22b305f955e625a4b3a2521d80a5f3"},
{file = "asyncpg-0.31.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a8d758dac9d2e723e173d286ef5e574f0b350ec00e9186fce84d0fc5f6a8e6b8"},
{file = "asyncpg-0.31.0-cp310-cp310-win32.whl", hash = "sha256:2d076d42eb583601179efa246c5d7ae44614b4144bc1c7a683ad1222814ed095"},
{file = "asyncpg-0.31.0-cp310-cp310-win_amd64.whl", hash = "sha256:9ea33213ac044171f4cac23740bed9a3805abae10e7025314cfbd725ec670540"},
{file = "asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d"},
{file = "asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab"},
{file = "asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c"},
{file = "asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109"},
{file = "asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da"},
{file = "asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9"},
{file = "asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24"},
{file = "asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047"},
{file = "asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad"},
{file = "asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d"},
{file = "asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a"},
{file = "asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671"},
{file = "asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec"},
{file = "asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20"},
{file = "asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8"},
{file = "asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186"},
{file = "asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b"},
{file = "asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e"},
{file = "asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403"},
{file = "asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4"},
{file = "asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2"},
{file = "asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602"},
{file = "asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696"},
{file = "asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab"},
{file = "asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44"},
{file = "asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5"},
{file = "asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2"},
{file = "asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2"},
{file = "asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218"},
{file = "asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d"},
{file = "asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b"},
{file = "asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be"},
{file = "asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2"},
{file = "asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31"},
{file = "asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7"},
{file = "asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e"},
{file = "asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c"},
{file = "asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a"},
{file = "asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d"},
{file = "asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3"},
{file = "asyncpg-0.31.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb3cde58321a1f89ce41812be3f2a98dddedc1e76d0838aba1d724f1e4e1a95"},
{file = "asyncpg-0.31.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6974f36eb9a224d8fb428bcf66bd411aa12cf57c2967463178149e73d4de366"},
{file = "asyncpg-0.31.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2b685f400ceae428f79f78b58110470d7b4466929a7f78d455964b17ad1008"},
{file = "asyncpg-0.31.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb223567dea5f47c45d347f2bde5486be8d9f40339f27217adb3fb1c3be51298"},
{file = "asyncpg-0.31.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:22be6e02381bab3101cd502d9297ac71e2f966c86e20e78caead9934c98a8af6"},
{file = "asyncpg-0.31.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:37a58919cfef2448a920df00d1b2f821762d17194d0dbf355d6dde8d952c04f9"},
{file = "asyncpg-0.31.0-cp39-cp39-win32.whl", hash = "sha256:c1a9c5b71d2371a2290bc93336cd05ba4ec781683cab292adbddc084f89443c6"},
{file = "asyncpg-0.31.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1e1ab5bc65373d92dd749d7308c5b26fb2dc0fbe5d3bf68a32b676aa3bcd24a"},
{file = "asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735"},
]
[package.dependencies]
async_timeout = {version = ">=4.0.3", markers = "python_version < \"3.11.0\""}
[package.extras]
gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""]
[[package]]
name = "babel"
version = "2.16.0"
description = "Internationalization utilities"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"},
{file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"},
]
[package.extras]
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "backports-asyncio-runner"
version = "1.2.0"
description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle."
optional = false
python-versions = "<3.11,>=3.8"
groups = ["dev"]
markers = "python_version < \"3.11\""
files = [
{file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"},
{file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"},
]
[[package]]
name = "backrefs"
version = "6.1"
description = "A wrapper around re and regex that adds additional back references."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1"},
{file = "backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7"},
{file = "backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a"},
{file = "backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05"},
{file = "backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853"},
{file = "backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0"},
{file = "backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231"},
]
[package.extras]
extras = ["regex"]
[[package]]
name = "certifi"
version = "2024.8.30"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
groups = ["dev"]
files = [
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
]
[[package]]
name = "cffi"
version = "2.0.0"
description = "Foreign Function Interface for Python calling C code."
optional = true
python-versions = ">=3.9"
groups = ["main"]
markers = "(extra == \"crypto\" or extra == \"all\") and platform_python_implementation != \"PyPy\""
files = [
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
{file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
{file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
{file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
{file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
{file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
{file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
{file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
{file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
{file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
{file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
{file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
{file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
{file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
{file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
{file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
{file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
{file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
{file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
{file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
{file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
{file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
{file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
{file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
{file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
{file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
{file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
{file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
{file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
{file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
{file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
{file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
{file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
{file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
{file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
{file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
{file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
{file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
{file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
]
[package.dependencies]
pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
[[package]]
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]]
name = "charset-normalizer"
version = "3.4.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7.0"
groups = ["dev"]
files = [
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"},
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"},
{file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"},
{file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"},
{file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"},
{file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"},
{file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"},
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"},
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"},
{file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"},
{file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"},
{file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"},
{file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"},
{file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"},
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"},
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"},
{file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"},
{file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"},
{file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"},
{file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"},
{file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"},
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"},
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"},
{file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"},
{file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"},
{file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"},
{file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"},
{file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"},
{file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"},
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"},
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"},
{file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"},
{file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"},
{file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"},
{file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"},
{file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"},
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"},
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"},
{file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"},
{file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"},
{file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"},
{file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"},
{file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"},
{file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"},
{file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "codecov"
version = "2.1.13"
description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
groups = ["dev"]
files = [
{file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"},
{file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"},
]
[package.dependencies]
coverage = "*"
requests = ">=2.7.9"
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "coverage"
version = "7.13.3"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0"},
{file = "coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b"},
{file = "coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8"},
{file = "coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0"},
{file = "coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6"},
{file = "coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f"},
{file = "coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e"},
{file = "coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56"},
{file = "coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f"},
{file = "coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a"},
{file = "coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be"},
{file = "coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b"},
{file = "coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73"},
{file = "coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00"},
{file = "coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2"},
{file = "coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c"},
{file = "coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b"},
{file = "coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0"},
{file = "coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14"},
{file = "coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4"},
{file = "coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad"},
{file = "coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222"},
{file = "coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb"},
{file = "coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301"},
{file = "coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba"},
{file = "coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595"},
{file = "coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6"},
{file = "coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395"},
{file = "coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23"},
{file = "coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34"},
{file = "coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8"},
{file = "coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a"},
{file = "coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4"},
{file = "coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7"},
{file = "coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0"},
{file = "coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1"},
{file = "coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d"},
{file = "coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f"},
{file = "coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25"},
{file = "coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a"},
{file = "coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627"},
{file = "coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8"},
{file = "coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1"},
{file = "coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b"},
{file = "coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc"},
{file = "coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea"},
{file = "coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67"},
{file = "coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86"},
{file = "coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43"},
{file = "coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587"},
{file = "coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051"},
{file = "coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9"},
{file = "coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e"},
{file = "coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107"},
{file = "coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43"},
{file = "coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3"},
{file = "coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a"},
{file = "coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e"},
{file = "coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155"},
{file = "coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e"},
{file = "coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96"},
{file = "coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f"},
{file = "coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c"},
{file = "coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9"},
{file = "coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b"},
{file = "coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10"},
{file = "coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39"},
{file = "coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f"},
{file = "coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4"},
{file = "coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef"},
{file = "coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75"},
{file = "coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895"},
{file = "coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c"},
{file = "coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a"},
{file = "coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4"},
{file = "coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0"},
{file = "coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3"},
{file = "coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8"},
{file = "coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca"},
{file = "coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba"},
{file = "coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f"},
{file = "coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508"},
{file = "coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba"},
{file = "coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd"},
{file = "coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab"},
{file = "coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e"},
{file = "coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024"},
{file = "coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3"},
{file = "coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8"},
{file = "coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3"},
{file = "coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910"},
{file = "coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac"},
]
[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
[package.extras]
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "cryptography"
version = "48.0.0"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = true
python-versions = "!=3.9.0,!=3.9.1,>=3.9"
groups = ["main"]
markers = "extra == \"crypto\" or extra == \"all\""
files = [
{file = "cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6"},
{file = "cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c"},
{file = "cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3"},
{file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5"},
{file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c"},
{file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f"},
{file = "cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25"},
{file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602"},
{file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c"},
{file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5"},
{file = "cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321"},
{file = "cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74"},
{file = "cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4"},
{file = "cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7"},
{file = "cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec"},
{file = "cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18"},
{file = "cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20"},
{file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff"},
{file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c"},
{file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db"},
{file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741"},
{file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166"},
{file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336"},
{file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057"},
{file = "cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae"},
{file = "cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c"},
{file = "cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f"},
{file = "cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12"},
{file = "cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86"},
{file = "cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e"},
{file = "cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f"},
{file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7"},
{file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832"},
{file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c"},
{file = "cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a"},
{file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a"},
{file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a"},
{file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239"},
{file = "cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c"},
{file = "cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4"},
{file = "cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd"},
{file = "cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8"},
{file = "cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855"},
{file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b"},
{file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13"},
{file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb"},
{file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355"},
{file = "cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a"},
{file = "cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920"},
]
[package.dependencies]
cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""}
typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""}
[package.extras]
ssh = ["bcrypt (>=3.1.5)"]
[[package]]
name = "dataclasses"
version = "0.6"
description = "A backport of the dataclasses module for Python 3.6"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"},
{file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"},
]
[[package]]
name = "distlib"
version = "0.3.9"
description = "Distribution utilities"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"},
{file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"},
]
[[package]]
name = "dnspython"
version = "2.6.1"
description = "DNS toolkit"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"},
{file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"},
]
[package.extras]
dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"]
dnssec = ["cryptography (>=41)"]
doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"]
doq = ["aioquic (>=0.9.25)"]
idna = ["idna (>=3.6)"]
trio = ["trio (>=0.23)"]
wmi = ["wmi (>=1.5.1)"]
[[package]]
name = "email-validator"
version = "2.3.0"
description = "A robust email address syntax and deliverability validation library."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4"},
{file = "email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426"},
]
[package.dependencies]
dnspython = ">=2.0.0"
idna = ">=2.0.0"
[[package]]
name = "exceptiongroup"
version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version < \"3.11\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "faker"
version = "40.21.0"
description = "Faker is a Python package that generates fake data for you."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "faker-40.21.0-py3-none-any.whl", hash = "sha256:cb6601b2ae8e128895dc96814d271eab6b930a2d2d7932c6f9ff26785c24ee18"},
{file = "faker-40.21.0.tar.gz", hash = "sha256:2fdee1b650a723a54432db9c6dfe17cfa29d1adc8bd60520444a07698524ba4d"},
]
[package.dependencies]
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
tzdata = ["tzdata"]
[[package]]
name = "fastapi"
version = "0.136.3"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620"},
{file = "fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab"},
]
[package.dependencies]
annotated-doc = ">=0.0.2"
pydantic = ">=2.9.0"
starlette = ">=0.46.0"
typing-extensions = ">=4.8.0"
typing-inspection = ">=0.4.2"
[package.extras]
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "fastar (>=0.9.0)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "filelock"
version = "3.20.3"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"},
{file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"},
]
[[package]]
name = "ghp-import"
version = "2.1.0"
description = "Copy your docs directly to the gh-pages branch."
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
]
[package.dependencies]
python-dateutil = ">=2.8.1"
[package.extras]
dev = ["flake8", "markdown", "twine", "wheel"]
[[package]]
name = "greenlet"
version = "3.1.1"
description = "Lightweight in-process concurrent programming"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""
files = [
{file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"},
{file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"},
{file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"},
{file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"},
{file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"},
{file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"},
{file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"},
{file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"},
{file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"},
{file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"},
{file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"},
{file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"},
{file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"},
{file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"},
{file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"},
{file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"},
{file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"},
{file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"},
{file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"},
{file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"},
{file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"},
{file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"},
{file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"},
{file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"},
{file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"},
{file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"},
{file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"},
{file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"},
{file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"},
{file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"},
{file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"},
{file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"},
{file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"},
{file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"},
{file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"},
{file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"},
{file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"},
{file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"},
{file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"},
{file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"},
{file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"},
{file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"},
{file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"},
{file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"},
{file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"},
{file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"},
{file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"},
{file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"},
{file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"},
{file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"},
{file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"},
{file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"},
{file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"},
{file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"},
{file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"},
{file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"},
{file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"},
{file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"},
{file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"},
{file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"},
{file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"},
{file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"},
{file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"},
{file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"},
{file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"},
{file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"},
{file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"},
{file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"},
{file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"},
{file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"},
{file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"},
{file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"},
{file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"},
]
[package.extras]
docs = ["Sphinx", "furo"]
test = ["objgraph", "psutil"]
[[package]]
name = "griffe"
version = "1.15.0"
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."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3"},
{file = "griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea"},
]
[package.dependencies]
colorama = ">=0.4"
[package.extras]
pypi = ["pip (>=24.0)", "platformdirs (>=4.2)", "wheel (>=0.42)"]
[[package]]
name = "h11"
version = "0.16.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
]
[[package]]
name = "httpcore"
version = "1.0.9"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
]
[package.dependencies]
certifi = "*"
h11 = ">=0.16"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<1.0)"]
[[package]]
name = "httpx"
version = "0.28.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
]
[package.dependencies]
anyio = "*"
certifi = "*"
httpcore = "==1.*"
idna = "*"
[package.extras]
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "identify"
version = "2.6.1"
description = "File identification library for Python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"},
{file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"},
]
[package.extras]
license = ["ukkonen"]
[[package]]
name = "idna"
version = "3.15"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"},
{file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"},
]
[package.extras]
all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "jinja2"
version = "3.1.6"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "librt"
version = "0.11.0"
description = "Mypyc runtime library"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
markers = "platform_python_implementation != \"PyPy\""
files = [
{file = "librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f"},
{file = "librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45"},
{file = "librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c"},
{file = "librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33"},
{file = "librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884"},
{file = "librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280"},
{file = "librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c"},
{file = "librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb"},
{file = "librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783"},
{file = "librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0"},
{file = "librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89"},
{file = "librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4"},
{file = "librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29"},
{file = "librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9"},
{file = "librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5"},
{file = "librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b"},
{file = "librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89"},
{file = "librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc"},
{file = "librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5"},
{file = "librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7"},
{file = "librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d"},
{file = "librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412"},
{file = "librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d"},
{file = "librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73"},
{file = "librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c"},
{file = "librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46"},
{file = "librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3"},
{file = "librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67"},
{file = "librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a"},
{file = "librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a"},
{file = "librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f"},
{file = "librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b"},
{file = "librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766"},
{file = "librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d"},
{file = "librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8"},
{file = "librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a"},
{file = "librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9"},
{file = "librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c"},
{file = "librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894"},
{file = "librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c"},
{file = "librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea"},
{file = "librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230"},
{file = "librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2"},
{file = "librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3"},
{file = "librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21"},
{file = "librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930"},
{file = "librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be"},
{file = "librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e"},
{file = "librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e"},
{file = "librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47"},
{file = "librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44"},
{file = "librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd"},
{file = "librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4"},
{file = "librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8"},
{file = "librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b"},
{file = "librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175"},
{file = "librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03"},
{file = "librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c"},
{file = "librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3"},
{file = "librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96"},
{file = "librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe"},
{file = "librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f"},
{file = "librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7"},
{file = "librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1"},
{file = "librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72"},
{file = "librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa"},
{file = "librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548"},
{file = "librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2"},
{file = "librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f"},
{file = "librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51"},
{file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2"},
{file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085"},
{file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3"},
{file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd"},
{file = "librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8"},
{file = "librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c"},
{file = "librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253"},
{file = "librt-0.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd72d903911d995ab666dbd1871f8b1e80925a699af8063fbf50053329fb05f"},
{file = "librt-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ef69ac715f3cd8e5cd252cb2aebfa72c015492aacc339d5d7bf8fef3c62c677"},
{file = "librt-0.11.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:624a40c4a4ad7773315c287276cd024509b2c66ff5904f504bfc08d2c70293ab"},
{file = "librt-0.11.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:41dc19fe150b69716c8ece4f76773a9e8813fe3e35e032a58b4d46423fb8d7c0"},
{file = "librt-0.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8bd98ea9c47ae90b319a087ab28dac493f1ffbc1ecd1f28fcdbf3b7e1108d1"},
{file = "librt-0.11.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84308fc49423ce6475d1c5d1985cd69a8ca9f0325fc7d5f81bb690a3f3625d4e"},
{file = "librt-0.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff0fbaf5f44a21beeb0110f2ab64f45135a9536a834b79c0d1ef018f2786bbfa"},
{file = "librt-0.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9c028a9442a18e266955d364ce42259136e79a7ba14d773e0d778d5f70cd56f1"},
{file = "librt-0.11.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9f1692105a02bcf853f355032a5fdc5494358ef83d8fd22d16de375c85cec3f5"},
{file = "librt-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a80a71e1fda83cc752a9141e87aae7fef279538597564d670e9ce513f286192"},
{file = "librt-0.11.0-cp39-cp39-win32.whl", hash = "sha256:140695816ddf3c86eb972981a26f35efd871c44b0c3aed44c8cd01749386617f"},
{file = "librt-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f7ff819c197fc30473190a12c2856f325ac90aabfccbeb2072d28cc2e234e3"},
{file = "librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1"},
]
[[package]]
name = "markdown"
version = "3.8.1"
description = "Python implementation of John Gruber's Markdown."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "markdown-3.8.1-py3-none-any.whl", hash = "sha256:46cc0c0f1e5211ab2e9d453582f0b28a1bfaf058a9f7d5c50386b99b588d8811"},
{file = "markdown-3.8.1.tar.gz", hash = "sha256:a2e2f01cead4828ee74ecca9623045f62216aef2212a7685d6eb9163f590b8c1"},
]
[package.extras]
docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
testing = ["coverage", "pyyaml"]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
]
[package.dependencies]
mdurl = ">=0.1,<1.0"
[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 = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
name = "markupsafe"
version = "2.1.5"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{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]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
[[package]]
name = "mergedeep"
version = "1.3.4"
description = "A deep merge function for 🐍."
optional = false
python-versions = ">=3.6"
groups = ["dev"]
files = [
{file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
{file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
]
[[package]]
name = "mike"
version = "2.2.0"
description = "Manage multiple versions of your MkDocs-powered documentation"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "mike-2.2.0-py3-none-any.whl", hash = "sha256:e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040"},
{file = "mike-2.2.0.tar.gz", hash = "sha256:1e3858e32c0f125aac14432fc7848434358f9ae0962c5c5cde387ad47f6ad25e"},
]
[package.dependencies]
jinja2 = ">=2.7"
mkdocs = ">=1.0,<2.0"
pyparsing = ">=3.0"
pyyaml = ">=5.1"
pyyaml-env-tag = "*"
verspec = "*"
[package.extras]
dev = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"]
test = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"]
[[package]]
name = "mkdocs"
version = "1.6.1"
description = "Project documentation with Markdown."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"},
{file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"},
]
[package.dependencies]
click = ">=7.0"
colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
ghp-import = ">=1.0"
jinja2 = ">=2.11.1"
markdown = ">=3.3.6"
markupsafe = ">=2.0.1"
mergedeep = ">=1.3.4"
mkdocs-get-deps = ">=0.2.0"
packaging = ">=20.5"
pathspec = ">=0.11.1"
pyyaml = ">=5.1"
pyyaml-env-tag = ">=0.1"
watchdog = ">=2.0"
[package.extras]
i18n = ["babel (>=2.9.0)"]
min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"]
[[package]]
name = "mkdocs-autorefs"
version = "1.4.3"
description = "Automatically link across pages in MkDocs."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9"},
{file = "mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75"},
]
[package.dependencies]
Markdown = ">=3.3"
markupsafe = ">=2.0.1"
mkdocs = ">=1.1"
[[package]]
name = "mkdocs-gen-files"
version = "0.6.1"
description = "MkDocs plugin to programmatically generate documentation pages during the build"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "mkdocs_gen_files-0.6.1-py3-none-any.whl", hash = "sha256:b3182bfc6219e35b8d26658cb988368659d5d023aac30c2a819247558fc12189"},
{file = "mkdocs_gen_files-0.6.1.tar.gz", hash = "sha256:57d7ff2229e23d077e46d14a33db6d37c8823f6ce1a503c874c1764a71679763"},
]
[package.dependencies]
mkdocs = ">=1.4.1,<=1.6.1"
properdocs = ">=1.6.5"
[[package]]
name = "mkdocs-get-deps"
version = "0.2.0"
description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"},
{file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"},
]
[package.dependencies]
mergedeep = ">=1.3.4"
platformdirs = ">=2.2.0"
pyyaml = ">=5.1"
[[package]]
name = "mkdocs-literate-nav"
version = "0.6.3"
description = "MkDocs plugin to specify the navigation in Markdown instead of YAML"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "mkdocs_literate_nav-0.6.3-py3-none-any.whl", hash = "sha256:2c421561280fa9184f88cbf399bebbd4cc17ee507e978a31ce11fd6f3aabf233"},
{file = "mkdocs_literate_nav-0.6.3.tar.gz", hash = "sha256:edbaca22343f861fe4e34aac47d55a0c9955c640dbf02eea99fe631e914cf9ee"},
]
[package.dependencies]
mkdocs = ">=1.4.1,<=1.6.1"
properdocs = ">=1.6.5"
[[package]]
name = "mkdocs-material"
version = "9.7.6"
description = "Documentation that simply works"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba"},
{file = "mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69"},
]
[package.dependencies]
babel = ">=2.10"
backrefs = ">=5.7.post1"
colorama = ">=0.4"
jinja2 = ">=3.1"
markdown = ">=3.2"
mkdocs = ">=1.6,<2"
mkdocs-material-extensions = ">=1.3"
paginate = ">=0.5"
pygments = ">=2.16"
pymdown-extensions = ">=10.2"
requests = ">=2.30"
[package.extras]
git = ["mkdocs-git-committers-plugin-2 (>=1.1)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4)"]
imaging = ["cairosvg (>=2.6)", "pillow (>=10.2)"]
recommended = ["mkdocs-minify-plugin (>=0.7)", "mkdocs-redirects (>=1.2)", "mkdocs-rss-plugin (>=1.6)"]
[[package]]
name = "mkdocs-material-extensions"
version = "1.3.1"
description = "Extension pack for Python Markdown and MkDocs Material."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{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]]
name = "mkdocs-section-index"
version = "0.3.12"
description = "MkDocs plugin to allow clickable sections that lead to an index page"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "mkdocs_section_index-0.3.12-py3-none-any.whl", hash = "sha256:a1100039546beb4ebef63ce6fc91f3195fb9c0c3763105d4d3d7cd31e0a046eb"},
{file = "mkdocs_section_index-0.3.12.tar.gz", hash = "sha256:285635bf86c643b0fc7a343053d7a818049817bff4408f52b80c4367bd5e7268"},
]
[package.dependencies]
mkdocs = ">=1.2,<=1.6.1"
properdocs = ">=1.6.5"
[[package]]
name = "mkdocstrings"
version = "1.0.4"
description = "Automatic documentation from sources, for MkDocs."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b"},
{file = "mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172"},
]
[package.dependencies]
Jinja2 = ">=3.1"
Markdown = ">=3.6"
MarkupSafe = ">=1.1"
mkdocs = ">=1.6"
mkdocs-autorefs = ">=1.4"
mkdocstrings-python = {version = ">=1.16.2", optional = true, markers = "extra == \"python\""}
pymdown-extensions = ">=6.3"
[package.extras]
crystal = ["mkdocstrings-crystal (>=0.3.4)"]
python = ["mkdocstrings-python (>=1.16.2)"]
python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
[[package]]
name = "mkdocstrings-python"
version = "2.0.1"
description = "A Python handler for mkdocstrings."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90"},
{file = "mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732"},
]
[package.dependencies]
griffe = ">=1.13"
mkdocs-autorefs = ">=1.4"
mkdocstrings = ">=0.30"
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
[[package]]
name = "mypy"
version = "2.1.0"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc"},
{file = "mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849"},
{file = "mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd"},
{file = "mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166"},
{file = "mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8"},
{file = "mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8"},
{file = "mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e"},
{file = "mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41"},
{file = "mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca"},
{file = "mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538"},
{file = "mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398"},
{file = "mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563"},
{file = "mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389"},
{file = "mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666"},
{file = "mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af"},
{file = "mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6"},
{file = "mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211"},
{file = "mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b"},
{file = "mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22"},
{file = "mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b"},
{file = "mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8"},
{file = "mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5"},
{file = "mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e"},
{file = "mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e"},
{file = "mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285"},
{file = "mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5"},
{file = "mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65"},
{file = "mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d"},
{file = "mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2"},
{file = "mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f"},
{file = "mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4"},
{file = "mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef"},
{file = "mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135"},
{file = "mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21"},
{file = "mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57"},
{file = "mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e"},
{file = "mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780"},
{file = "mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd"},
{file = "mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08"},
{file = "mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081"},
{file = "mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7"},
{file = "mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6"},
{file = "mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289"},
{file = "mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633"},
]
[package.dependencies]
ast-serialize = ">=0.3.0,<1.0.0"
librt = {version = ">=0.11.0", markers = "platform_python_implementation != \"PyPy\""}
mypy_extensions = ">=1.0.0"
pathspec = ">=1.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing_extensions = [
{version = ">=4.6.0", markers = "python_version < \"3.15\""},
{version = ">=4.14.0", markers = "python_version >= \"3.15\""},
]
[package.extras]
dmypy = ["psutil (>=4.0)"]
faster-cache = ["orjson"]
install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
groups = ["dev"]
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "mysqlclient"
version = "2.2.8"
description = "Python interface to MySQL"
optional = true
python-versions = ">=3.10"
groups = ["main"]
markers = "extra == \"all\""
files = [
{file = "mysqlclient-2.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:86db31bba7b3480fec2751350e9790e24f016f89af33a87bab7e79f7196474e8"},
{file = "mysqlclient-2.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:60c9ed339dc09e3d5380cc2a9f42e86754fee25a661ced77a02df77990664c92"},
{file = "mysqlclient-2.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:000c7ec3d11e7c411db832e4cfcd7f05db47464326381f5d5ae991b4bb572f93"},
{file = "mysqlclient-2.2.8-cp313-cp313-win_amd64.whl", hash = "sha256:a81f5e12f8d05439709cb02fba97f9f76d1a6c528164f2260d8798fec969e300"},
{file = "mysqlclient-2.2.8-cp314-cp314-win_amd64.whl", hash = "sha256:9bed7c8d3b629bdc09e17fb628d5b3b0a5fd1f12b09432b464b9126c727bedc0"},
{file = "mysqlclient-2.2.8-cp314-cp314t-win_amd64.whl", hash = "sha256:260cce0e81446c83bf0a389e0fae38d68547d9f8fc0833bc733014e10ce28a99"},
{file = "mysqlclient-2.2.8.tar.gz", hash = "sha256:8ed20c5615a915da451bb308c7d0306648a4fd9a2809ba95c992690006306199"},
]
[[package]]
name = "nest-asyncio"
version = "1.6.0"
description = "Patch asyncio to allow nested event loops"
optional = false
python-versions = ">=3.5"
groups = ["dev"]
files = [
{file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"},
{file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"},
]
[[package]]
name = "nodeenv"
version = "1.9.1"
description = "Node.js virtual environment builder"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
files = [
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
]
[[package]]
name = "orjson"
version = "3.11.9"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "orjson-3.11.9-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce"},
{file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd"},
{file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4"},
{file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4"},
{file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e"},
{file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb"},
{file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47"},
{file = "orjson-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d"},
{file = "orjson-3.11.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13"},
{file = "orjson-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92"},
{file = "orjson-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48"},
{file = "orjson-3.11.9-cp310-cp310-win32.whl", hash = "sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94"},
{file = "orjson-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244"},
{file = "orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f"},
{file = "orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c"},
{file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5"},
{file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c"},
{file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd"},
{file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62"},
{file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877"},
{file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1"},
{file = "orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd"},
{file = "orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff"},
{file = "orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980"},
{file = "orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2"},
{file = "orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180"},
{file = "orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02"},
{file = "orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697"},
{file = "orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49"},
{file = "orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291"},
{file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09"},
{file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4"},
{file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882"},
{file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff"},
{file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe"},
{file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61"},
{file = "orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2"},
{file = "orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206"},
{file = "orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f"},
{file = "orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa"},
{file = "orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470"},
{file = "orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be"},
{file = "orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624"},
{file = "orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021"},
{file = "orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c"},
{file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81"},
{file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a"},
{file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10"},
{file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362"},
{file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97"},
{file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218"},
{file = "orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9"},
{file = "orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677"},
{file = "orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4"},
{file = "orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0"},
{file = "orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32"},
{file = "orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979"},
{file = "orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254"},
{file = "orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e"},
{file = "orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e"},
{file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0"},
{file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124"},
{file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c"},
{file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7"},
{file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1"},
{file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db"},
{file = "orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b"},
{file = "orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972"},
{file = "orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0"},
{file = "orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586"},
{file = "orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673"},
{file = "orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b"},
{file = "orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9"},
{file = "orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f"},
]
[[package]]
name = "ormar-utils"
version = "0.2.0"
description = "Rust-accelerated utility functions for the ormar ORM"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "ormar_utils-0.2.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e164f5d3636a660cec8df91cc1a0e2159bbb429f55d990272c096b1eef9d6c59"},
{file = "ormar_utils-0.2.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:40f3d45737017309cea3c62569394cc56e4c1558f9926f1dded580eeb6f730bb"},
{file = "ormar_utils-0.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93299eda3e3a72fdbf5ff4ce8c82ff801247bba616debc52dbb48915480d3ce"},
{file = "ormar_utils-0.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55c7ec5e002b0cc5394830c2b8c109a5b492d518fc95367dcf6b1da57c8510e4"},
{file = "ormar_utils-0.2.0-cp310-abi3-win_amd64.whl", hash = "sha256:122c7dd9f2eb52342be4862beda0737f98e7813c01751793f0fa53715f5b3959"},
{file = "ormar_utils-0.2.0.tar.gz", hash = "sha256:e2bf1ff71fbb820cac192b33583d58fac29e838a79804995336ef2bee02cc9ae"},
]
[[package]]
name = "packaging"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
name = "paginate"
version = "0.5.7"
description = "Divides large result sets into pages for easier browsing"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"},
{file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"},
]
[package.extras]
dev = ["pytest", "tox"]
lint = ["black"]
[[package]]
name = "pathspec"
version = "1.0.4"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"},
{file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"},
]
[package.extras]
hyperscan = ["hyperscan (>=0.7)"]
optional = ["typing-extensions (>=4)"]
re2 = ["google-re2 (>=1.1)"]
tests = ["pytest (>=9)", "typing-extensions (>=4.15)"]
[[package]]
name = "platformdirs"
version = "4.3.6"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
]
[package.extras]
docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
type = ["mypy (>=1.11.2)"]
[[package]]
name = "pluggy"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pre-commit"
version = "4.6.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b"},
{file = "pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9"},
]
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]]
name = "properdocs"
version = "1.6.7"
description = "Project documentation with Markdown."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "properdocs-1.6.7-py3-none-any.whl", hash = "sha256:6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd"},
{file = "properdocs-1.6.7.tar.gz", hash = "sha256:adc7b16e562890af0e098a7e5b02e3a81c20894a87d6a28d345c9300de73c26e"},
]
[package.dependencies]
click = ">=7.0"
colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
ghp-import = ">=1.0"
jinja2 = ">=2.11.1"
markdown = ">=3.3.6"
markupsafe = ">=2.0.1"
packaging = ">=20.5"
pathspec = ">=0.11.1"
platformdirs = ">=2.2.0"
pyyaml = ">=5.1"
pyyaml-env-tag = ">=0.1"
watchdog = ">=2.0"
[package.extras]
i18n = ["babel (>=2.9.0)"]
[[package]]
name = "psycopg2-binary"
version = "2.9.12"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
optional = true
python-versions = ">=3.9"
groups = ["main"]
markers = "extra == \"aiopg\" or extra == \"all\" or extra == \"postgresql\" or extra == \"postgres\""
files = [
{file = "psycopg2_binary-2.9.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4"},
{file = "psycopg2_binary-2.9.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56"},
{file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433"},
{file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e"},
{file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3"},
{file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4"},
{file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256"},
{file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c"},
{file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463"},
{file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1"},
{file = "psycopg2_binary-2.9.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03"},
{file = "psycopg2_binary-2.9.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b"},
{file = "psycopg2_binary-2.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326"},
{file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06"},
{file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8"},
{file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3"},
{file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39"},
{file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c"},
{file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a"},
{file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c"},
{file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be"},
{file = "psycopg2_binary-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936"},
{file = "psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433"},
{file = "psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6"},
{file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580"},
{file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f"},
{file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d"},
{file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9"},
{file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d"},
{file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e"},
{file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6"},
{file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b"},
{file = "psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c"},
{file = "psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965"},
{file = "psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7"},
{file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777"},
{file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5"},
{file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9"},
{file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019"},
{file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c"},
{file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f"},
{file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be"},
{file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290"},
{file = "psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0"},
{file = "psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2"},
{file = "psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2"},
{file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f"},
{file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354"},
{file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033"},
{file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e"},
{file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5"},
{file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5"},
{file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d"},
{file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd"},
{file = "psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980"},
{file = "psycopg2_binary-2.9.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ee2d84ef5eb6c04702d2e9c372ad557fb027f26a5d82804f749dfb14c7fdd2ab"},
{file = "psycopg2_binary-2.9.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cfa2517c94ea3af6deb46f81e1bbd884faa63e28481eb2f889989dd8d95e5f03"},
{file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ba3df2fc42a1cfa45b72cf096d4acb2b885937eedc61461081d53538d4a82a86"},
{file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:718e1fc18edf573b02cb8aea868de8d8d33f99ce9620206aa9144b67b0985e94"},
{file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c7cb4cbf894a1d36c720d713de507952c7c58f66d30834708f03dbe5c822ccf"},
{file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:049366c6d884bdcd65d66e6ca1fdbebe670b56c6c9ba46f164e6667e90881964"},
{file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fb1828cf3da68f99e45ebce1355d65d2d12b6a78fb5dfb16247aad6bdef5f5d2"},
{file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:127467c6e476dd876634f17c3d870530e73ff454ff99bff73d36e80af28e1115"},
{file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ace94261f43850e9e79f6c56636c5e0147978ab79eda5e5e5ebf13ae146fc8fe"},
{file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a7e39a65b7d2a20e4ba2e0aaad1960b61cc2888d6ab047769f8347bd3c9ad915"},
{file = "psycopg2_binary-2.9.12-cp39-cp39-win_amd64.whl", hash = "sha256:f625abb7020e4af3432d95342daa1aa0db3fa369eed19807aa596367ba791b10"},
{file = "psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c"},
]
[[package]]
name = "py-cpuinfo"
version = "9.0.0"
description = "Get CPU info with pure Python"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"},
{file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"},
]
[[package]]
name = "pycparser"
version = "2.22"
description = "C parser in Python"
optional = true
python-versions = ">=3.8"
groups = ["main"]
markers = "(extra == \"crypto\" or extra == \"all\") and platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
[[package]]
name = "pydantic"
version = "2.12.5"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"},
{file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
pydantic-core = "2.41.5"
typing-extensions = ">=4.14.1"
typing-inspection = ">=0.4.2"
[package.extras]
email = ["email-validator (>=2.0.0)"]
timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
[[package]]
name = "pydantic-core"
version = "2.41.5"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"},
{file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"},
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"},
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"},
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"},
{file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"},
{file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"},
{file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"},
{file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"},
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"},
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"},
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"},
{file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"},
{file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"},
{file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"},
{file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"},
{file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"},
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"},
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"},
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"},
{file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"},
{file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"},
{file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"},
{file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"},
{file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"},
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"},
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"},
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"},
{file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"},
{file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"},
{file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"},
{file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"},
{file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"},
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"},
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"},
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"},
{file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"},
{file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"},
{file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"},
{file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"},
{file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"},
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"},
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"},
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"},
{file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"},
{file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"},
{file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"},
{file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"},
{file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"},
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"},
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"},
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"},
{file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"},
{file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"},
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"},
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"},
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"},
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"},
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"},
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"},
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"},
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"},
{file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"},
]
[package.dependencies]
typing-extensions = ">=4.14.1"
[[package]]
name = "pydantic-extra-types"
version = "2.11.2"
description = "Extra Pydantic types."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pydantic_extra_types-2.11.2-py3-none-any.whl", hash = "sha256:683b8943252543e49760f89733b1519bc62f31d1a287ebbdc5a7b7959fb4acfd"},
{file = "pydantic_extra_types-2.11.2.tar.gz", hash = "sha256:3a2b83b61fe920925688e7838b59caa90a45637d1dbba2b1364b8d1f7ff72a0a"},
]
[package.dependencies]
pydantic = ">=2.5.2"
typing-extensions = "*"
[package.extras]
all = ["cron-converter (>=1.2.2)", "jsonschema (>=4.0.0)", "pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<10)", "pycountry (>=23)", "pymongo (>=4.0.0,<5.0.0)", "python-ulid (>=1,<2) ; python_version < \"3.9\"", "python-ulid (>=1,<4) ; python_version >= \"3.9\"", "pytz (>=2024.1)", "semver (>=3.0.2)", "semver (>=3.0.2,<3.1.0)", "tzdata (>=2024.1)", "uuid-utils (>=0.6.0) ; python_version < \"3.14\""]
cron = ["cron-converter (>=1.2.2)"]
jsonschema = ["jsonschema (>=4.0.0)"]
pendulum = ["pendulum (>=3.0.0,<4.0.0)"]
phonenumbers = ["phonenumbers (>=8,<10)"]
pycountry = ["pycountry (>=23)"]
python-ulid = ["python-ulid (>=1,<2) ; python_version < \"3.9\"", "python-ulid (>=1,<4) ; python_version >= \"3.9\""]
semver = ["semver (>=3.0.2)"]
uuid-utils = ["uuid-utils (>=0.6.0) ; python_version < \"3.14\""]
[[package]]
name = "pygments"
version = "2.20.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pymdown-extensions"
version = "10.21.3"
description = "Extension pack for Python Markdown."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6"},
{file = "pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354"},
]
[package.dependencies]
markdown = ">=3.6"
pyyaml = "*"
[package.extras]
extra = ["pygments (>=2.19.1)"]
[[package]]
name = "pymysql"
version = "1.2.0"
description = "Pure Python MySQL Driver"
optional = true
python-versions = ">=3.9"
groups = ["main"]
markers = "extra == \"mysql\" or extra == \"all\""
files = [
{file = "pymysql-1.2.0-py3-none-any.whl", hash = "sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0"},
{file = "pymysql-1.2.0.tar.gz", hash = "sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33"},
]
[package.extras]
ed25519 = ["PyNaCl (>=1.6.2)"]
rsa = ["cryptography (>=46.0.7)"]
[[package]]
name = "pyparsing"
version = "3.1.4"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
optional = false
python-versions = ">=3.6.8"
groups = ["dev"]
files = [
{file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"},
{file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"},
]
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pytest"
version = "9.0.3"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"},
{file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"},
]
[package.dependencies]
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""}
iniconfig = ">=1.0.1"
packaging = ">=22"
pluggy = ">=1.5,<2"
pygments = ">=2.7.2"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-asyncio"
version = "1.4.0"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"},
{file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"},
]
[package.dependencies]
backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""}
pytest = ">=8.4,<10"
typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""}
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]]
name = "pytest-benchmark"
version = "5.2.3"
description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803"},
{file = "pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779"},
]
[package.dependencies]
py-cpuinfo = "*"
pytest = ">=8.1"
[package.extras]
aspect = ["aspectlib"]
elasticsearch = ["elasticsearch"]
histogram = ["pygal", "pygaljs", "setuptools"]
[[package]]
name = "pytest-codspeed"
version = "5.0.3"
description = "Pytest plugin to create CodSpeed benchmarks"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pytest_codspeed-5.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:005348ea52ace3ede2e2f595913912ad2564cca7b124211a88dc78a9cb1fca63"},
{file = "pytest_codspeed-5.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbe6a4a00b449b6ba2771f644cbc38bdf55acf5c812e60e5659110e19dd9f510"},
{file = "pytest_codspeed-5.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac4344f34bbcdd17f6f8c30dbac3da2f80d223dd112e568fd7f7c2cd4cbc693"},
{file = "pytest_codspeed-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f56d0339cd98d26f6e561987be25bdd2761a5d53d8f73493b1ebe02d0d451093"},
{file = "pytest_codspeed-5.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c682f6645d4eb472f3bd95dbda1805e3af4243610572cb7d6bf94a88e8a0b6c"},
{file = "pytest_codspeed-5.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f852bee785a7a124cb1720b1915670c6742af87747dc4d838f3ffdbd365ce9d9"},
{file = "pytest_codspeed-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2eeb25fb1ac3f73c4de50e739e78fea396b89782bdb740bf2a7cd2df21f8d4ee"},
{file = "pytest_codspeed-5.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73c5c9d98a3372a42611989ccfa437cce3842431ac6d6b9ab42c4f0e59c070f7"},
{file = "pytest_codspeed-5.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2e0ab65df73e837666d12357280ca50ff6d6ac03ea5266703be518b68170edf"},
{file = "pytest_codspeed-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6524c57fec279a22ffef6112af404036afc71b4704758ae9f0abda429b8478d4"},
{file = "pytest_codspeed-5.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c383c9121deb58a69f174188e9e4488ffc0daced0ed276abf87747182511901"},
{file = "pytest_codspeed-5.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4bcdb4b6522738152885ef067e0c8524d5699828d780fb6f464cdb3db44369c"},
{file = "pytest_codspeed-5.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:25464363c7f9b9bd5022e969c0addba616fa40ac9b8f0fc9e030c4538863b32d"},
{file = "pytest_codspeed-5.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efd43f82ea03ced8488a767ded9473f050791ab7783ea8654107e1e0ac66af40"},
{file = "pytest_codspeed-5.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:782f9985b6f6b45b8bc20152d206d3a52b56dd088ba81cb70a71f0b39841be9e"},
{file = "pytest_codspeed-5.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9aa0815b90196f3c20d736ea8691381e97f12bbe8c7d87af10a351e434b452cb"},
{file = "pytest_codspeed-5.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85505c96a3477c346ec2d2b7dced8478f4c651e2b1666ee102d53a832b511853"},
{file = "pytest_codspeed-5.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20eba63765be9d1b6cacbbfad84b87d49eb04b357a7045a0899880da181f81e3"},
{file = "pytest_codspeed-5.0.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ec9fa6f0af0a9feb0e0bd517fb59ef28f806fbd50c0c6900ac26cbb4d080eba5"},
{file = "pytest_codspeed-5.0.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8df77b3409f54f4a268f77f3ff74992fe1d995cdbaf2cecf8ad74d32db217ce7"},
{file = "pytest_codspeed-5.0.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5d8695a227ea1c3a41d25db5b3fe720bf1b4808bd38862be811a4efd902c792"},
{file = "pytest_codspeed-5.0.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bf4cc4178cbace8f4d2bd240408276bc4da3850ac5fcb5fb5f8a74ab417615bb"},
{file = "pytest_codspeed-5.0.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abe793da40f87295d33988673d34f06ea569848b44490b847552cd416816258a"},
{file = "pytest_codspeed-5.0.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3a9ed38dfa776443b86f4b49a982e8443d0953db4974bd2673d63cc904ae1ad"},
{file = "pytest_codspeed-5.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bce0a6ea93a5b19658f713312bb67554c19283ab15b454a1e3e55a13e78130f8"},
{file = "pytest_codspeed-5.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a2097247985f5d915a94b80c5552d10979ca858c859fc3edef1bf2baa5c9b7a"},
{file = "pytest_codspeed-5.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e192905a2230f9956e6160732f76577836953a4a1fb2b1e7be74e51ac7b2a0"},
{file = "pytest_codspeed-5.0.3-py3-none-any.whl", hash = "sha256:fe2ea83c924c2250675b75686c3ee456b8cf0208d83d552e182a195fdf467378"},
{file = "pytest_codspeed-5.0.3.tar.gz", hash = "sha256:91afef90e6a96b013495e4702ef5d6358614a449e71008cdc194ef668778b92f"},
]
[package.dependencies]
pytest = ">=3.8"
rich = ">=13.8.1"
[package.extras]
compat = ["pytest-benchmark (>=5.0.0,<5.1.0)", "pytest-xdist (>=3.6.1,<3.7.0)"]
[[package]]
name = "pytest-cov"
version = "7.1.0"
description = "Pytest plugin for measuring coverage."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"},
{file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"},
]
[package.dependencies]
coverage = {version = ">=7.10.6", extras = ["toml"]}
pluggy = ">=1.2"
pytest = ">=7"
[package.extras]
testing = ["process-tests", "pytest-xdist", "virtualenv"]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
groups = ["dev"]
files = [
{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]
six = ">=1.5"
[[package]]
name = "pyyaml"
version = "6.0.2"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
{file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
{file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
{file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
{file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
{file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
{file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
{file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
{file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
{file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
{file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
{file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
{file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
{file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
{file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
]
[[package]]
name = "pyyaml-env-tag"
version = "0.1"
description = "A custom YAML tag for referencing environment variables in YAML files. "
optional = false
python-versions = ">=3.6"
groups = ["dev"]
files = [
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
]
[package.dependencies]
pyyaml = "*"
[[package]]
name = "requests"
version = "2.33.0"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b"},
{file = "requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652"},
]
[package.dependencies]
certifi = ">=2023.5.7"
charset_normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.26,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
test = ["PySocks (>=1.5.6,!=1.5.7)", "pytest (>=3)", "pytest-cov", "pytest-httpbin (==2.1.0)", "pytest-mock", "pytest-xdist"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
[[package]]
name = "rich"
version = "14.2.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.8.0"
groups = ["dev"]
files = [
{file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"},
{file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"},
]
[package.dependencies]
markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.15.16"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2"},
{file = "ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a"},
{file = "ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071"},
{file = "ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf"},
{file = "ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d"},
{file = "ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628"},
{file = "ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb"},
{file = "ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4"},
{file = "ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb"},
{file = "ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951"},
{file = "ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8"},
{file = "ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123"},
{file = "ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a"},
{file = "ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3"},
{file = "ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e"},
{file = "ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec"},
{file = "ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121"},
{file = "ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78"},
]
[[package]]
name = "setuptools"
version = "82.0.1"
description = "Most extensible Python build backend with support for C/C++ extension modules"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"},
{file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"},
]
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""]
core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"]
[[package]]
name = "six"
version = "1.17.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
groups = ["dev"]
files = [
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
]
[[package]]
name = "sniffio"
version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{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 = "2.0.50"
description = "Database Abstraction Library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "sqlalchemy-2.0.50-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7af6eeb84985bf840ba779018ff9424d61ff69b52e66b8789d3c8da7bf5341b2"},
{file = "sqlalchemy-2.0.50-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fe7822866f3a9fc5f3db21a290ce8961a53050115f05edf9402b6a5feb92a9f"},
{file = "sqlalchemy-2.0.50-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e1b0f6a4dcd9b4839e2320afb5df37a6981cbc20ff9c423ae11c5537bdbd21"},
{file = "sqlalchemy-2.0.50-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e195687f1af431c9515416288373b323b6eb599f774409814e89e9d603a56e39"},
{file = "sqlalchemy-2.0.50-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ea1a8a2db4b2217d456c8d7a873bfc605f06fe3584d315264ea18c2a17585d0b"},
{file = "sqlalchemy-2.0.50-cp310-cp310-win32.whl", hash = "sha256:68b154b08088b4ec32bb4d2958bfbb50e57549f91a4cd3e7f928e3553ed69031"},
{file = "sqlalchemy-2.0.50-cp310-cp310-win_amd64.whl", hash = "sha256:66e374271ecb7101273f57af1a62446a953d327eec4f8089147de57c591bbacc"},
{file = "sqlalchemy-2.0.50-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa6e403663a9c43c8fef7ce4bdb4cf48bcd8d352e91deda2a99f963270bd508"},
{file = "sqlalchemy-2.0.50-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51b637a84f9fa35ae1f9017e786cb142974a25305085e1b378b3647a67f65ad3"},
{file = "sqlalchemy-2.0.50-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2dab927761d9108550f0cf8e66ff21af56f907a0ce0a689793db615e2b55f62c"},
{file = "sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:545eae198d37bcf837a10ede3684e2af32458d6f35c597c35c2de7502dc38fc4"},
{file = "sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fec460e18cdbb4c7773531122ce9a27e96c6ca17af3933941d94da475ad2c86"},
{file = "sqlalchemy-2.0.50-cp311-cp311-win32.whl", hash = "sha256:e6e814658818fd165e749e3d8490ef16cc7f379a118c37ada8b0589ffbaaac22"},
{file = "sqlalchemy-2.0.50-cp311-cp311-win_amd64.whl", hash = "sha256:1c5f858fe79c9f5d8fda065c06186356acb7f8df3cd52dbd5ee3f200e4b144f5"},
{file = "sqlalchemy-2.0.50-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae23d8b9d344d30d0a92f06d45825024a5790f1c1dd4cf452636a50d3e58cb"},
{file = "sqlalchemy-2.0.50-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47b71b933e7b4ebad407c8fdfd70d2c4f08b78b3238bb30eebdd6eb32ca51b89"},
{file = "sqlalchemy-2.0.50-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:110fdac56ace278949f00de805edacbd6141e382d992f9ba28238b3a0827a600"},
{file = "sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5e4ac70e9e757f6b3e87c0491ff034442ecd8dfd36d041a50564c322dafc0e"},
{file = "sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724f3dcbe53dd0151e3cb5e7ec4ba4c620bede579caacd16275dc35ce06e8615"},
{file = "sqlalchemy-2.0.50-cp312-cp312-win32.whl", hash = "sha256:1208050441471d003b7c8cb4054fb084f185cf35ac3f0ea270803865bca9939a"},
{file = "sqlalchemy-2.0.50-cp312-cp312-win_amd64.whl", hash = "sha256:9d1af51558029a156a70986b7df88f042b3d158d7c8d8fb5072912d4b32d89c7"},
{file = "sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093"},
{file = "sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873"},
{file = "sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db"},
{file = "sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064"},
{file = "sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f"},
{file = "sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5"},
{file = "sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3"},
{file = "sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0"},
{file = "sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb"},
{file = "sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e"},
{file = "sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d"},
{file = "sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f"},
{file = "sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8"},
{file = "sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39"},
{file = "sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70"},
{file = "sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086"},
{file = "sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52"},
{file = "sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a"},
{file = "sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d"},
{file = "sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e"},
{file = "sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51"},
{file = "sqlalchemy-2.0.50-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8e8af330cbb3a1931d3d6c91b239fc2ef135f7dd471dfa34c575028e0b1fa8"},
{file = "sqlalchemy-2.0.50-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eefd9a03cc0047b14153872d228499d048bd7deaf926109c9ec25b15157b8e23"},
{file = "sqlalchemy-2.0.50-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b85b20f9ab714a666df9d8e72e253ec33c16c7e1e375c877e5bf6367a3e917"},
{file = "sqlalchemy-2.0.50-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:27b7062af702c61994e8806ad87e42d0a2c879e0a8e5c61c7f69d81dabe24fdf"},
{file = "sqlalchemy-2.0.50-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2c1920cde9d741ba3dda9b1aa5acd8c23ea17780ccfb2252d01878d5d0d628d3"},
{file = "sqlalchemy-2.0.50-cp38-cp38-win32.whl", hash = "sha256:7b1ddb7b5fc60dfa9df6a487f06a143c77def47c0351849da2bcea59b244a56c"},
{file = "sqlalchemy-2.0.50-cp38-cp38-win_amd64.whl", hash = "sha256:0e104e196f457ec608eb8af736c5eb4c6bc58f481b546f485a7f9c628ee532be"},
{file = "sqlalchemy-2.0.50-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:409a8121b917116b035bedc5e532ad470c74a2d279f6c302100985b6304e9f9e"},
{file = "sqlalchemy-2.0.50-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9602c07b03e1449747ecb69f9998a7194a589124475788b370adce57c9e9a56e"},
{file = "sqlalchemy-2.0.50-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d10700bd519573f6ce5badbabbfe7f5baea84cdf370f2cbbfb4be28dfddbf1d"},
{file = "sqlalchemy-2.0.50-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7e36efdcc5493f8024ec873a4ee3855bfd2de0c5b19eba16f920e9d2a0d28622"},
{file = "sqlalchemy-2.0.50-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:adc0fe7d38d8c8058f7421c25508fcbc74df38233a42aa8324409844122dce8f"},
{file = "sqlalchemy-2.0.50-cp39-cp39-win32.whl", hash = "sha256:0a31c5963d58d3e3d11c5b97709e248305705de1fdf51ec3bf396674c5898b7e"},
{file = "sqlalchemy-2.0.50-cp39-cp39-win_amd64.whl", hash = "sha256:83a9fce296b7e052316d8c6943237b31b9c00f58ca9c253f2d165df52637a293"},
{file = "sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9"},
{file = "sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9"},
]
[package.dependencies]
greenlet = {version = ">=1", optional = true, markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\" or extra == \"asyncio\""}
typing-extensions = ">=4.6.0"
[package.extras]
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"]
aioodbc = ["aioodbc", "greenlet (>=1)"]
aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"]
asyncio = ["greenlet (>=1)"]
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"]
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"]
mssql = ["pyodbc"]
mssql-pymssql = ["pymssql"]
mssql-pyodbc = ["pyodbc"]
mypy = ["mypy (>=0.910)"]
mysql = ["mysqlclient (>=1.4.0)"]
mysql-connector = ["mysql-connector-python"]
oracle = ["cx_oracle (>=8)"]
oracle-oracledb = ["oracledb (>=1.0.1)"]
postgresql = ["psycopg2 (>=2.7)"]
postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"]
postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
postgresql-psycopg = ["psycopg (>=3.0.7)"]
postgresql-psycopg2binary = ["psycopg2-binary"]
postgresql-psycopg2cffi = ["psycopg2cffi"]
postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
pymysql = ["pymysql"]
sqlcipher = ["sqlcipher3_binary"]
[[package]]
name = "starlette"
version = "0.49.1"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875"},
{file = "starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb"},
]
[package.dependencies]
anyio = ">=3.6.2,<5"
typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""}
[package.extras]
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
[[package]]
name = "tomli"
version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_full_version <= \"3.11.0a6\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
{file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
{file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
{file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
{file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
{file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
{file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
{file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
{file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
{file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
{file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
]
[[package]]
name = "types-aiofiles"
version = "25.1.0.20260518"
description = "Typing stubs for aiofiles"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "types_aiofiles-25.1.0.20260518-py3-none-any.whl", hash = "sha256:f776bdfb4bec17f743d9ef042e61edf03bdcc7821fc08556fba9b63d873fdea9"},
{file = "types_aiofiles-25.1.0.20260518.tar.gz", hash = "sha256:c0c95eb78755d4fa7b397d4f0332c632714dd7cd0d17f49b96e31d4d7a8d8c76"},
]
[[package]]
name = "types-cryptography"
version = "3.3.23.2"
description = "Typing stubs for cryptography"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "types-cryptography-3.3.23.2.tar.gz", hash = "sha256:09cc53f273dd4d8c29fa7ad11fefd9b734126d467960162397bc5e3e604dea75"},
{file = "types_cryptography-3.3.23.2-py3-none-any.whl", hash = "sha256:b965d548f148f8e87f353ccf2b7bd92719fdf6c845ff7cedf2abb393a0643e4f"},
]
[[package]]
name = "types-enum34"
version = "1.1.8"
description = "Typing stubs for enum34"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "types-enum34-1.1.8.tar.gz", hash = "sha256:6f9c769641d06d73a55e11c14d38ac76fcd37eb545ce79cebb6eec9d50a64110"},
{file = "types_enum34-1.1.8-py3-none-any.whl", hash = "sha256:05058c7a495f6bfaaca0be4aeac3cce5cdd80a2bad2aab01fd49a20bf4a0209d"},
]
[[package]]
name = "types-ipaddress"
version = "1.0.8"
description = "Typing stubs for ipaddress"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "types-ipaddress-1.0.8.tar.gz", hash = "sha256:a03df3be5935e50ba03fa843daabff539a041a28e73e0fce2c5705bee54d3841"},
{file = "types_ipaddress-1.0.8-py3-none-any.whl", hash = "sha256:4933b74da157ba877b1a705d64f6fa7742745e9ffd65e51011f370c11ebedb55"},
]
[[package]]
name = "types-orjson"
version = "3.6.2"
description = "Typing stubs for orjson"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "types-orjson-3.6.2.tar.gz", hash = "sha256:cf9afcc79a86325c7aff251790338109ed6f6b1bab09d2d4262dd18c85a3c638"},
{file = "types_orjson-3.6.2-py3-none-any.whl", hash = "sha256:22ee9a79236b6b0bfb35a0684eded62ad930a88a56797fa3c449b026cf7dbfe4"},
]
[[package]]
name = "types-pymysql"
version = "1.1.0.20260518"
description = "Typing stubs for PyMySQL"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "types_pymysql-1.1.0.20260518-py3-none-any.whl", hash = "sha256:cf697ce4e44124fc859e8e8a7f047c1dc864745c3c628b85a51b3ee01502ef98"},
{file = "types_pymysql-1.1.0.20260518.tar.gz", hash = "sha256:39a2448c4267dc4551e0824d2bfaecf7dfd171e89e6dbba90f4d4d45d55e4342"},
]
[[package]]
name = "types-requests"
version = "2.33.0.20260518"
description = "Typing stubs for requests"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0"},
{file = "types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e"},
]
[package.dependencies]
urllib3 = ">=2"
[[package]]
name = "types-toml"
version = "0.10.8.20260518"
description = "Typing stubs for toml"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "types_toml-0.10.8.20260518-py3-none-any.whl", hash = "sha256:0e564ab05f6fde62a315b3b5a9b6624fda569399795d30a37e64705a70459303"},
{file = "types_toml-0.10.8.20260518.tar.gz", hash = "sha256:80e10facd24fdeda9d5c672187d72be3ac284843788d67f5aae59e3e016db6fe"},
]
[[package]]
name = "types-ujson"
version = "5.10.0.20250822"
description = "Typing stubs for ujson"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "types_ujson-5.10.0.20250822-py3-none-any.whl", hash = "sha256:3e9e73a6dc62ccc03449d9ac2c580cd1b7a8e4873220db498f7dd056754be080"},
{file = "types_ujson-5.10.0.20250822.tar.gz", hash = "sha256:0a795558e1f78532373cf3f03f35b1f08bc60d52d924187b97995ee3597ba006"},
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
description = "Runtime typing introspection tools"
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"},
{file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"},
]
[package.dependencies]
typing-extensions = ">=4.12.0"
[[package]]
name = "tzdata"
version = "2025.3"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
groups = ["dev"]
markers = "platform_system == \"Windows\""
files = [
{file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"},
{file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"},
]
[[package]]
name = "urllib3"
version = "2.7.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
{file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
]
[package.extras]
brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[[package]]
name = "verspec"
version = "0.1.0"
description = "Flexible version handling"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31"},
{file = "verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e"},
]
[package.extras]
test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"]
[[package]]
name = "virtualenv"
version = "20.36.1"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"},
{file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"},
]
[package.dependencies]
distlib = ">=0.3.7,<1"
filelock = {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""}
platformdirs = ">=3.9.1,<5"
typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""}
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""]
[[package]]
name = "watchdog"
version = "6.0.0"
description = "Filesystem events monitoring"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"},
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"},
{file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"},
{file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"},
{file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"},
{file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"},
{file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"},
{file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"},
{file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"},
{file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"},
{file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"},
{file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"},
{file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"},
{file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"},
{file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"},
{file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"},
{file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"},
{file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"},
{file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"},
{file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"},
{file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"},
{file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"},
{file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"},
{file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"},
]
[package.extras]
watchmedo = ["PyYAML (>=3.10)"]
[[package]]
name = "yappi"
version = "1.7.6"
description = "Yet Another Python Profiler"
optional = false
python-versions = ">=3.6"
groups = ["dev"]
files = [
{file = "yappi-1.7.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90ba3317c5d58b1da592f6368658776e9401abd2cc39aef8a11e4e220fdbd4ab"},
{file = "yappi-1.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:13dec55a9fe794754471109bab7919ab251296d41ca1ede6e4bb94cb4437916d"},
{file = "yappi-1.7.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e67dba03d83408ac2a1f32343b5b0eea0b0758d9c76091d24f7265eb3a57cbdc"},
{file = "yappi-1.7.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7a7dcf4ddfa2be4e08f543241320855bfa90c5c566d68ffb60980f07e347226"},
{file = "yappi-1.7.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cca3d18602d0f9d3ed3529dc3117a006a0c772c86c780c7842438ae8c62e9688"},
{file = "yappi-1.7.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:343d7c74ff93389d89ef448f91afde80eb3411c81aa0711682342d6933bf006f"},
{file = "yappi-1.7.6-cp310-cp310-win32.whl", hash = "sha256:91363676076f7361db7e9762c64f330d0a25d93904b036afe1af09a507658c83"},
{file = "yappi-1.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:e6494b59c04c6c16d35bb44df0f625e738a9632f644236015660b1a20e39db81"},
{file = "yappi-1.7.6-cp310-cp310-win_arm64.whl", hash = "sha256:21e0347a8cf2dcda5f013dacf60b3b887d5aba0bae77b3d4ada51e85a2f5069a"},
{file = "yappi-1.7.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d6b52ebe13f05c4845df803aca02ea209cb6de71b5e16a26a938543d9df4342"},
{file = "yappi-1.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4b62efda1ca0b820985ae31f5081fa8250307f45d5905ba78b19c558fccd9e2"},
{file = "yappi-1.7.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8aa1f8983463d064cdd28709f76c5886bc1417607f075fc326e605faa44e7f04"},
{file = "yappi-1.7.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:757199a1d4e8b3f27656b69612d6db99fb06df6e25dc3b37a01b11e564b135fe"},
{file = "yappi-1.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:84fb5444b1e10c66f34fc65fdaa461dbce703865925d5d81604e614e775f9c24"},
{file = "yappi-1.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322bdfe2693492c226c31fcb0c197a203a0ff8e65e0594882c4d6061a1184b49"},
{file = "yappi-1.7.6-cp311-cp311-win32.whl", hash = "sha256:380d6b49d5d62df60c022c7c510dc56cac688f2329cda07954a0d99edeba644c"},
{file = "yappi-1.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:d8721d2137155880eaf851b0a1bc9ad3e9a3c175e28869a87e8abd22d2b12029"},
{file = "yappi-1.7.6-cp311-cp311-win_arm64.whl", hash = "sha256:b6a4e5b7c813aa147ddc6a12f660de01829da184d760095dd3609cf1059b64e3"},
{file = "yappi-1.7.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56fae31c4e09448a9919c1e6a4f976b2a49aa914f42e6e95355f6329c83003da"},
{file = "yappi-1.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:15ed0d845e30b35952d09dd4f70df81089db05b735d57c75e0924ebacda14a34"},
{file = "yappi-1.7.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e878f63781761db7b62265468d78147b95df9ca2af9bc5140f94ad0faffe11c"},
{file = "yappi-1.7.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a88615e2b9817887f6d1addfd12466a8529f25acc58b656205ae3f641cd725b"},
{file = "yappi-1.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:178b23a56a59ddd58a528848e9242040ecad6d2fe0bcfb439455eef02cd43b07"},
{file = "yappi-1.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62cbb47dffca45b906d52a3c8f02e508f67d657275fb9d897e1e736fe5afe25f"},
{file = "yappi-1.7.6-cp312-cp312-win32.whl", hash = "sha256:dbcf79ee2f1a96ec52e8291c07e27c0e38eead61a5c24d57eb467b5d9e6f2f9f"},
{file = "yappi-1.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:59bd23fb39a7b9027c5eecc94585042849cb36be9af2d35c31812be1408af356"},
{file = "yappi-1.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:0adc831b099a554831335819d7eb1643e189e4f3aa2db873ca5d584bd42dba01"},
{file = "yappi-1.7.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:072df6fa8b4cfb5159c261dd0df8e8b85de0adbadbc5e953e1183da193674bc4"},
{file = "yappi-1.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4643d431656ec63e83455605ba29d1609d36b2fe14412e6939a223c323a7aee"},
{file = "yappi-1.7.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b27541c7f77ef2f76b2e0bb5da6dce5dc5fcdc7e500b4756e7a3e077d499ac25"},
{file = "yappi-1.7.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e100b6c36b922fc407078ed74f08b2463f46efc1fb440387eb493966e4ec434"},
{file = "yappi-1.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5beecd15ff133c93fc505669754cb7caadd7fb19e87a71af133dfd1410e17aff"},
{file = "yappi-1.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3b5742d39c1ebe8909db0dec4a5b724a5a6167161864280021298f7ef4e76a1"},
{file = "yappi-1.7.6-cp313-cp313-win32.whl", hash = "sha256:c9e3a92a04d9d6199fa0d157139beff1ca7eea7389e0e6b46b1353d8ffeec6a3"},
{file = "yappi-1.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:95f9f326483d111b768f630a2d60689de7defff777f016b1f0dab9e93f36beb5"},
{file = "yappi-1.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:4981a243c5dbf105f6e1415197935ca36fde2b28adf26d2feceb95b5f1f77f06"},
{file = "yappi-1.7.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8bf3595e8c1c0326b8012591bc96b72625c7424d4d9fbe4b640b0aafd81f88dc"},
{file = "yappi-1.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e9b018df48bc061248ae1fc36e161e9b4fb2cbbc50a8a0dfb68b9db4608bc9da"},
{file = "yappi-1.7.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06c0487ab02e3a9722524c8d034feeadbdc2070d6530c38f7483291bf978b800"},
{file = "yappi-1.7.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dedd28687f48607db40874629a47bc93d16f1b9c93045f34961620bda76df9d7"},
{file = "yappi-1.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1e3ef62417c598474a359de6aef92e13ea623416bb0ff45fa4b97e6569120549"},
{file = "yappi-1.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2b44e7a3187290615877d039bb2f4e232e1b7a5858b314ef6b011bd90447b537"},
{file = "yappi-1.7.6-cp314-cp314-win32.whl", hash = "sha256:5d1d7ba37477da04cc1005784036a535ec5e053cfa09aec7d20e5bc436aedb8c"},
{file = "yappi-1.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:53b8b8b6ad4f42cb82107c9fa96d103de33f76785e0ce84f5a326e66efc80f64"},
{file = "yappi-1.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:b6a189c4b666933218d4bd4b7e1e22d03123120dcba3af4d6c2748ba7efba9ac"},
{file = "yappi-1.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e3cb10af86fa45e643308630ca52b8e9f0909f215ccb767403876439167b3b5"},
{file = "yappi-1.7.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:945daf6c86900cca1f8c449704c3d95f1d6c7f14285da2ade3bdd74cacdb24a0"},
{file = "yappi-1.7.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35687a345aaf41b89965b30d51aba6603bd738263ff76c0c0b538b4347988df2"},
{file = "yappi-1.7.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:853da78543d5e8c445e7bf313331b58da95b218b25741ae7734d77574d09ec0a"},
{file = "yappi-1.7.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:312b51f3325ecb68d4d5163f3b576bd841bf18368862fad1461c10eec720c477"},
{file = "yappi-1.7.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a91358c612022d35d49359ee3a9fc7a6f8b6a3b652852660501ddd7982b310c4"},
{file = "yappi-1.7.6-cp38-cp38-win32.whl", hash = "sha256:0a1b3317977e2614b1983ba814b0a56c0f21acd7e7dfc1e2ed7d59141895f3a3"},
{file = "yappi-1.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:4fc9f5b1a050cfc8e0829b7f0ca9522c4e9c153ee9f78c90447412af2deb4aa7"},
{file = "yappi-1.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98a1f975e94c6367a4dcdc4d56db8d5ae7384dd3580324b5e1efa59ed32b5c0a"},
{file = "yappi-1.7.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:767e2d290c887a4de253f5d51cbe64a5d76945d1f2ac79ef31462b8d6ad38835"},
{file = "yappi-1.7.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:becef89596237a9337cbd0e6bf24118dc5f81be0a22309c7d3fbb43b888a9bdc"},
{file = "yappi-1.7.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c57116c8325c734d87b19d165bf6f111b27b75de70e2a12416a1132dcf205e43"},
{file = "yappi-1.7.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d29db4473f8b7917dbb2c74458f1599448a6c8d9af0da2bedfa4700d6a8d5f3"},
{file = "yappi-1.7.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:da87ca817e6496c2eafebc3e3773e2f253062295cfe82682121c1934a6403063"},
{file = "yappi-1.7.6-cp39-cp39-win32.whl", hash = "sha256:718f0e1b51eef701663755850a6ae8d2d9e11bb34204692bdcc6da71e1c37813"},
{file = "yappi-1.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:587584ba6b21ec8b7839b4737fe08c970c19730e76329bd5207e591904df0cfc"},
{file = "yappi-1.7.6-cp39-cp39-win_arm64.whl", hash = "sha256:94286e4b18b0d06d4d0d5be1c9a19c1ec34d630ad2e216263de83f4bb303c8ec"},
{file = "yappi-1.7.6.tar.gz", hash = "sha256:c94281936af77c00c6ac2306a0e7f85a67e354d717120df85fcc5dfb9243d4dd"},
]
[package.extras]
test = ["gevent (>=20.6.2)"]
[extras]
aiopg = ["aiopg", "psycopg2-binary"]
all = ["PyMySQL", "aiomysql", "aiopg", "aiosqlite", "asyncpg", "cryptography", "mysqlclient", "psycopg2-binary"]
crypto = ["cryptography"]
mysql = ["PyMySQL", "aiomysql"]
postgres = ["asyncpg", "psycopg2-binary"]
postgresql = ["asyncpg", "psycopg2-binary"]
sqlite = ["aiosqlite"]
[metadata]
lock-version = "2.1"
python-versions = "^3.10.0"
content-hash = "ee20c99bf036c0659e078adcfc74a957a5569edf0f3abf1c6c60e46e4530d804"
ormar-orm-ormar-a7e7fde/pyproject.toml 0000664 0000000 0000000 00000011153 15211567312 0020247 0 ustar 00root root 0000000 0000000 [project]
name = "ormar"
[tool.poetry]
name = "ormar"
version = "0.26.0"
description = "An async ORM with fastapi in mind and pydantic validation."
authors = ["Radosław Drążkiewicz "]
license = "MIT"
readme = "README.md"
homepage = "https://github.com/ormar-orm/ormar"
repository = "https://github.com/ormar-orm/ormar"
documentation = "https://ormar-orm.github.io/ormar/"
packages = [
{ include="ormar" }
]
include = [
{ path = "tests/**/*.py", format = "sdist" },
]
keywords = [
"orm",
"sqlalchemy",
"fastapi",
"pydantic",
"databases",
"async",
"alembic",
]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Internet :: WWW/HTTP",
"Framework :: AsyncIO",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3 :: Only",
]
[tool.poetry.dependencies]
python = "^3.10.0"
# Pinned below 2.14 because ormar relies on pydantic internals; bump the
# upper bound manually after verifying compatibility with each new release.
pydantic = ">=2.11.9,<2.14"
SQLAlchemy = {version = "^2.0.40", extras = ["asyncio"]}
cryptography = { version = ">=44.0.1,<49.0.0", optional = true }
# Async database drivers
aiosqlite = { version = ">=0.19,<0.23", optional = true }
aiomysql = { version = ">=0.1.0", optional = true }
aiopg = { version = "^1.3.3", optional = true }
asyncpg = { version = ">=0.28,<0.32", optional = true }
# Sync database drivers for standard tooling around setup/teardown/migrations.
psycopg2-binary = { version = "^2.9.1", optional = true }
mysqlclient = { version = "^2.1.0", optional = true }
PyMySQL = { version = "^1.1.0", optional = true }
ormar-utils = ">=0.2.0"
orjson = ">=3.6.4"
[tool.poetry.extras]
postgresql = ["asyncpg", "psycopg2-binary"]
postgres = ["asyncpg", "psycopg2-binary"]
aiopg = ["aiopg", "psycopg2-binary"]
mysql = ["aiomysql", "PyMySQL"]
sqlite = ["aiosqlite"]
crypto = ["cryptography"]
all = [
"aiosqlite",
"asyncpg",
"aiopg",
"psycopg2-binary",
"aiomysql",
"mysqlclient",
"PyMySQL",
"cryptography",
]
[tool.poetry.group.dev.dependencies]
# Testing
pytest = ">=8.2,<10.0"
pytest-cov = ">=7,<8"
codecov = "^2.1.13"
pytest-asyncio = ">=1.1.0,<2.0"
fastapi = ">=0.125.0"
ruff = ">=0.5.1,<0.15.17"
setuptools = ">=78.1.1,<83.0.0"
# types
mypy = ">=1.8,<3.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"
types-orjson = "^3.6.1"
types-aiofiles = ">=23.2,<26.0"
types-requests = "^2.32.0"
types-toml = "^0.10.8"
# Documentation
mkdocs = "^1.5.3"
mkdocs-material = ">=8.1.2,<9.8"
mkdocs-material-extensions = "^1.2"
mkdocstrings = {version = "==1.0.4", extras = ["python"]}
mkdocs-gen-files = ">=0.5,<0.7"
mkdocs-literate-nav = "^0.6.1"
mkdocs-section-index = "^0.3.7"
dataclasses = { version = ">=0.6.0,<0.8 || >0.8,<1.0.0" }
# Performance testing
yappi = "^1.6.0"
pytest-benchmark = ">=4,<6"
nest-asyncio = "^1.6.0"
pre-commit = ">=2.21,<5.0"
httpx = ">=0.28.0"
asgi-lifespan = "^2.1.0"
pydantic-extra-types = "^2.5.0"
watchdog = "<7.0.0"
pytest-codspeed = ">=4.2,<6.0"
mike = "^2.0.0"
faker = ">=24.3,<41.0"
email-validator = "^2.1.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options]
asyncio_mode = "strict"
asyncio_default_fixture_loop_scope = "module"
asyncio_default_test_loop_scope = "module"
[tool.mypy]
# TODO: Enable mypy plugin after pydantic release supporting toml file
disallow_untyped_calls = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
[[tool.mypy.overrides]]
module = ["tests.*", "benchmarks.*"]
disallow_untyped_calls = false
disallow_untyped_defs = false
disallow_incomplete_defs = false
[[tool.mypy.overrides]]
module = "docs_src.*"
ignore_errors = true
[[tool.mypy.overrides]]
module = ["sqlalchemy.*", "asyncpg", "nest_asyncio", "ormar_rust_utils"]
ignore_missing_imports = true
[tool.yapf]
based_on_style = "pep8"
disable_ending_comma_heuristic = true
split_arguments_when_comma_terminated = true
[tool.ruff]
line-length = 88
src = ["ormar", "tests"]
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = ["E402"]
[tool.ruff.format]
indent-style = "space"
docstring-code-format = true
[tool.ruff.lint.isort]
known-first-party = ["ormar"] ormar-orm-ormar-a7e7fde/scripts/ 0000775 0000000 0000000 00000000000 15211567312 0017021 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/scripts/docker-compose.yml 0000664 0000000 0000000 00000000705 15211567312 0022460 0 ustar 00root root 0000000 0000000 services:
postgres:
image: postgres:10.8
environment:
POSTGRES_USER: username
POSTGRES_PASSWORD: password
POSTGRES_DB: testsuite
ports:
- 5432:5432
mysql:
image: mysql:5.7
environment:
MYSQL_USER: username
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testsuite
ports:
- 3306:3306
volumes:
- ./mysql-init:/docker-entrypoint-initdb.d:ro ormar-orm-ormar-a7e7fde/scripts/mysql-init/ 0000775 0000000 0000000 00000000000 15211567312 0021127 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/scripts/mysql-init/01-grant-cross-schema.sql 0000664 0000000 0000000 00000000405 15211567312 0025565 0 ustar 00root root 0000000 0000000 -- Grant the test user full privileges across databases so cross-schema
-- (cross-database, in MySQL terms) tests can CREATE / DROP additional
-- databases and reference tables that live in them.
GRANT ALL PRIVILEGES ON *.* TO 'username'@'%';
FLUSH PRIVILEGES;
ormar-orm-ormar-a7e7fde/scripts/test.sh 0000775 0000000 0000000 00000000372 15211567312 0020341 0 ustar 00root root 0000000 0000000 #!/bin/sh -e
PACKAGE="ormar"
PREFIX=""
if [ -d 'venv' ] ; then
PREFIX="venv/bin/"
fi
set -x
PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov=${PACKAGE} --cov=tests --cov-report=xml --cov-fail-under=100 --cov-report=term-missing tests/ "${@}"
ormar-orm-ormar-a7e7fde/scripts/test_docs.sh 0000664 0000000 0000000 00000000250 15211567312 0021341 0 ustar 00root root 0000000 0000000 #!/bin/sh -e
PACKAGE="docs_src"
PREFIX=""
if [ -d 'venv' ] ; then
PREFIX="venv/bin/"
fi
set -x
PYTHONPATH=. ${PREFIX}pytest -vvv --ignore venv docs_src/ "${@}"
ormar-orm-ormar-a7e7fde/tests/ 0000775 0000000 0000000 00000000000 15211567312 0016474 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0020573 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/lifespan.py 0000664 0000000 0000000 00000011237 15211567312 0020653 0 ustar 00root root 0000000 0000000 from contextlib import asynccontextmanager
from typing import AsyncIterator
import pytest_asyncio
import sqlalchemy
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.schema import CreateSchema, DropSchema, DropTable
from ormar import OrmarConfig
from ormar.models.helpers.sqlalchemy import validate_cross_schema_constraints
from tests.settings import ASYNC_DATABASE_URL
class DropTableCascade(DropTable):
"""``DROP TABLE`` variant that emits ``CASCADE`` on PostgreSQL.
SQLAlchemy's ``DropTable`` exposes only ``element`` and ``if_exists`` — no
``cascade`` flag (verified on 2.0.x and on the 2.1 branch; ``DropSchema``
has ``cascade``, ``DropTable`` is the odd one out). The ``@compiles``
extension below is upstream's recommended workaround. We need ``CASCADE``
because tests share a database between modules and may leave dependent
objects from earlier runs that block a plain drop.
"""
@compiles(DropTableCascade, "postgresql")
def _compile_drop_table_cascade(
element: DropTableCascade,
compiler: sqlalchemy.sql.compiler.DDLCompiler,
**_: object,
) -> str: # pragma: no cover
return compiler.visit_drop_table(element) + " CASCADE"
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 collect_schemas(metadata: sqlalchemy.MetaData) -> list[str]:
"""Returns the deduplicated, sorted list of non-default schemas in metadata."""
return sorted({t.schema for t in metadata.sorted_tables if t.schema})
def create_schemas(
connection: sqlalchemy.Connection, config: OrmarConfig
) -> None: # pragma: no cover
schemas = collect_schemas(config.metadata)
if connection.dialect.name not in ("postgresql", "mysql"):
return
for schema in schemas:
connection.execute(CreateSchema(schema, if_not_exists=True))
def drop_schemas(
connection: sqlalchemy.Connection, config: OrmarConfig
) -> None: # pragma: no cover
schemas = collect_schemas(config.metadata)
dialect = connection.dialect.name
if dialect not in ("postgresql", "mysql"):
return
# MySQL's DROP SCHEMA aliases DROP DATABASE and rejects CASCADE; on MySQL
# the database drop already cascades to the contained tables.
cascade = dialect == "postgresql"
for schema in schemas:
connection.execute(DropSchema(schema, cascade=cascade, if_exists=True))
def drop_tables(
connection: sqlalchemy.Connection, config: OrmarConfig
): # pragma: no cover
if connection.dialect.name == "postgresql":
for table in reversed(config.metadata.sorted_tables):
connection.execute(DropTableCascade(table, if_exists=True))
else:
config.metadata.drop_all(connection)
def init_tests(config, scope="module"):
@pytest_asyncio.fixture(autouse=True, scope=scope)
async def create_database():
# Drop and create tables in a single connection to avoid event loop issues
async with config.engine.begin() as conn:
def setup_tables(connection): # pragma: no cover
validate_cross_schema_constraints(
config.metadata, connection.dialect.name
)
drop_tables(connection, config)
drop_schemas(connection, config)
create_schemas(connection, config)
config.metadata.create_all(connection)
await conn.run_sync(setup_tables)
# For PostgreSQL and MySQL, recreate engine to avoid event loop conflicts
# asyncpg and aiomysql are strict about event loops
if config.engine.dialect.name in ("postgresql", "mysql"): # pragma: no cover
await config.engine.dispose()
config._original_engine = config.engine
config.engine = create_async_engine(ASYNC_DATABASE_URL)
yield
# Restore the original engine if it was swapped
if hasattr(config, "_original_engine"): # pragma: no cover
await config.engine.dispose()
config.engine = config._original_engine
delattr(config, "_original_engine")
async with config.engine.begin() as conn:
def teardown_tables(connection): # pragma: no cover
drop_tables(connection, config)
drop_schemas(connection, config)
await conn.run_sync(teardown_tables)
await config.engine.dispose()
return create_database
ormar-orm-ormar-a7e7fde/tests/settings.py 0000664 0000000 0000000 00000002066 15211567312 0020712 0 ustar 00root root 0000000 0000000 import os
import sqlalchemy
from sqlalchemy.ext.asyncio import create_async_engine
import ormar
from ormar.databases.connection import DatabaseConnection
def convert_to_async_url(url: str) -> str: # pragma: nocover
"""Convert database URL to async driver version."""
if url.startswith("postgresql://"):
return url.replace("postgresql://", "postgresql+asyncpg://", 1)
elif url.startswith("mysql://"):
return url.replace("mysql://", "mysql+aiomysql://", 1)
elif url.startswith("sqlite://"):
return url.replace("sqlite://", "sqlite+aiosqlite://", 1)
return url
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///test.db")
ASYNC_DATABASE_URL = convert_to_async_url(DATABASE_URL)
print("USED DB:", ASYNC_DATABASE_URL)
def create_config(**args):
database_ = DatabaseConnection(ASYNC_DATABASE_URL, **args)
metadata_ = sqlalchemy.MetaData()
async_engine_ = create_async_engine(ASYNC_DATABASE_URL)
return ormar.OrmarConfig(
metadata=metadata_,
database=database_,
engine=async_engine_,
)
ormar-orm-ormar-a7e7fde/tests/test_completion.py 0000664 0000000 0000000 00000000404 15211567312 0022254 0 ustar 00root root 0000000 0000000 import ormar
from tests.settings import create_config
base_ormar_config = create_config()
class Department(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
ormar-orm-ormar-a7e7fde/tests/test_databases/ 0000775 0000000 0000000 00000000000 15211567312 0021462 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_databases/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0023561 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_databases/test_concurent_transactions.py 0000664 0000000 0000000 00000003515 15211567312 0027667 0 ustar 00root root 0000000 0000000 import asyncio
import sys
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Department(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="departments")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Course(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="courses")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
department: Department | None = ormar.ForeignKey(Department)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.skipif(
sys.version_info < (3, 11), reason="taskgroup requires python3.11 or higher"
)
@pytest.mark.asyncio
async def test_asyncio_run(): # pragma: no cover
async with base_ormar_config.database:
async with base_ormar_config.database.transaction():
csse = await Department(
id=1337,
name="Computer Science & Software Engineering",
).save()
courses = [
("Introduction to Computer Science", 101),
("Computer Architecture", 255),
("Algorithms and Data Structures:I", 225),
("Algorithms and Data Structures:II", 226),
("Operating Systems", 360),
("Database Systems", 370),
("Concurrent Programming and Distributed Systems", 461),
("Analysis of Algorithms", 425),
("Data Analysis and Pattern Recognition", 535),
]
async with asyncio.TaskGroup() as tasks:
for name, id in courses:
tasks.create_task(Course(id=id, name=name, department=csse).save())
assert len(await Course.objects.all()) == 9
ormar-orm-ormar-a7e7fde/tests/test_databases/test_connection.py 0000664 0000000 0000000 00000020653 15211567312 0025240 0 ustar 00root root 0000000 0000000 import pytest
from sqlalchemy import event, text
import ormar
from ormar.databases.connection import DatabaseConnection
from tests.lifespan import init_tests
from tests.settings import ASYNC_DATABASE_URL, DATABASE_URL, create_config
base_ormar_config = create_config()
class Team(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="teams")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
create_test_database = init_tests(base_ormar_config)
def test_url_is_replaced_to_async_and_accessible():
database = DatabaseConnection(ASYNC_DATABASE_URL)
assert database.url == ASYNC_DATABASE_URL
expected_drivers = {
"mysql": "mysql+aiomysql",
"sqlite": "sqlite+aiosqlite",
"postgresql": "postgresql+asyncpg",
}
dialect = DATABASE_URL.split(":")[0]
assert dialect in expected_drivers
assert expected_drivers[dialect] in database.url
@pytest.mark.asyncio
async def test_getting_raw_connection():
database = DatabaseConnection(ASYNC_DATABASE_URL)
async with database:
async with database.connection() as conn:
result = await conn.execute(text("SELECT 123"))
async with database.transaction():
async with database.connection() as conn2:
assert conn2 != conn
async with database.connection() as conn3:
assert conn3 == conn2
async with database.connection() as conn4:
assert conn4 != conn
assert conn3 == conn4
async with database.connection() as conn5:
assert conn5 != conn4
assert conn5 != conn
assert result.fetchone()[0] == 123
@pytest.mark.asyncio
async def test_getting_commit_in_transaction():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True) as tran:
assert tran._depth == 0
async with base_ormar_config.database.transaction() as tran2:
assert tran2._depth == 1
await Team.objects.create(name="Red Team")
await Team.objects.create(name="Blue Team")
async with base_ormar_config.database.transaction() as tran3:
assert tran3._depth == 2
await Team.objects.create(name="Yellow Team")
yellow = await Team.objects.get(name="Yellow Team")
yellow.name = "Green Team"
await yellow.update()
teams = await Team.objects.all()
assert len(teams) == 3
assert {teams.name for teams in teams} == {
"Red Team",
"Blue Team",
"Green Team",
}
@pytest.mark.asyncio
async def test_exception_in_transaction_rollbacks():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
async with base_ormar_config.database.transaction():
await Team.objects.create(name="Red Team")
await Team.objects.create(name="Blue Team")
try:
async with base_ormar_config.database.transaction() as tran2:
assert tran2._depth == 2
await Team.objects.create(name="Yellow Team")
raise ValueError("test")
except ValueError:
pass
teams = await Team.objects.all()
assert len(teams) == 2
assert {teams.name for teams in teams} == {
"Red Team",
"Blue Team",
}
@pytest.mark.asyncio
async def test_parent_rollback_cascades_to_children():
"""Test that rolling back parent transaction also rolls back child transactions."""
async with base_ormar_config.database:
try:
async with base_ormar_config.database.transaction() as parent:
assert parent._depth == 0
await Team.objects.create(name="Parent Team")
async with base_ormar_config.database.transaction() as child1:
assert child1._depth == 1
await Team.objects.create(name="Child Team 1")
async with base_ormar_config.database.transaction() as child2:
assert child2._depth == 1
await Team.objects.create(name="Child Team 2")
async with base_ormar_config.database.transaction() as grandchild:
assert grandchild._depth == 2
await Team.objects.create(name="Grandchild Team")
raise ValueError("rollback parent")
except ValueError:
pass
teams = await Team.objects.all()
assert len(teams) == 0
@pytest.mark.asyncio
async def test_force_rollback_cascades():
"""Test that force_rollback also cascades to children."""
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(
force_rollback=True
) as parent:
assert parent._depth == 0
await Team.objects.create(name="Parent Team")
async with base_ormar_config.database.transaction() as child:
assert child._depth == 1
await Team.objects.create(name="Child Team")
teams = await Team.objects.all()
assert len(teams) == 0
@pytest.mark.asyncio
async def test_standalone_query_uses_autocommit():
"""Standalone CRUD must not wrap each statement in BEGIN/COMMIT.
The old code path held a real transaction around every call; this test
pins the replacement behavior so future refactors don't silently reintroduce
the per-query commit overhead (see discussion #1629).
"""
seen_autocommit: list[bool] = []
def capture(conn, cursor, statement, parameters, context, executemany):
seen_autocommit.append(
conn.get_execution_options().get("isolation_level") == "AUTOCOMMIT"
)
async with base_ormar_config.database:
event.listen(
base_ormar_config.database.engine.sync_engine,
"before_cursor_execute",
capture,
)
try:
await Team.objects.create(name="Standalone")
await Team.objects.get(name="Standalone")
async with base_ormar_config.database.transaction():
await Team.objects.create(name="InsideTransaction")
await Team.objects.get(name="InsideTransaction")
finally:
event.remove(
base_ormar_config.database.engine.sync_engine,
"before_cursor_execute",
capture,
)
# First two statements were standalone → must have run under AUTOCOMMIT.
# Last two were inside an explicit transaction → must NOT have.
assert seen_autocommit[:2] == [True, True]
assert all(flag is False for flag in seen_autocommit[2:])
@pytest.mark.asyncio
async def test_bulk_update_is_atomic_under_autocommit():
"""``bulk_update`` must run inside a transaction so that if anything
after the write raises, the write is rolled back.
The mock below lets the real ``execute_many`` run first (so the rows
actually get updated inside the open transaction) and only then raises.
If the transaction wrapper around ``execute_many`` were removed,
AUTOCOMMIT would already have persisted the renames and the post-update
assertion would see ``renamed-*`` instead of the original names.
"""
async with base_ormar_config.database:
seeded = [await Team.objects.create(name=f"bulk-{n}") for n in "XYZ"]
ids = [t.id for t in seeded]
for idx, team in enumerate(seeded):
team.name = f"renamed-{idx}"
from ormar.databases.query_executor import QueryExecutor
orig = QueryExecutor.execute_many
async def execute_then_raise(self, query, values):
await orig(self, query, values)
raise RuntimeError("boom")
QueryExecutor.execute_many = execute_then_raise # type: ignore[assignment]
try:
with pytest.raises(RuntimeError, match="boom"):
await Team.objects.bulk_update(seeded)
finally:
QueryExecutor.execute_many = orig # type: ignore[assignment]
reloaded = await Team.objects.filter(id__in=ids).all()
assert {t.name for t in reloaded} == {"bulk-X", "bulk-Y", "bulk-Z"}
ormar-orm-ormar-a7e7fde/tests/test_databases/test_transaction_as_fixture.py 0000664 0000000 0000000 00000002242 15211567312 0027651 0 ustar 00root root 0000000 0000000 import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Jimmy(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="jimmies")
name = ormar.String(primary_key=True, max_length=42)
create_test_database = init_tests(base_ormar_config)
@pytest.fixture(scope="session")
def anyio_backend():
return "asyncio"
@pytest.fixture(scope="function", autouse=True)
async def db_session():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
yield
@pytest.mark.anyio
async def test_jimmy_can():
await Jimmy.objects.create(
name="jimmy",
)
await Jimmy.objects.create(
name="jimmyRus",
)
jimmies = await Jimmy.objects.all()
assert len(jimmies) == 2
@pytest.mark.anyio
async def test_jimmy_cant():
jimmies = await Jimmy.objects.all()
assert not jimmies
await Jimmy.objects.create(
name="jimmy",
)
await Jimmy.objects.create(
name="jimmyRus",
)
jimmies = await Jimmy.objects.all()
assert len(jimmies) == 2
ormar-orm-ormar-a7e7fde/tests/test_deferred/ 0000775 0000000 0000000 00000000000 15211567312 0021313 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_deferred/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0023412 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_deferred/test_forward_cross_refs.py 0000664 0000000 0000000 00000012207 15211567312 0026622 0 ustar 00root root 0000000 0000000 # type: ignore
from typing import ForwardRef, Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
TeacherRef = ForwardRef("Teacher")
class Student(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
primary_teacher: TeacherRef = ormar.ForeignKey(
TeacherRef, related_name="own_students"
)
class StudentTeacher(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="students_x_teachers")
class Teacher(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
students = ormar.ManyToMany(
Student, through=StudentTeacher, related_name="teachers"
)
Student.update_forward_refs()
CityRef = ForwardRef("City")
CountryRef = ForwardRef("Country")
class Country(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="countries")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=128)
capital: Optional[CityRef] = ormar.ForeignKey(
CityRef, related_name="capital_city", nullable=True
)
borders: Optional[list[CountryRef]] = ormar.ManyToMany(CountryRef)
class City(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="cities")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=128)
country: Country = ormar.ForeignKey(
Country, related_name="cities", skip_reverse=True
)
Country.update_forward_refs()
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_double_relations():
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")
s1 = await Student.objects.create(name="Joe", primary_teacher=t1)
s2 = await Student.objects.create(name="Sam", primary_teacher=t1)
s3 = await Student.objects.create(name="Kate", primary_teacher=t2)
s4 = await Student.objects.create(name="Zoe", primary_teacher=t2)
s5 = await Student.objects.create(name="John", primary_teacher=t3)
s6 = await Student.objects.create(name="Anna", primary_teacher=t3)
for t in [t1, t2, t3]:
for s in [s1, s2, s3, s4, s5, s6]:
await t.students.add(s)
jones = (
await Teacher.objects.select_related(["students", "own_students"])
.order_by(["students__name", "own_students__name"])
.get(name="Mr. Jones")
)
assert len(jones.students) == 6
assert jones.students[0].name == "Anna"
assert jones.students[5].name == "Zoe"
assert len(jones.own_students) == 2
assert jones.own_students[0].name == "Joe"
assert jones.own_students[1].name == "Sam"
smith = (
await Teacher.objects.select_related(["students", "own_students"])
.filter(students__name__contains="a")
.order_by(["students__name", "own_students__name"])
.get(name="Ms. Smith")
)
assert len(smith.students) == 3
assert smith.students[0].name == "Anna"
assert smith.students[2].name == "Sam"
assert len(smith.own_students) == 2
assert smith.own_students[0].name == "Kate"
assert smith.own_students[1].name == "Zoe"
quibble = (
await Teacher.objects.select_related(["students", "own_students"])
.filter(students__name__startswith="J")
.order_by(["-students__name", "own_students__name"])
.get(name="Mr. Quibble")
)
assert len(quibble.students) == 2
assert quibble.students[1].name == "Joe"
assert quibble.students[0].name == "John"
assert len(quibble.own_students) == 2
assert quibble.own_students[1].name == "John"
assert quibble.own_students[0].name == "Anna"
@pytest.mark.asyncio
async def test_auto_through_model():
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()
england.capital = london
await england.update()
await england.borders.add(france)
check = await Country.objects.select_related(["capital", "borders"]).get(
name="England"
)
assert check.name == "England"
assert check.capital.name == "London"
assert check.capital.country.pk == check.pk
assert check.borders[0] == france
ormar-orm-ormar-a7e7fde/tests/test_deferred/test_forward_refs.py 0000664 0000000 0000000 00000021752 15211567312 0025416 0 ustar 00root root 0000000 0000000 # type: ignore
from typing import ForwardRef, Optional
import pytest
import pytest_asyncio
import sqlalchemy as sa
import ormar
from ormar.exceptions import ModelError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
PersonRef = ForwardRef("Person")
class Person(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
supervisor: PersonRef = ormar.ForeignKey(PersonRef, related_name="employees")
Person.update_forward_refs()
GameRef = ForwardRef("Game")
ChildRef = ForwardRef("Child")
ChildFriendRef = ForwardRef("ChildFriend")
class Child(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
favourite_game: GameRef = ormar.ForeignKey(GameRef, related_name="liked_by")
least_favourite_game: GameRef = ormar.ForeignKey(
GameRef, related_name="not_liked_by"
)
friends = ormar.ManyToMany(
ChildRef, through=ChildFriendRef, related_name="also_friends"
)
class ChildFriend(ormar.Model):
ormar_config = base_ormar_config.copy()
class Game(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
Child.update_forward_refs()
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(scope="function")
async def cleanup():
yield
async with base_ormar_config.database:
await ChildFriend.objects.delete(each=True)
await Child.objects.delete(each=True)
await Game.objects.delete(each=True)
await Person.objects.delete(each=True)
@pytest.mark.asyncio
async def test_not_updated_model_raises_errors():
Person2Ref = ForwardRef("Person2")
class Person2(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
supervisor: Person2Ref = ormar.ForeignKey(Person2Ref, related_name="employees")
with pytest.raises(ModelError):
await Person2.objects.create(name="Test")
with pytest.raises(ModelError):
Person2(name="Test")
with pytest.raises(ModelError):
await Person2.objects.get()
@pytest.mark.asyncio
async def test_not_updated_model_m2m_raises_errors():
Person3Ref = ForwardRef("Person3")
class PersonFriend(ormar.Model):
ormar_config = base_ormar_config.copy()
class Person3(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
supervisors: Person3Ref = ormar.ManyToMany(
Person3Ref, through=PersonFriend, related_name="employees"
)
with pytest.raises(ModelError):
await Person3.objects.create(name="Test")
with pytest.raises(ModelError):
Person3(name="Test")
with pytest.raises(ModelError):
await Person3.objects.get()
@pytest.mark.asyncio
async def test_not_updated_model_m2m_through_raises_errors():
PersonPetRef = ForwardRef("PersonPet")
class Pet(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Person4(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
pets: list[Pet] = ormar.ManyToMany(
Pet, through=PersonPetRef, related_name="owners"
)
class PersonPet(ormar.Model):
ormar_config = base_ormar_config.copy()
with pytest.raises(ModelError):
await Person4.objects.create(name="Test")
with pytest.raises(ModelError):
Person4(name="Test")
with pytest.raises(ModelError):
await Person4.objects.get()
def test_proper_field_init():
assert "supervisor" in Person.ormar_config.model_fields
assert Person.ormar_config.model_fields["supervisor"].to == Person
assert "supervisor" in Person.model_fields
assert Person.model_fields["supervisor"].annotation == Optional[Person]
assert "supervisor" in Person.ormar_config.table.columns
assert isinstance(
Person.ormar_config.table.columns["supervisor"].type, sa.sql.sqltypes.Integer
)
assert len(Person.ormar_config.table.columns["supervisor"].foreign_keys) > 0
assert "person_supervisor" in Person.ormar_config.alias_manager._aliases_new
@pytest.mark.asyncio
async def test_self_relation():
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"
joe_check = await Person.objects.select_related("supervisor").get(
name="Joe"
)
assert joe_check.supervisor.name == "Sam"
sam_check = await Person.objects.select_related("employees").get(name="Sam")
assert sam_check.name == "Sam"
assert sam_check.employees[0].name == "Joe"
@pytest.mark.asyncio
async def test_other_forwardref_relation(cleanup):
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()
await Child(
name="Billy", favourite_game=uno, least_favourite_game=checkers
).save()
await Child(
name="Kate", favourite_game=checkers, least_favourite_game=uno
).save()
billy_check = await Child.objects.select_related(
["favourite_game", "least_favourite_game"]
).get(name="Billy")
assert billy_check.favourite_game == uno
assert billy_check.least_favourite_game == checkers
uno_check = await Game.objects.select_related(
["liked_by", "not_liked_by"]
).get(name="Uno")
assert uno_check.liked_by[0].name == "Billy"
assert uno_check.not_liked_by[0].name == "Kate"
@pytest.mark.asyncio
async def test_m2m_self_forwardref_relation(cleanup):
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()
billy = await Child(
name="Billy", favourite_game=uno, least_favourite_game=checkers
).save()
kate = await Child(
name="Kate", favourite_game=checkers, least_favourite_game=uno
).save()
steve = await Child(
name="Steve", favourite_game=jenga, least_favourite_game=uno
).save()
await billy.friends.add(kate)
await billy.friends.add(steve)
billy_check = await Child.objects.select_related(
[
"friends",
"favourite_game",
"least_favourite_game",
"friends__favourite_game",
"friends__least_favourite_game",
]
).get(name="Billy")
assert len(billy_check.friends) == 2
assert billy_check.friends[0].name == "Kate"
assert billy_check.friends[0].favourite_game.name == "Checkers"
assert billy_check.friends[0].least_favourite_game.name == "Uno"
assert billy_check.friends[1].name == "Steve"
assert billy_check.friends[1].favourite_game.name == "Jenga"
assert billy_check.friends[1].least_favourite_game.name == "Uno"
assert billy_check.favourite_game.name == "Uno"
kate_check = await Child.objects.select_related(["also_friends"]).get(
name="Kate"
)
assert len(kate_check.also_friends) == 1
assert kate_check.also_friends[0].name == "Billy"
billy_check = (
await Child.objects.select_related(
[
"friends",
"favourite_game",
"least_favourite_game",
"friends__favourite_game",
"friends__least_favourite_game",
]
)
.filter(friends__favourite_game__name="Checkers")
.get(name="Billy")
)
assert len(billy_check.friends) == 1
assert billy_check.friends[0].name == "Kate"
assert billy_check.friends[0].favourite_game.name == "Checkers"
assert billy_check.friends[0].least_favourite_game.name == "Uno"
ormar-orm-ormar-a7e7fde/tests/test_deferred/test_more_same_table_joins.py 0000664 0000000 0000000 00000012261 15211567312 0027246 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config(force_rollback=True)
class Department(ormar.Model):
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):
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):
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
department: Optional[Department] = ormar.ForeignKey(Department, nullable=False)
class Student(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="students")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
class Teacher(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="teachers")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
create_test_database = init_tests(base_ormar_config)
async def create_data():
department = await Department.objects.create(id=1, name="Math Department")
department2 = await Department.objects.create(id=2, name="Law Department")
class1 = await SchoolClass.objects.create(name="Math")
class2 = await SchoolClass.objects.create(name="Logic")
category = await Category.objects.create(name="Foreign", department=department)
category2 = await Category.objects.create(name="Domestic", department=department2)
await Student.objects.create(name="Jane", category=category, schoolclass=class1)
await Student.objects.create(name="Judy", category=category2, schoolclass=class1)
await Student.objects.create(name="Jack", category=category2, schoolclass=class2)
await Teacher.objects.create(name="Joe", category=category2, schoolclass=class1)
@pytest.mark.asyncio
async def test_model_multiple_instances_of_same_table_in_schema():
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].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 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.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 base_ormar_config.database:
await create_data()
math_class = (
await SchoolClass.objects.select_related(
["teachers__category__department", "students__category__department"]
)
.filter(
ormar.or_(
students__name="Jane",
teachers__category__name="Domestic",
students__category__name="Foreign",
)
)
.get(name="Math")
)
assert math_class.name == "Math"
assert math_class.students[0].name == "Jane"
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"
classes = (
await SchoolClass.objects.select_related(
["students__category__department", "teachers__category__department"]
)
.filter(
ormar.and_(
ormar.or_(
students__name="Jane", students__category__name="Foreign"
),
teachers__category__department__name="Law Department",
)
)
.all()
)
assert len(classes) == 1
assert classes[0].teachers[0].category.department.name == "Law Department"
assert classes[0].students[0].category.department.name == "Math Department"
ormar-orm-ormar-a7e7fde/tests/test_deferred/test_same_table_joins.py 0000664 0000000 0000000 00000011473 15211567312 0026230 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Department(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="schoolclasses")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
department: Optional[Department] = ormar.ForeignKey(Department, nullable=False)
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 Student(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="students")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
class Teacher(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="teachers")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
schoolclass: Optional[SchoolClass] = ormar.ForeignKey(SchoolClass)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
create_test_database = init_tests(base_ormar_config)
async def create_data():
department = await Department.objects.create(id=1, name="Math Department")
department2 = await Department.objects.create(id=2, name="Law Department")
class1 = await SchoolClass.objects.create(name="Math", department=department)
class2 = await SchoolClass.objects.create(name="Logic", department=department2)
category = await Category.objects.create(name="Foreign")
category2 = await Category.objects.create(name="Domestic")
await Student.objects.create(name="Jane", category=category, schoolclass=class1)
await Student.objects.create(name="Judy", category=category2, schoolclass=class1)
await Student.objects.create(name="Jack", category=category2, schoolclass=class2)
await Teacher.objects.create(name="Joe", category=category2, schoolclass=class1)
@pytest.mark.asyncio
async def test_model_multiple_instances_of_same_table_in_schema():
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"]
).all()
assert classes[0].name == "Math"
assert classes[0].students[0].name == "Jane"
assert len(classes[0].model_dump().get("students")) == 2
# 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()
assert (
classes[0].students[0].schoolclass.department.name == "Math Department"
)
await classes[1].students[0].schoolclass.department.load()
assert (
classes[1].students[0].schoolclass.department.name == "Law Department"
)
@pytest.mark.asyncio
async def test_right_tables_join():
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"]
).all()
assert classes[0].teachers[0].category.name == "Domestic"
assert classes[0].students[0].category.name is None
await classes[0].students[0].category.load()
assert classes[0].students[0].category.name == "Foreign"
@pytest.mark.asyncio
async def test_multiple_reverse_related_objects():
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"]
).all()
assert classes[0].name == "Math"
assert classes[0].students[1].name == "Judy"
assert classes[0].students[0].category.name == "Foreign"
assert classes[0].students[1].category.name == "Domestic"
assert classes[0].teachers[0].category.name == "Domestic"
ormar-orm-ormar-a7e7fde/tests/test_encryption/ 0000775 0000000 0000000 00000000000 15211567312 0021725 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_encryption/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0024024 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_encryption/test_encrypted_columns.py 0000664 0000000 0000000 00000021106 15211567312 0027073 0 ustar 00root root 0000000 0000000 # type: ignore
import base64
import datetime
import decimal
import hashlib
import uuid
from typing import Any, Optional
import pytest
import ormar
from ormar import ModelDefinitionError, NoMatch
from ormar.fields.sqlalchemy_encrypted import EncryptedString
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
)
class DummyBackend(ormar.fields.EncryptBackend):
def _initialize_backend(self, secret_key: bytes) -> None:
pass
def encrypt(self, value: Any) -> str:
return value
def decrypt(self, value: Any) -> str:
return value
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, **default_fernet)
uuid_test = ormar.UUID(default=uuid.uuid4, uuid_format="string")
uuid_test2 = ormar.UUID(nullable=True, uuid_format="string")
password: str = ormar.String(
max_length=128,
encrypt_secret="udxc32",
encrypt_backend=ormar.EncryptBackends.HASH,
)
birth_year: int = ormar.Integer(
nullable=True,
encrypt_secret="secure89key%^&psdijfipew",
encrypt_backend=ormar.EncryptBackends.FERNET,
)
test_text: str = ormar.Text(default="", **default_fernet)
test_bool: bool = ormar.Boolean(nullable=False, **default_fernet)
test_float: float = ormar.Float(**default_fernet)
test_float2: Optional[float] = ormar.Float(nullable=True, **default_fernet)
test_datetime = ormar.DateTime(default=datetime.datetime.now, **default_fernet)
test_date = ormar.Date(default=datetime.date.today, **default_fernet)
test_time = ormar.Time(default=datetime.time, **default_fernet)
test_json = ormar.JSON(default={}, **default_fernet)
test_bigint: int = ormar.BigInteger(default=0, **default_fernet)
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",
encrypt_backend=ormar.EncryptBackends.CUSTOM,
encrypt_custom_backend=DummyBackend,
)
class Hash(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="hashes")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(
max_length=128,
encrypt_secret="udxc32",
encrypt_backend=ormar.EncryptBackends.HASH,
)
class Filter(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="filters")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, **default_fernet)
hash = ormar.ForeignKey(Hash)
class Report(ormar.Model):
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)
create_test_database = init_tests(base_ormar_config)
def test_error_on_encrypted_pk():
with pytest.raises(ModelDefinitionError):
class Wrong(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="wrongs")
id: int = ormar.Integer(
primary_key=True,
encrypt_secret="asd123",
encrypt_backend=ormar.EncryptBackends.FERNET,
)
def test_error_on_encrypted_relation():
with pytest.raises(ModelDefinitionError):
class Wrong2(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="wrongs2")
id: int = ormar.Integer(primary_key=True)
author = ormar.ForeignKey(
Author,
encrypt_secret="asd123",
encrypt_backend=ormar.EncryptBackends.FERNET,
)
def test_error_on_encrypted_m2m_relation():
with pytest.raises(ModelDefinitionError):
class Wrong3(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="wrongs3")
id: int = ormar.Integer(primary_key=True)
author = ormar.ManyToMany(
Author,
encrypt_secret="asd123",
encrypt_backend=ormar.EncryptBackends.FERNET,
)
def test_wrong_backend():
with pytest.raises(ModelDefinitionError):
class Wrong3(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="wrongs3")
id: int = ormar.Integer(primary_key=True)
author = ormar.Integer(
encrypt_secret="asd123",
encrypt_backend=ormar.EncryptBackends.CUSTOM,
encrypt_custom_backend="aa",
)
def test_db_structure():
assert Author.ormar_config.table.c.get("name").type.__class__ == EncryptedString
@pytest.mark.asyncio
async def test_save_and_retrieve():
async with base_ormar_config.database:
test_uuid = uuid.uuid4()
await Author(
name="Test",
birth_year=1988,
password="test123",
uuid_test=test_uuid,
test_float=1.2,
test_bool=True,
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()
assert author.name == "Test"
assert author.birth_year == 1988
password = (
"03e4a4d513e99cb3fe4ee3db282c053daa3f3572b849c3868939a306944ad5c08"
"22b50d4886e10f4cd418c3f2df3ceb02e2e7ac6e920ae0c90f2dedfc8fa16e2"
)
assert author.password == password
assert author.uuid_test == test_uuid
assert author.uuid_test2 is None
assert author.test_datetime.date() == datetime.date.today()
assert author.test_date == datetime.date.today()
assert author.test_text == ""
assert author.test_float == 1.2
assert author.test_float2 is None
assert author.test_bigint == 0
assert author.test_json == {"aa": 12}
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 base_ormar_config.database:
await Filter(name="test1").save()
await Filter(name="test1").save()
filters = await Filter.objects.all()
assert filters[0].name == filters[1].name == "test1"
with pytest.raises(NoMatch):
await Filter.objects.get(name="test1")
assert await Filter.objects.get_or_none(name="test1") is None
@pytest.mark.asyncio
async def test_hash_filters_works():
async with base_ormar_config.database:
await Hash(name="test1").save()
await Hash(name="test2").save()
secret = hashlib.sha256("udxc32".encode()).digest()
secret = base64.urlsafe_b64encode(secret)
hashed_test1 = hashlib.sha512(secret + "test1".encode()).hexdigest()
hash1 = await Hash.objects.get(name="test1")
assert hash1.name == hashed_test1
with pytest.raises(NoMatch):
await Filter.objects.get(name__icontains="test")
@pytest.mark.asyncio
async def test_related_model_fields_properly_decrypted():
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)
await report.filters.create(name="test2")
report2 = await Report.objects.select_related("filters").get()
assert report2.filters[0].name == "test1"
assert report2.filters[1].name == "test2"
secret = hashlib.sha256("udxc32".encode()).digest()
secret = base64.urlsafe_b64encode(secret)
hashed_test1 = hashlib.sha512(secret + "test1".encode()).hexdigest()
report2 = await Report.objects.select_related("filters__hash").get()
assert report2.filters[0].name == "test1"
assert report2.filters[0].hash.name == hashed_test1
ormar-orm-ormar-a7e7fde/tests/test_exclude_include_dict/ 0000775 0000000 0000000 00000000000 15211567312 0023672 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_exclude_include_dict/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0025771 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_exclude_include_dict/test_complex_relation_tree_performance.py 0000664 0000000 0000000 00000032267 15211567312 0034261 0 ustar 00root root 0000000 0000000 from datetime import datetime
from typing import Optional, Union
import pydantic
import pytest
import ormar as orm
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class ChagenlogRelease(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
ormar_config = base_ormar_config.copy(tablename="changelog_release")
class CommitIssue(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
ormar_config = base_ormar_config.copy(tablename="commit_issues")
class CommitLabel(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
ormar_config = base_ormar_config.copy(tablename="commit_label")
class MergeRequestCommit(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
ormar_config = base_ormar_config.copy(tablename="merge_request_commits")
class MergeRequestIssue(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
ormar_config = base_ormar_config.copy(tablename="merge_request_issues")
class MergeRequestLabel(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
ormar_config = base_ormar_config.copy(tablename="merge_request_labels")
class ProjectLabel(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
ormar_config = base_ormar_config.copy(tablename="project_label")
class PushCommit(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
ormar_config = base_ormar_config.copy(tablename="push_commit")
class PushLabel(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
ormar_config = base_ormar_config.copy(tablename="push_label")
class TagCommit(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
ormar_config = base_ormar_config.copy(tablename="tag_commits")
class TagIssue(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
ormar_config = base_ormar_config.copy(tablename="tag_issue")
class TagLabel(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
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)
ormar_config = base_ormar_config.copy(tablename="user_project")
class Label(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
title: str = orm.String(max_length=100)
description: str = orm.Text(default="")
type: str = orm.String(max_length=100, default="")
ormar_config = base_ormar_config.copy(tablename="labels")
class Project(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
name: str = orm.String(max_length=100)
description: str = orm.Text(default="")
git_url: str = orm.String(max_length=500, default="")
labels: Optional[Union[list[Label], Label]] = orm.ManyToMany(
Label, through=ProjectLabel, ondelete="CASCADE", onupdate="CASCADE"
)
changelog_jira_tag: str = orm.String(max_length=100, default="")
change_type_jira_tag: str = orm.String(max_length=100, default="")
jira_prefix: str = orm.String(max_length=10, default="SAN")
type: str = orm.String(max_length=10, default="cs")
target_branch_name: str = orm.String(max_length=100, default="master")
header: str = orm.String(max_length=250, default="")
jira_url: str = orm.String(max_length=500)
changelog_file: str = orm.String(max_length=250, default="")
version_file: str = orm.String(max_length=250, default="")
ormar_config = base_ormar_config.copy(tablename="projects")
class Issue(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
summary: str = orm.Text(default="")
description: str = orm.Text(default="")
changelog: str = orm.Text(default="")
link: str = orm.String(max_length=500)
issue_type: str = orm.String(max_length=100)
key: str = orm.String(max_length=100)
change_type: str = orm.String(max_length=100, default="")
data: pydantic.Json = orm.JSON(default={})
ormar_config = base_ormar_config.copy(tablename="issues")
class User(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
username: str = orm.String(max_length=100, unique=True)
name: str = orm.String(max_length=200, default="")
ormar_config = base_ormar_config.copy(tablename="users")
class Branch(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
name: str = orm.String(max_length=200)
description: str = orm.Text(default="")
automatic_tags: bool = orm.Boolean(default=False)
is_it_locked: bool = orm.Boolean(default=True)
prefix_tag: str = orm.String(max_length=50, default="")
postfix_tag: str = orm.String(max_length=50, default="")
project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE")
ormar_config = base_ormar_config.copy(tablename="branches")
class Changelog(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
content: str = orm.Text(default="")
version: str = orm.Text(default="")
past_changelog: int = orm.Integer(default=0)
label: Label = orm.ForeignKey(
Label, nullable=True, ondelete="CASCADE", onupdate="CASCADE"
)
project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE")
created_date: datetime = orm.DateTime(default=datetime.utcnow())
ormar_config = base_ormar_config.copy(tablename="changelogs")
class Commit(orm.Model):
id: str = orm.String(max_length=500, primary_key=True)
short_id: str = orm.String(max_length=500)
title: str = orm.String(max_length=500)
message: str = orm.Text(default="")
url = orm.String(max_length=500, default="")
author_name = orm.String(max_length=500, default="")
labels: Optional[Union[list[Label], Label]] = orm.ManyToMany(
Label, through=CommitLabel, ondelete="CASCADE", onupdate="CASCADE"
)
issues: Optional[Union[list[Issue], Issue]] = orm.ManyToMany(
Issue, through=CommitIssue, ondelete="CASCADE", onupdate="CASCADE"
)
ormar_config = base_ormar_config.copy(tablename="commits")
class MergeRequest(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
idd: int = orm.Integer(default=0)
title: str = orm.String(max_length=500)
state: str = orm.String(max_length=100)
merge_status: str = orm.String(max_length=100)
description: str = orm.Text(default="")
source: Branch = orm.ForeignKey(Branch, related_name="source")
target: Branch = orm.ForeignKey(Branch, related_name="target")
labels: Optional[Union[list[Label], Label]] = orm.ManyToMany(
Label, through=MergeRequestLabel, ondelete="CASCADE", onupdate="CASCADE"
)
commits: Optional[Union[list[Commit], Commit]] = orm.ManyToMany(
Commit, through=MergeRequestCommit, ondelete="CASCADE", onupdate="CASCADE"
)
issues: Optional[Union[list[Issue], Issue]] = orm.ManyToMany(
Issue, through=MergeRequestIssue, ondelete="CASCADE", onupdate="CASCADE"
)
project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE")
ormar_config = base_ormar_config.copy(tablename="merge_requests")
class Push(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
branch: Branch = orm.ForeignKey(
Branch, nullable=True, ondelete="CASCADE", onupdate="CASCADE"
)
has_locking_changes: bool = orm.Boolean(default=False)
sha: str = orm.String(max_length=200)
labels: Optional[Union[list[Label], Label]] = orm.ManyToMany(
Label, through=PushLabel, ondelete="CASCADE", onupdate="CASCADE"
)
commits: Optional[Union[list[Commit], Commit]] = orm.ManyToMany(
Commit,
through=PushCommit,
through_relation_name="push",
through_reverse_relation_name="commit_id",
ondelete="CASCADE",
onupdate="CASCADE",
)
author: User = orm.ForeignKey(User, ondelete="CASCADE", onupdate="CASCADE")
project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE")
ormar_config = base_ormar_config.copy(tablename="pushes")
class Tag(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
name: str = orm.String(max_length=200)
ref: str = orm.String(max_length=200)
project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE")
title: str = orm.String(max_length=200, default="")
description: str = orm.Text(default="")
commits: Optional[Union[list[Commit], Commit]] = orm.ManyToMany(
Commit,
through=TagCommit,
through_relation_name="tag",
through_reverse_relation_name="commit_id",
ondelete="CASCADE",
onupdate="CASCADE",
)
issues: Optional[Union[list[Issue], Issue]] = orm.ManyToMany(
Issue, through=TagIssue, ondelete="CASCADE", onupdate="CASCADE"
)
labels: Optional[Union[list[Label], Label]] = orm.ManyToMany(
Label, through=TagLabel, ondelete="CASCADE", onupdate="CASCADE"
)
user: User = orm.ForeignKey(
User, nullable=True, ondelete="CASCADE", onupdate="CASCADE"
)
branch: Branch = orm.ForeignKey(
Branch, nullable=True, ondelete="CASCADE", onupdate="CASCADE"
)
ormar_config = base_ormar_config.copy(tablename="tags")
class Release(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
title: str = orm.String(max_length=200, default="")
description: str = orm.Text(default="")
tag: Tag = orm.ForeignKey(Tag, ondelete="CASCADE", onupdate="CASCADE")
changelogs: list[Changelog] = orm.ManyToMany(
Changelog, through=ChagenlogRelease, ondelete="CASCADE", onupdate="CASCADE"
)
data: pydantic.Json = orm.JSON(default={})
ormar_config = base_ormar_config.copy(tablename="releases")
class Webhook(orm.Model):
id: int = orm.Integer(name="id", primary_key=True)
object_kind = orm.String(max_length=100)
project: Project = orm.ForeignKey(Project, ondelete="CASCADE", onupdate="CASCADE")
merge_request: MergeRequest = orm.ForeignKey(
MergeRequest, nullable=True, ondelete="CASCADE", onupdate="CASCADE"
)
tag: Tag = orm.ForeignKey(
Tag, nullable=True, ondelete="CASCADE", onupdate="CASCADE"
)
push: Push = orm.ForeignKey(
Push, nullable=True, ondelete="CASCADE", onupdate="CASCADE"
)
created_at: datetime = orm.DateTime(default=datetime.now())
data: pydantic.Json = orm.JSON(default={})
status: int = orm.Integer(default=200)
error: str = orm.Text(default="")
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_very_complex_relation_map():
async with base_ormar_config.database:
tags = [
{"id": 18, "name": "name-18", "ref": "ref-18"},
{"id": 17, "name": "name-17", "ref": "ref-17"},
{"id": 12, "name": "name-12", "ref": "ref-12"},
]
payload = [
{
"id": 9,
"title": "prueba-2321",
"description": "\n"
"Description 1"
"\n",
"data": {},
},
{
"id": 8,
"title": "prueba-123-prod",
"description": "\n"
"Description 2"
"\n",
"data": {},
},
{
"id": 6,
"title": "prueba-3-2",
"description": "\n"
"Description 3"
"\n",
"data": {},
},
]
saved_tags = []
for tag in tags:
saved_tags.append(await Tag(**tag).save())
for ind, pay in enumerate(payload):
await Release(**pay, tag=saved_tags[ind]).save()
releases = await Release.objects.order_by(Release.id.desc()).all()
dicts = [release.model_dump() for release in releases]
result = [
{
"id": 9,
"title": "prueba-2321",
"description": "\n"
"Description 1"
"\n",
"data": {},
"tag": {
"id": 18,
},
"changelogs": [],
},
{
"id": 8,
"title": "prueba-123-prod",
"description": "\n"
"Description 2"
"\n",
"data": {},
"tag": {
"id": 17,
},
"changelogs": [],
},
{
"id": 6,
"title": "prueba-3-2",
"description": "\n"
"Description 3"
"\n",
"data": {},
"tag": {
"id": 12,
},
"changelogs": [],
},
]
assert dicts == result
ormar-orm-ormar-a7e7fde/tests/test_exclude_include_dict/test_dumping_model_to_dict.py 0000664 0000000 0000000 00000013205 15211567312 0031634 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Role(ormar.Model):
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):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255, nullable=False)
password: Optional[str] = ormar.String(max_length=255, nullable=True)
first_name: str = ormar.String(max_length=255, nullable=False)
roles: list[Role] = ormar.ManyToMany(Role)
class Tier(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
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)
tier: Optional[Tier] = ormar.ForeignKey(Tier)
class Item(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
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)
role2 = Role(name="Admin", id=2)
user = User(
id=1,
email="test@test.com",
password="ijacids7^*&",
first_name="Anna",
roles=[role, role2],
)
tier = Tier(id=1, name="Tier I")
category1 = Category(id=1, name="Toys", tier=tier)
category2 = Category(id=2, name="Weapons", tier=tier)
item1 = Item(id=1, name="Teddy Bear", category=category1, created_by=user)
item2 = Item(id=2, name="M16", category=category2, created_by=user)
return item1, item2
def test_dumping_to_dict_no_exclusion(sample_data):
item1, item2 = sample_data
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.model_dump()
assert dict2["name"] == "M16"
assert dict2["category"]["name"] == "Weapons"
assert dict2["created_by"]["email"] == "test@test.com"
def test_dumping_to_dict_exclude_set(sample_data):
item1, item2 = sample_data
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.model_dump(exclude={"category"})
assert dict4["name"] == "M16"
assert "category" not in dict4
assert dict4["created_by"]["email"] == "test@test.com"
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"
def test_dumping_to_dict_exclude_dict(sample_data):
item1, item2 = sample_data
dict6 = item2.model_dump(exclude={"category": {"name"}, "name": ...})
assert "name" not in dict6
assert "category" in dict6
assert "name" not in dict6["category"]
assert dict6["created_by"]["email"] == "test@test.com"
def test_dumping_to_dict_exclude_nested_dict(sample_data):
item1, item2 = sample_data
dict1 = item2.model_dump(exclude={"category": {"tier": {"name"}}, "name": ...})
assert "name" not in dict1
assert "category" in dict1
assert dict1["category"]["name"] == "Weapons"
assert dict1["created_by"]["email"] == "test@test.com"
assert dict1["category"]["tier"].get("name") is None
def test_dumping_to_dict_exclude_and_include_nested_dict(sample_data):
item1, item2 = sample_data
dict1 = item2.model_dump(
exclude={"category": {"tier": {"name"}}}, include={"name", "category"}
)
assert dict1.get("name") == "M16"
assert "category" in dict1
assert dict1["category"]["name"] == "Weapons"
assert "created_by" not in dict1
assert dict1["category"]["tier"].get("name") is None
dict2 = item1.model_dump(
exclude={"id": ...},
include={"name": ..., "category": {"name": ..., "tier": {"id"}}},
)
assert dict2.get("name") == "Teddy Bear"
assert dict2.get("id") is None # models not saved
assert dict2["category"]["name"] == "Toys"
assert "created_by" not in dict1
assert dict1["category"]["tier"].get("name") is None
assert dict1["category"]["tier"]["id"] == 1
def test_dumping_dict_without_primary_keys(sample_data):
item1, item2 = sample_data
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"},
],
},
"name": "M16",
}
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"},
],
},
"name": "Teddy Bear",
}
ormar-orm-ormar-a7e7fde/tests/test_exclude_include_dict/test_excludable_items.py 0000664 0000000 0000000 00000031417 15211567312 0030622 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from ormar.exceptions import QueryDefinitionError
from ormar.models.excludable import ExcludableItems
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class NickNames(ormar.Model):
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")
is_lame: Optional[bool] = ormar.Boolean(nullable=True)
class NicksHq(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="nicks_x_hq")
class HQ(ormar.Model):
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")
nicks: list[NickNames] = ormar.ManyToMany(NickNames, through=NicksHq)
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, nullable=False, name="company_name")
founded: Optional[int] = ormar.Integer(nullable=True)
hq: HQ = ormar.ForeignKey(HQ)
class Car(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
manufacturer: Optional[Company] = ormar.ForeignKey(Company)
name: str = ormar.String(max_length=100)
year: Optional[int] = ormar.Integer(nullable=True)
gearbox_type: Optional[str] = ormar.String(max_length=20, nullable=True)
gears: Optional[int] = ormar.Integer(nullable=True)
aircon_type: Optional[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"}
assert car_excludable.include == set()
assert car_excludable.is_excluded("year")
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()
assert manu_excludable.is_excluded("founded")
def compare_results_include(excludable):
manager = Company.ormar_config.alias_manager
car_excludable = excludable.get(Car)
assert car_excludable.include == {"id", "name"}
assert car_excludable.exclude == set()
assert car_excludable.is_included("name")
assert not car_excludable.is_included("gears")
alias = manager.resolve_relation_alias(Car, "manufacturer")
manu_excludable = excludable.get(Company, alias=alias)
assert manu_excludable.include == {"name"}
assert manu_excludable.exclude == set()
assert manu_excludable.is_included("name")
assert not manu_excludable.is_included("founded")
alias = manager.resolve_relation_alias(Company, "hq")
hq_excludable = excludable.get(HQ, alias=alias)
assert hq_excludable.include == {"name"}
assert hq_excludable.exclude == set()
alias = manager.resolve_relation_alias(NicksHq, "nicknames")
nick_excludable = excludable.get(NickNames, alias=alias)
assert nick_excludable.include == {"name"}
assert nick_excludable.exclude == set()
def test_excluding_fields_from_list():
fields = ["gearbox_type", "gears", "aircon_type", "year", "manufacturer__founded"]
excludable = ExcludableItems()
excludable.build(items=fields, model_cls=Car, slot="exclude")
compare_results(excludable)
def test_excluding_fields_from_dict():
fields = {
"gearbox_type": ...,
"gears": ...,
"aircon_type": ...,
"year": ...,
"manufacturer": {"founded": ...},
}
excludable = ExcludableItems()
excludable.build(items=fields, model_cls=Car, slot="exclude")
compare_results(excludable)
def test_excluding_fields_from_dict_with_set():
fields = {
"gearbox_type": ...,
"gears": ...,
"aircon_type": ...,
"year": ...,
"manufacturer": {"founded"},
}
excludable = ExcludableItems()
excludable.build(items=fields, model_cls=Car, slot="exclude")
compare_results(excludable)
def test_gradual_build_from_lists():
fields_col = [
"year",
["gearbox_type", "gears"],
"aircon_type",
["manufacturer__founded"],
]
excludable = ExcludableItems()
for fields in fields_col:
excludable.build(items=fields, model_cls=Car, slot="exclude")
compare_results(excludable)
def test_nested_includes():
fields = [
"id",
"name",
"manufacturer__name",
"manufacturer__hq__name",
"manufacturer__hq__nicks__name",
]
excludable = ExcludableItems()
excludable.build(items=fields, model_cls=Car, slot="include")
compare_results_include(excludable)
def test_nested_includes_from_dict():
fields = {
"id": ...,
"name": ...,
"manufacturer": {"name": ..., "hq": {"name": ..., "nicks": {"name": ...}}},
}
excludable = ExcludableItems()
excludable.build(items=fields, model_cls=Car, slot="include")
compare_results_include(excludable)
def test_nested_includes_from_dict_with_set():
fields = {
"id": ...,
"name": ...,
"manufacturer": {"name": ..., "hq": {"name": ..., "nicks": {"name"}}},
}
excludable = ExcludableItems()
excludable.build(items=fields, model_cls=Car, slot="include")
compare_results_include(excludable)
def _compare_flatten_results_single(excludable):
car_excludable = excludable.get(Car)
assert car_excludable.flatten == {"manufacturer"}
assert car_excludable.is_flattened("manufacturer")
assert not car_excludable.is_flattened("gears")
def _compare_flatten_results_deep(excludable):
manager = Company.ormar_config.alias_manager
alias = manager.resolve_relation_alias(Car, "manufacturer")
manu_excludable = excludable.get(Company, alias=alias)
assert manu_excludable.flatten == {"hq"}
assert manu_excludable.is_flattened("hq")
car_excludable = excludable.get(Car)
assert car_excludable.flatten == set()
def test_flatten_from_string():
excludable = ExcludableItems()
excludable.build(items="manufacturer", model_cls=Car, slot="flatten")
_compare_flatten_results_single(excludable)
assert excludable._flatten_paths == {("manufacturer",)}
def test_flatten_from_list():
excludable = ExcludableItems()
excludable.build(items=["manufacturer"], model_cls=Car, slot="flatten")
_compare_flatten_results_single(excludable)
def test_flatten_from_set():
excludable = ExcludableItems()
excludable.build(items={"manufacturer"}, model_cls=Car, slot="flatten")
_compare_flatten_results_single(excludable)
def test_flatten_from_dict_ellipsis():
excludable = ExcludableItems()
excludable.build(items={"manufacturer": ...}, model_cls=Car, slot="flatten")
_compare_flatten_results_single(excludable)
def test_flatten_deep_dunder():
excludable = ExcludableItems()
excludable.build(items="manufacturer__hq", model_cls=Car, slot="flatten")
_compare_flatten_results_deep(excludable)
assert excludable._flatten_paths == {("manufacturer", "hq")}
def test_flatten_deep_dict():
excludable = ExcludableItems()
excludable.build(items={"manufacturer": {"hq": ...}}, model_cls=Car, slot="flatten")
_compare_flatten_results_deep(excludable)
def test_flatten_deep_dict_with_set():
excludable = ExcludableItems()
excludable.build(items={"manufacturer": {"hq"}}, model_cls=Car, slot="flatten")
_compare_flatten_results_deep(excludable)
def test_flatten_map_is_none_when_no_paths():
excludable = ExcludableItems()
assert excludable.flatten_map() is None
def test_flatten_map_caches_and_invalidates_on_new_path():
excludable = ExcludableItems()
excludable.build(items="hq", model_cls=Company, slot="flatten")
first = excludable.flatten_map()
assert first.data == {"hq": ...}
assert excludable.flatten_map() is first # cached: same FlattenMap reference
# adding a new (non-colliding) flatten path invalidates the cache
excludable.build(items="cars", model_cls=Company, slot="flatten")
rebuilt = excludable.flatten_map()
assert rebuilt is not first
assert rebuilt.data == {"hq": ..., "cars": ...}
def test_flatten_has_flatten_entries_flag():
excludable = ExcludableItems()
assert not any(entry.flatten for entry in excludable.items.values())
excludable.build(items="manufacturer", model_cls=Car, slot="flatten")
assert any(entry.flatten for entry in excludable.items.values())
def test_flatten_copy_preserves_paths_and_sets():
excludable = ExcludableItems()
excludable.build(items="manufacturer__hq", model_cls=Car, slot="flatten")
clone = ExcludableItems.from_excludable(excludable)
assert clone._flatten_paths == {("manufacturer", "hq")}
manager = Company.ormar_config.alias_manager
alias = manager.resolve_relation_alias(Car, "manufacturer")
assert clone.get(Company, alias=alias).flatten == {"hq"}
def test_flatten_rejects_non_relation_leaf_from_dunder():
excludable = ExcludableItems()
with pytest.raises(QueryDefinitionError, match="not a relation"):
excludable.build(items="manufacturer__name", model_cls=Car, slot="flatten")
def test_flatten_rejects_non_relation_leaf_from_dict():
excludable = ExcludableItems()
with pytest.raises(QueryDefinitionError, match="not a relation"):
excludable.build(
items={"manufacturer": {"name": ...}},
model_cls=Car,
slot="flatten",
)
def test_flatten_rejects_non_relation_leaf_from_set_in_dict():
excludable = ExcludableItems()
with pytest.raises(QueryDefinitionError, match="not a relation"):
excludable.build(
items={"manufacturer": {"name"}}, model_cls=Car, slot="flatten"
)
def test_flatten_rejects_unknown_relation():
excludable = ExcludableItems()
with pytest.raises(QueryDefinitionError, match="Unknown relation"):
excludable.build(items="nope", model_cls=Car, slot="flatten")
def test_flatten_rejects_through_target():
excludable = ExcludableItems()
with pytest.raises(QueryDefinitionError, match="through model"):
excludable.build(
items="manufacturer__hq__nickshq", model_cls=Car, slot="flatten"
)
def test_flatten_rejects_prefix_collision_list():
excludable = ExcludableItems()
with pytest.raises(QueryDefinitionError, match="unreachable"):
excludable.build(
items=["manufacturer", "manufacturer__hq"],
model_cls=Car,
slot="flatten",
)
def test_flatten_rejects_prefix_collision_gradual():
excludable = ExcludableItems()
excludable.build(items="manufacturer", model_cls=Car, slot="flatten")
with pytest.raises(QueryDefinitionError, match="unreachable"):
excludable.build(items="manufacturer__hq", model_cls=Car, slot="flatten")
def test_flatten_vs_excludable_errors_on_child_sub_selection():
excludable = ExcludableItems()
excludable.build(items={"manufacturer__name"}, model_cls=Car, slot="include")
excludable.build(items="manufacturer", model_cls=Car, slot="flatten")
with pytest.raises(QueryDefinitionError, match="Flatten conflict"):
excludable.validate_flatten_vs_excludable(source_model=Car)
def test_flatten_vs_excludable_allows_parent_whole_relation_include():
excludable = ExcludableItems()
excludable.build(items={"manufacturer"}, model_cls=Car, slot="include")
excludable.build(items="manufacturer", model_cls=Car, slot="flatten")
excludable.validate_flatten_vs_excludable(source_model=Car)
def test_includes_and_excludes_combo():
fields_inc1 = ["id", "name", "year", "gearbox_type", "gears"]
fields_inc2 = {"manufacturer": {"name"}}
fields_exc1 = {"manufacturer__founded"}
fields_exc2 = "aircon_type"
excludable = ExcludableItems()
excludable.build(items=fields_inc1, model_cls=Car, slot="include")
excludable.build(items=fields_inc2, model_cls=Car, slot="include")
excludable.build(items=fields_exc1, model_cls=Car, slot="exclude")
excludable.build(items=fields_exc2, model_cls=Car, slot="exclude")
car_excludable = excludable.get(Car)
assert car_excludable.include == {"id", "name", "year", "gearbox_type", "gears"}
assert car_excludable.exclude == {"aircon_type"}
assert car_excludable.is_excluded("aircon_type")
assert car_excludable.is_included("name")
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"}
assert manu_excludable.is_excluded("founded")
ormar-orm-ormar-a7e7fde/tests/test_exclude_include_dict/test_excluding_fields_in_fastapi.py 0000664 0000000 0000000 00000021056 15211567312 0033014 0 ustar 00root root 0000000 0000000 import datetime
import random
import string
from typing import Optional
import pydantic
import pytest
import sqlalchemy
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from pydantic import ConfigDict, computed_field
import ormar
from ormar import post_save
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))
# note that you can set orm_mode here
# and in this case UserSchema become unnecessary
class UserBase(pydantic.BaseModel):
model_config = ConfigDict(from_attributes=True)
email: str
first_name: str
last_name: str
class UserCreateSchema(UserBase):
password: str
category: str
class UserSchema(UserBase):
model_config = ConfigDict(from_attributes=True)
def gen_pass():
choices = string.ascii_letters + string.digits + "!@#$%^&*()"
return "".join(random.choice(choices) for _ in range(20))
class RandomModel(ormar.Model):
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)
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()
)
@computed_field
def full_name(self) -> str:
return " ".join([self.first_name, self.last_name])
class User(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="users")
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255)
password: Optional[str] = ormar.String(max_length=255, nullable=True)
first_name: str = ormar.String(max_length=255)
last_name: str = ormar.String(max_length=255)
category: Optional[str] = ormar.String(max_length=255, nullable=True)
class User2(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="users2")
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(max_length=255, nullable=False)
password: str = ormar.String(max_length=255)
first_name: str = ormar.String(max_length=255)
last_name: str = ormar.String(max_length=255)
category: Optional[str] = ormar.String(max_length=255, nullable=True)
timestamp: datetime.datetime = pydantic.Field(default_factory=datetime.datetime.now)
create_test_database = init_tests(base_ormar_config)
@app.post("/users/", response_model=User, response_model_exclude={"password"})
async def create_user(user: User):
return await user.save()
@app.post("/users2/", response_model=User)
async def create_user2(user: User):
user = await user.save()
return user.model_dump(exclude={"password"})
@app.post("/users3/", response_model=UserBase)
async def create_user3(user: User2):
return await user.save()
@app.post("/users4/")
async def create_user4(user: User2):
return (await user.save()).model_dump(exclude={"password"})
@app.post("/random/", response_model=RandomModel)
async def create_user5(user: RandomModel):
return await user.save()
@app.post("/random2/", response_model=RandomModel)
async def create_user6(user: RandomModel):
return await user.save()
@app.post("/random3/", response_model=RandomModel, response_model_exclude={"full_name"})
async def create_user7(user: RandomModel):
return await user.save()
@pytest.mark.asyncio
async def test_excluding_fields_in_endpoints():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
user = {
"email": "test@domain.com",
"password": "^*^%A*DA*IAAA",
"first_name": "John",
"last_name": "Doe",
}
response = await client.post("/users/", json=user)
created_user = User(**response.json())
assert created_user.pk is not None
assert created_user.password is None
user2 = {"email": "test@domain.com", "first_name": "John", "last_name": "Doe"}
response = await client.post("/users/", json=user2)
created_user = User(**response.json())
assert created_user.pk is not None
assert created_user.password is None
response = await client.post("/users2/", json=user)
created_user2 = User(**response.json())
assert created_user2.pk is not None
assert created_user2.password is None
# response has only 3 fields from UserBase
response = await client.post("/users3/", json=user)
assert list(response.json().keys()) == ["email", "first_name", "last_name"]
timestamp = datetime.datetime.now()
user3 = {
"email": "test@domain.com",
"password": "^*^%A*DA*IAAA",
"first_name": "John",
"last_name": "Doe",
"timestamp": str(timestamp),
}
response = await client.post("/users4/", json=user3)
assert list(response.json().keys()) == [
"id",
"email",
"first_name",
"last_name",
"category",
"timestamp",
]
assert response.json().get("timestamp") == str(timestamp).replace(" ", "T")
resp_dict = response.json()
resp_dict.update({"password": "random"})
user_instance = User2(**resp_dict)
assert user_instance.timestamp is not None
assert isinstance(user_instance.timestamp, datetime.datetime)
assert user_instance.timestamp == timestamp
response = await client.post("/users4/", json=user3)
assert list(response.json().keys()) == [
"id",
"email",
"first_name",
"last_name",
"category",
"timestamp",
]
assert (
datetime.datetime.strptime(
response.json().get("timestamp"), "%Y-%m-%dT%H:%M:%S.%f"
)
== timestamp
)
@pytest.mark.asyncio
async def test_adding_fields_in_endpoints():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
user3 = {"last_name": "Test", "full_name": "deleted"}
response = await client.post("/random/", json=user3)
assert list(response.json().keys()) == [
"id",
"password",
"first_name",
"last_name",
"created_date",
"full_name",
]
assert response.json().get("full_name") == "John Test"
user3 = {"last_name": "Test"}
response = await client.post("/random/", json=user3)
assert list(response.json().keys()) == [
"id",
"password",
"first_name",
"last_name",
"created_date",
"full_name",
]
assert response.json().get("full_name") == "John Test"
@pytest.mark.asyncio
async def test_adding_fields_in_endpoints2():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
user3 = {"last_name": "Test"}
response = await client.post("/random2/", json=user3)
assert list(response.json().keys()) == [
"id",
"password",
"first_name",
"last_name",
"created_date",
"full_name",
]
assert response.json().get("full_name") == "John Test"
@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.model_dump()
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
user3 = {"last_name": "Test"}
response = await client.post("/random3/", json=user3)
assert list(response.json().keys()) == [
"id",
"password",
"first_name",
"last_name",
"created_date",
]
assert response.json().get("full_name") is None
assert len(dummy_registry) == 1
check_dict = dummy_registry.get(response.json().get("id"))
check_dict.pop("full_name")
assert response.json().get("password") == check_dict.get("password")
ormar-orm-ormar-a7e7fde/tests/test_exclude_include_dict/test_excluding_fields_with_default.py 0000664 0000000 0000000 00000006652 15211567312 0033363 0 ustar 00root root 0000000 0000000 import random
from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
def get_position() -> int:
return random.randint(1, 10)
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: Optional[bool] = ormar.Boolean(default=False, nullable=True)
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(default=get_position)
play_count: Optional[int] = ormar.Integer(nullable=True, default=0)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_excluding_field_with_default():
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)
await Track.objects.create(title="Night Lights", album=album)
album = await Album.objects.fields("name").get()
assert album.is_best_seller is None
album = await Album.objects.exclude_fields({"is_best_seller", "id"}).get()
assert album.is_best_seller is None
album = await Album.objects.exclude_fields({"is_best_seller": ...}).get()
assert album.is_best_seller is None
tracks = await Track.objects.all()
for track in tracks:
assert track.play_count is not None
assert track.position is not None
album = (
await Album.objects.select_related("tracks")
.exclude_fields({"is_best_seller": ..., "tracks": {"play_count"}})
.get(name="Miami")
)
assert album.is_best_seller is None
assert len(album.tracks) == 3
for track in album.tracks:
assert track.play_count is None
assert track.position is not None
album = (
await Album.objects.select_related("tracks")
.exclude_fields(
{
"is_best_seller": ...,
"tracks": {"play_count": ..., "position": ...},
}
)
.get(name="Miami")
)
assert album.is_best_seller is None
assert len(album.tracks) == 3
for track in album.tracks:
assert track.play_count is None
assert track.position is None
album = (
await Album.objects.select_related("tracks")
.exclude_fields(
{"is_best_seller": ..., "tracks": {"play_count", "position"}}
)
.get(name="Miami")
)
assert album.is_best_seller is None
assert len(album.tracks) == 3
for track in album.tracks:
assert track.play_count is None
assert track.position is None
ormar-orm-ormar-a7e7fde/tests/test_exclude_include_dict/test_excluding_nested_models_lists.py 0000664 0000000 0000000 00000000000 15211567312 0033376 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_exclude_include_dict/test_excluding_subset_of_columns.py 0000664 0000000 0000000 00000017201 15211567312 0033077 0 ustar 00root root 0000000 0000000 from typing import Optional
import pydantic
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
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, nullable=False)
founded: Optional[int] = ormar.Integer(nullable=True)
class Car(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="cars")
id: int = ormar.Integer(primary_key=True)
manufacturer: Optional[Company] = ormar.ForeignKey(Company)
name: str = ormar.String(max_length=100)
year: Optional[int] = ormar.Integer(nullable=True)
gearbox_type: Optional[str] = ormar.String(max_length=20, nullable=True)
gears: Optional[int] = ormar.Integer(nullable=True, name="gears_number")
aircon_type: Optional[str] = ormar.String(max_length=20, nullable=True)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_selecting_subset():
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,
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",
)
all_cars = (
await Car.objects.select_related("manufacturer")
.exclude_fields(
[
"gearbox_type",
"gears",
"aircon_type",
"year",
"manufacturer__founded",
]
)
.all()
)
for car in all_cars:
assert all(
getattr(car, x) is None
for x in ["year", "gearbox_type", "gears", "aircon_type"]
)
assert car.manufacturer.name == "Toyota"
assert car.manufacturer.founded is None
all_cars = (
await Car.objects.select_related("manufacturer")
.exclude_fields(
{
"gearbox_type": ...,
"gears": ...,
"aircon_type": ...,
"year": ...,
"manufacturer": {"founded": ...},
}
)
.all()
)
all_cars2 = (
await Car.objects.select_related("manufacturer")
.exclude_fields(
{
"gearbox_type": ...,
"gears": ...,
"aircon_type": ...,
"year": ...,
"manufacturer": {"founded"},
}
)
.all()
)
assert all_cars == all_cars2
for car in all_cars:
assert all(
getattr(car, x) is None
for x in ["year", "gearbox_type", "gears", "aircon_type"]
)
assert car.manufacturer.name == "Toyota"
assert car.manufacturer.founded is None
all_cars = (
await Car.objects.select_related("manufacturer")
.exclude_fields("year")
.exclude_fields(["gearbox_type", "gears"])
.exclude_fields("aircon_type")
.all()
)
for car in all_cars:
assert all(
getattr(car, x) is None
for x in ["year", "gearbox_type", "gears", "aircon_type"]
)
assert car.manufacturer.name == "Toyota"
assert car.manufacturer.founded == 1937
all_cars_check = await Car.objects.select_related("manufacturer").all()
for car in all_cars_check:
assert all(
getattr(car, x) is not None
for x in ["year", "gearbox_type", "gears", "aircon_type"]
)
assert car.manufacturer.name == "Toyota"
assert car.manufacturer.founded == 1937
all_cars_check2 = (
await Car.objects.select_related("manufacturer")
.fields(["id", "name", "manufacturer"])
.exclude_fields("manufacturer__founded")
.all()
)
for car in all_cars_check2:
assert all(
getattr(car, x) is None
for x in ["year", "gearbox_type", "gears", "aircon_type"]
)
assert car.manufacturer.name == "Toyota"
assert car.manufacturer.founded is None
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",
}
ormar-orm-ormar-a7e7fde/tests/test_exclude_include_dict/test_flatten_fields.py 0000664 0000000 0000000 00000066116 15211567312 0030300 0 ustar 00root root 0000000 0000000 """
End-to-end tests for :py:meth:`QuerySet.flatten_fields` and the
``flatten_fields`` / ``flatten_all`` kwargs on :py:meth:`Model.model_dump`.
"""
from typing import ForwardRef, Optional
import pytest
import ormar
from ormar.exceptions import QueryDefinitionError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class HQ(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="flatten_hqs")
id: int = ormar.Integer(primary_key=True)
city: str = ormar.String(max_length=100)
class Company(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="flatten_companies")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
hq: Optional[HQ] = ormar.ForeignKey(HQ)
class Manager(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="flatten_managers")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Car(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="flatten_cars")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
year: Optional[int] = ormar.Integer(nullable=True)
manufacturer: Optional[Company] = ormar.ForeignKey(Company)
lead_manager: Optional[Manager] = ormar.ForeignKey(Manager)
PersonRef = ForwardRef("Person")
class Person(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="flatten_people")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
supervisor = ormar.ForeignKey(to=PersonRef, related_name="reports")
Person.update_forward_refs()
class Category(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="flatten_categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class PostCategory(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="flatten_post_categories")
id: int = ormar.Integer(primary_key=True)
rating: int = ormar.Integer(default=0)
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="flatten_authors")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Post(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="flatten_posts")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=100)
author: Optional[Author] = ormar.ForeignKey(Author)
categories: list[Category] = ormar.ManyToMany(Category, through=PostCategory)
create_test_database = init_tests(base_ormar_config)
async def _setup_car_graph():
hq = await HQ.objects.create(city="Tokyo")
company = await Company.objects.create(name="Toyota", hq=hq)
manager = await Manager.objects.create(name="Akio")
car = await Car.objects.create(
name="Corolla", year=2020, manufacturer=company, lead_manager=manager
)
return hq, company, manager, car
async def _setup_post_graph():
author = await Author.objects.create(name="Kent")
cat_a = await Category.objects.create(name="Tech")
cat_b = await Category.objects.create(name="News")
post = await Post.objects.create(title="Intro", author=author)
await post.categories.add(cat_a)
await post.categories.add(cat_b)
return author, cat_a, cat_b, post
# ---------------------------------------------------------------------------
# Core dunder / python style coverage
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@pytest.mark.parametrize(
"flatten_arg",
[
"manufacturer",
["manufacturer"],
{"manufacturer"},
{"manufacturer": ...},
Car.manufacturer,
],
ids=["string", "list", "set", "dict_ellipsis", "field_accessor"],
)
async def test_flatten_fk_input_forms(flatten_arg):
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, _ = await _setup_car_graph()
cars = (
await Car.objects.select_related("manufacturer")
.flatten_fields(flatten_arg)
.all()
)
assert cars[0].model_dump()["manufacturer"] == company.id
@pytest.mark.asyncio
@pytest.mark.parametrize(
"flatten_arg",
[["manufacturer", "lead_manager"], {"manufacturer": ..., "lead_manager": ...}],
ids=["list", "dict_ellipsis"],
)
async def test_flatten_fk_multiple_relations(flatten_arg):
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, manager, _ = await _setup_car_graph()
cars = (
await Car.objects.select_related(["manufacturer", "lead_manager"])
.flatten_fields(flatten_arg)
.all()
)
data = cars[0].model_dump()
assert data["manufacturer"] == company.id
assert data["lead_manager"] == manager.id
@pytest.mark.asyncio
@pytest.mark.parametrize(
"flatten_arg",
[
"manufacturer__hq",
{"manufacturer": {"hq": ...}},
[Car.manufacturer.hq], # type: ignore[union-attr]
],
ids=["dunder", "dict", "field_accessor"],
)
async def test_flatten_fk_deep_dunder_vs_dict(flatten_arg):
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
hq, _, _, _ = await _setup_car_graph()
cars = (
await Car.objects.select_related("manufacturer__hq")
.flatten_fields(flatten_arg)
.all()
)
data = cars[0].model_dump()
assert isinstance(data["manufacturer"], dict)
assert data["manufacturer"]["hq"] == hq.id
# ---------------------------------------------------------------------------
# Relation-kind coverage
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_flatten_reverse_fk_list_of_pks():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, car = await _setup_car_graph()
car2 = await Car.objects.create(name="Yaris", manufacturer=company)
companies = (
await Company.objects.select_related("cars")
.flatten_fields("cars")
.all()
)
companies_sorted = sorted(companies, key=lambda m: m.id)
data = companies_sorted[0].model_dump()
assert sorted(data["cars"]) == sorted([car.id, car2.id])
@pytest.mark.asyncio
async def test_flatten_m2m_list_of_pks():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, cat_a, cat_b, _ = await _setup_post_graph()
posts = (
await Post.objects.select_related("categories")
.flatten_fields("categories")
.all()
)
posts[0].model_dump()
assert sorted(posts[0].model_dump()["categories"]) == sorted(
[cat_a.id, cat_b.id]
)
@pytest.mark.asyncio
async def test_flatten_m2m_empty_list():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
author = await Author.objects.create(name="Kent")
await Post.objects.create(title="Empty", author=author)
posts = (
await Post.objects.select_related("categories")
.flatten_fields("categories")
.all()
)
assert posts[0].model_dump()["categories"] == []
@pytest.mark.asyncio
async def test_flatten_null_fk_is_none():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
company = await Company.objects.create(name="No HQ")
cars = (
await Company.objects.select_related("hq")
.flatten_fields("hq")
.filter(id=company.id)
.all()
)
assert cars[0].model_dump()["hq"] is None
@pytest.mark.asyncio
async def test_flatten_self_referential_fk():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
boss = await Person.objects.create(name="Boss")
await Person.objects.create(name="Worker", supervisor=boss)
rows = (
await Person.objects.select_related("supervisor")
.filter(name="Worker")
.flatten_fields("supervisor")
.all()
)
assert rows[0].model_dump()["supervisor"] == boss.id
@pytest.mark.asyncio
async def test_flatten_m2m_suppresses_through_models():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, cat_a, cat_b, _ = await _setup_post_graph()
posts = (
await Post.objects.select_related("categories")
.flatten_fields("categories")
.all()
)
data = posts[0].model_dump(exclude_through_models=False)
assert sorted(data["categories"]) == sorted([cat_a.id, cat_b.id])
# the scalar-pk list has no place for through dicts; still safe
assert "postcategory" not in data
# ---------------------------------------------------------------------------
# Multi-level nesting combinations
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@pytest.mark.parametrize(
"flatten_arg",
["posts__categories", {"posts": {"categories": ...}}],
ids=["dunder", "dict"],
)
async def test_flatten_fk_then_m2m_both_styles(flatten_arg):
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, cat_a, cat_b, _ = await _setup_post_graph()
authors = (
await Author.objects.select_related("posts__categories")
.flatten_fields(flatten_arg)
.all()
)
data = authors[0].model_dump()
assert sorted(data["posts"][0]["categories"]) == sorted(
[cat_a.id, cat_b.id]
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"flatten_arg",
["manufacturer__hq", {"manufacturer": {"hq": ...}}],
ids=["dunder", "dict"],
)
async def test_flatten_three_levels_deep(flatten_arg):
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
hq, _, _, _ = await _setup_car_graph()
# flatten the deepest hop — car.manufacturer is still a dict, but
# manufacturer.hq renders as its pk
rows = (
await Car.objects.select_related("manufacturer__hq")
.flatten_fields(flatten_arg)
.all()
)
assert rows[0].model_dump()["manufacturer"]["hq"] == hq.id
# ---------------------------------------------------------------------------
# Combination with include / exclude (non-conflicting)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_flatten_plus_include_whole_relation_ok():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, _ = await _setup_car_graph()
cars = (
await Car.objects.select_related("manufacturer")
.fields({"name", "manufacturer"})
.flatten_fields("manufacturer")
.all()
)
assert cars[0].model_dump()["manufacturer"] == company.id
@pytest.mark.asyncio
async def test_flatten_plus_exclude_sibling_column_ok():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, _, _, _ = await _setup_car_graph()
cars = (
await Car.objects.select_related("manufacturer")
.exclude_fields("year")
.flatten_fields("manufacturer")
.all()
)
data = cars[0].model_dump()
assert data["year"] is None
assert isinstance(data["manufacturer"], int)
@pytest.mark.asyncio
async def test_flatten_plus_exclude_same_relation_drops_field():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await _setup_car_graph()
cars = (
await Car.objects.select_related("manufacturer")
.flatten_fields("manufacturer")
.exclude_fields("manufacturer")
.all()
)
data = cars[0].model_dump()
assert data.get("manufacturer") is None
# ---------------------------------------------------------------------------
# Error cases
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
@pytest.mark.parametrize(
"build_query",
[
lambda: Car.objects.flatten_fields("manufacturer").fields(
{"manufacturer": {"name"}}
),
lambda: Car.objects.flatten_fields("manufacturer").exclude_fields(
{"manufacturer": {"name"}}
),
lambda: Car.objects.fields({"manufacturer": {"name"}}).flatten_fields(
"manufacturer"
),
],
ids=["include_after_flatten", "exclude_after_flatten", "include_before_flatten"],
)
async def test_flatten_conflicts_with_child_sub_fields(build_query):
async with base_ormar_config.database:
with pytest.raises(QueryDefinitionError, match="Flatten conflict"):
build_query()
@pytest.mark.asyncio
async def test_flatten_prefix_collision_list():
async with base_ormar_config.database:
with pytest.raises(QueryDefinitionError, match="unreachable"):
Car.objects.flatten_fields(["manufacturer", "manufacturer__hq"])
@pytest.mark.asyncio
async def test_flatten_prefix_collision_chained():
async with base_ormar_config.database:
qs = Car.objects.flatten_fields("manufacturer")
with pytest.raises(QueryDefinitionError, match="unreachable"):
qs.flatten_fields("manufacturer__hq")
@pytest.mark.asyncio
async def test_flatten_rejects_non_relation_target():
async with base_ormar_config.database:
with pytest.raises(QueryDefinitionError, match="not a relation"):
Car.objects.flatten_fields("name")
@pytest.mark.asyncio
async def test_flatten_rejects_unknown_relation():
async with base_ormar_config.database:
with pytest.raises(QueryDefinitionError, match="Unknown relation"):
Car.objects.flatten_fields("nope")
@pytest.mark.asyncio
async def test_flatten_vs_exclude_primary_keys_raises():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await _setup_car_graph()
cars = await Car.objects.select_related("manufacturer").all()
with pytest.raises(QueryDefinitionError, match="exclude_primary_keys"):
cars[0].model_dump(
flatten_fields="manufacturer", exclude_primary_keys=True
)
@pytest.mark.asyncio
async def test_flatten_all_vs_exclude_primary_keys_raises():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await _setup_car_graph()
cars = await Car.objects.select_related("manufacturer").all()
with pytest.raises(QueryDefinitionError, match="exclude_primary_keys"):
cars[0].model_dump(flatten_all=True, exclude_primary_keys=True)
# ---------------------------------------------------------------------------
# flatten_all
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_flatten_all_single_level():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, manager, _ = await _setup_car_graph()
cars = await Car.objects.select_related(
["manufacturer", "lead_manager"]
).all()
data = cars[0].model_dump(flatten_all=True)
assert data["manufacturer"] == company.id
assert data["lead_manager"] == manager.id
@pytest.mark.asyncio
async def test_flatten_all_deeply_nested():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, _ = await _setup_car_graph()
cars = await Car.objects.select_related("manufacturer__hq").all()
data = cars[0].model_dump(flatten_all=True)
# top-level FK collapses first; nested hq is unreachable through
# the scalar - which is the exact intent of flatten_all
assert data["manufacturer"] == company.id
@pytest.mark.asyncio
async def test_flatten_all_covers_m2m_and_reverse():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
author, cat_a, cat_b, post = await _setup_post_graph()
authors = await Author.objects.select_related("posts__categories").all()
data = authors[0].model_dump(flatten_all=True)
assert data["posts"] == [post.id]
# ---------------------------------------------------------------------------
# Auto-load select_related / prefetch_related
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_flatten_auto_adds_select_related_for_fk():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, _ = await _setup_car_graph()
cars = await Car.objects.flatten_fields("manufacturer").all()
assert cars[0].model_dump()["manufacturer"] == company.id
@pytest.mark.asyncio
async def test_flatten_auto_adds_prefetch_related_for_reverse_fk():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, car = await _setup_car_graph()
companies = (
await Company.objects.flatten_fields("cars").filter(id=company.id).all()
)
assert companies[0].model_dump()["cars"] == [car.id]
@pytest.mark.asyncio
async def test_flatten_auto_adds_prefetch_related_for_m2m():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, cat_a, cat_b, _ = await _setup_post_graph()
posts = await Post.objects.flatten_fields("categories").all()
assert sorted(posts[0].model_dump()["categories"]) == sorted(
[cat_a.id, cat_b.id]
)
@pytest.mark.asyncio
async def test_flatten_does_not_duplicate_select_related():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, _ = await _setup_car_graph()
qs = Car.objects.select_related("manufacturer").flatten_fields(
"manufacturer"
)
assert qs._select_related.count("manufacturer") == 1
cars = await qs.all()
assert cars[0].model_dump()["manufacturer"] == company.id
# ---------------------------------------------------------------------------
# model_dump direct path
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_flatten_directly_on_model_dump():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, _ = await _setup_car_graph()
cars = await Car.objects.select_related("manufacturer").all()
data = cars[0].model_dump(flatten_fields="manufacturer")
assert data["manufacturer"] == company.id
@pytest.mark.asyncio
async def test_flatten_directly_uses_pk_only_instance():
# Even without select_related, ormar exposes the FK as a pk-only model,
# so flattening on model_dump returns the pk value (not None).
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, _ = await _setup_car_graph()
cars = await Car.objects.all()
data = cars[0].model_dump(flatten_fields="manufacturer")
assert data["manufacturer"] == company.id
@pytest.mark.asyncio
async def test_flatten_direct_with_null_fk_returns_none():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await Car.objects.create(name="Orphan", manufacturer=None)
cars = await Car.objects.filter(name="Orphan").all()
data = cars[0].model_dump(flatten_fields="manufacturer")
assert data["manufacturer"] is None
@pytest.mark.asyncio
async def test_flatten_through_model_dump_json():
import json
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, _ = await _setup_car_graph()
cars = await Car.objects.select_related("manufacturer").all()
payload = cars[0].model_dump_json(flatten_fields="manufacturer")
assert json.loads(payload)["manufacturer"] == company.id
# ---------------------------------------------------------------------------
# Filter composition, chaining, isolation
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_filter_on_deeper_relation_with_flattened_parent():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, _ = await _setup_car_graph()
cars = (
await Car.objects.filter(manufacturer__hq__city="Tokyo")
.flatten_fields("manufacturer")
.all()
)
assert cars[0].model_dump()["manufacturer"] == company.id
@pytest.mark.asyncio
async def test_chained_flatten_fields_unions():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, manager, _ = await _setup_car_graph()
qs = (
Car.objects.select_related(["manufacturer", "lead_manager"])
.flatten_fields("manufacturer")
.flatten_fields("lead_manager")
)
cars = await qs.all()
data = cars[0].model_dump()
assert data["manufacturer"] == company.id
assert data["lead_manager"] == manager.id
@pytest.mark.asyncio
async def test_sibling_querysets_remain_isolated():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, _ = await _setup_car_graph()
base = Car.objects.select_related("manufacturer")
flat = base.flatten_fields("manufacturer")
plain = base
flat_cars = await flat.all()
plain_cars = await plain.all()
assert flat_cars[0].model_dump()["manufacturer"] == company.id
assert isinstance(plain_cars[0].model_dump()["manufacturer"], dict)
@pytest.mark.asyncio
async def test_flatten_direct_field_accessor_single():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, _ = await _setup_car_graph()
cars = await Car.objects.select_related("manufacturer").all()
data = cars[0].model_dump(flatten_fields=Car.manufacturer)
assert data["manufacturer"] == company.id
@pytest.mark.asyncio
async def test_flatten_direct_field_accessor_list():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, manager, _ = await _setup_car_graph()
cars = await Car.objects.select_related(
["manufacturer", "lead_manager"]
).all()
data = cars[0].model_dump(
flatten_fields=[Car.manufacturer, Car.lead_manager]
)
assert data["manufacturer"] == company.id
assert data["lead_manager"] == manager.id
@pytest.mark.asyncio
@pytest.mark.parametrize(
"select_related, dump_kwargs",
[
(
"manufacturer",
{
"flatten_fields": "manufacturer",
"include": {"manufacturer": {"name"}},
},
),
(
"manufacturer",
{
"flatten_fields": "manufacturer",
"exclude": {"manufacturer": {"name": ...}},
},
),
(
"manufacturer__hq",
{
"flatten_fields": "manufacturer__hq",
"include": {"manufacturer": {"hq": {"city"}}},
},
),
],
ids=["include_child", "exclude_child", "deep_include_child"],
)
async def test_flatten_direct_conflict_raises(select_related, dump_kwargs):
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await _setup_car_graph()
cars = await Car.objects.select_related(select_related).all()
with pytest.raises(QueryDefinitionError, match="Flatten conflict"):
cars[0].model_dump(**dump_kwargs)
@pytest.mark.asyncio
async def test_flatten_list_plus_exclude_list_skips():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, _, _, _ = await _setup_post_graph()
posts = (
await Post.objects.select_related("categories")
.flatten_fields("categories")
.all()
)
data = posts[0].model_dump(exclude_list=True)
assert "categories" not in data
@pytest.mark.asyncio
async def test_flatten_direct_with_dict_input_non_relation_is_silent():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
_, company, _, _ = await _setup_car_graph()
cars = await Car.objects.select_related("manufacturer").all()
# dict input is treated as already resolved; invalid leaves are
# simply never visited during serialization and produce no effect.
data = cars[0].model_dump(flatten_fields={"name": ...})
assert data["name"] == "Corolla"
assert isinstance(data["manufacturer"], dict)
ormar-orm-ormar-a7e7fde/tests/test_exclude_include_dict/test_pydantic_dict_params.py 0000664 0000000 0000000 00000006742 15211567312 0031475 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Category(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: Optional[str] = ormar.String(max_length=100, default="Test", nullable=True)
visibility: bool = ormar.Boolean(default=True)
class Item(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="items")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
price: float = ormar.Float(default=9.99)
categories: list[Category] = ormar.ManyToMany(Category)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_exclude_default():
async with base_ormar_config.database:
category = Category()
assert category.model_dump() == {
"id": None,
"items": [],
"name": "Test",
"visibility": True,
}
assert category.model_dump(exclude_defaults=True) == {"items": []}
await category.save()
category2 = await Category.objects.get()
assert category2.model_dump() == {
"id": 1,
"items": [],
"name": "Test",
"visibility": True,
}
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 base_ormar_config.database:
category = Category(id=2, name=None)
assert category.model_dump() == {
"id": 2,
"items": [],
"name": None,
"visibility": True,
}
assert category.model_dump(exclude_none=True) == {
"id": 2,
"items": [],
"visibility": True,
}
await category.save()
category2 = await Category.objects.get()
assert category2.model_dump() == {
"id": 2,
"items": [],
"name": None,
"visibility": True,
}
assert category2.model_dump(exclude_none=True) == {
"id": 2,
"items": [],
"visibility": True,
}
assert (
category2.model_dump_json(exclude_none=True)
== '{"id":2,"visibility":true,"items":[]}'
)
@pytest.mark.asyncio
async def test_exclude_unset():
async with base_ormar_config.database:
category = Category(id=3, name="Test 2")
assert category.model_dump() == {
"id": 3,
"items": [],
"name": "Test 2",
"visibility": True,
}
assert category.model_dump(exclude_unset=True) == {
"id": 3,
"items": [],
"name": "Test 2",
}
await category.save()
category2 = await Category.objects.get()
assert category2.model_dump() == {
"id": 3,
"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.model_dump(exclude_unset=True) == {
"id": 3,
"items": [],
"name": "Test 2",
"visibility": True,
}
ormar-orm-ormar-a7e7fde/tests/test_fastapi/ 0000775 0000000 0000000 00000000000 15211567312 0021162 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_fastapi/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0023261 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_binary_fields.py 0000664 0000000 0000000 00000004062 15211567312 0025407 0 ustar 00root root 0000000 0000000 import base64
import uuid
from enum import Enum
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
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))
blob3 = b"\xc3\x83\x28"
blob4 = b"\xf0\x28\x8c\x28"
blob5 = b"\xee"
blob6 = b"\xff"
class BinaryEnum(Enum):
blob3 = blob3
blob4 = blob4
blob5 = blob5
blob6 = blob6
class BinaryThing(ormar.Model):
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(represent_as_base64_str=True, max_length=100)
create_test_database = init_tests(base_ormar_config)
@app.get("/things", response_model=list[BinaryThing])
async def read_things():
return await BinaryThing.objects.order_by("name").all()
@app.post("/things", response_model=BinaryThing)
async def create_things(thing: BinaryThing):
thing = await thing.save()
return thing
@pytest.mark.asyncio
async def test_read_main():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
response = await client.post(
"/things",
json={"bt": base64.b64encode(blob3).decode()},
headers=headers,
)
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 thing.__dict__["bt"] == blob3
assert thing.bt == base64.b64encode(blob3).decode()
def test_schema():
schema = BinaryThing.model_json_schema()
assert schema["properties"]["bt"]["format"] == "base64"
assert schema["example"]["bt"] == "string"
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_docs_with_multiple_relations_to_one.py 0000664 0000000 0000000 00000004065 15211567312 0032121 0 ustar 00root root 0000000 0000000 from typing import Optional
from uuid import UUID, uuid4
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
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))
class CA(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="cb1s")
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
cb1_name: str = ormar.Text(default="")
ca1: Optional[CA] = ormar.ForeignKey(CA, nullable=True)
class CB2(ormar.Model):
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
@app.get("/cb1", response_model=CB1)
async def get_cb1(): # pragma: no cover
return None
@app.get("/cb2", response_model=CB2)
async def get_cb2(): # pragma: no cover
return None
@pytest.mark.asyncio
async def test_all_endpoints():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
response = await client.get("/openapi.json")
assert response.status_code == 200, response.text
schema = response.json()
components = schema["components"]["schemas"]
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
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_enum_schema.py 0000664 0000000 0000000 00000001223 15211567312 0025055 0 ustar 00root root 0000000 0000000 from enum import Enum
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class MyEnum(Enum):
SMALL = 1
BIG = 2
class EnumExample(ormar.Model):
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.model_json_schema()
assert {"MyEnum": {"title": "MyEnum", "enum": [1, 2], "type": "integer"}} == schema[
"$defs"
]
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_excludes_with_get_pydantic.py 0000664 0000000 0000000 00000011102 15211567312 0030167 0 ustar 00root root 0000000 0000000 from typing import ForwardRef, Optional
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
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))
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")
SelfRef.update_forward_refs()
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.model_dump()).save()
create_category.__annotations__["category"] = Category.get_pydantic(exclude={"id"})
app.post("/categories/", response_model=Category)(create_category)
@app.post(
"/selfrefs/",
response_model=SelfRef.get_pydantic(exclude={"parent", "children__name"}),
)
async def create_selfref(
selfref: SelfRef.get_pydantic( # type: ignore
exclude={"children__name"} # noqa: F821
),
):
selfr = SelfRef(**selfref.model_dump())
await selfr.save()
if selfr.children:
for child in selfr.children:
await child.upsert()
return selfr
@app.get("/selfrefs/{ref_id}/")
async def get_selfref(ref_id: int):
selfr = await SelfRef.objects.select_related("children").get(id=ref_id)
return selfr
@pytest.mark.asyncio
async def test_read_main():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
test_category = dict(name="Foo", id=12)
response = await client.post("/categories/", json=test_category)
assert response.status_code == 200
cat = Category(**response.json())
assert cat.name == "Foo"
assert cat.id == 1
assert cat.items == []
test_selfref = dict(name="test")
test_selfref2 = dict(name="test2", parent={"id": 1})
test_selfref3 = dict(name="test3", children=[{"name": "aaa"}])
response = await client.post("/selfrefs/", json=test_selfref)
assert response.status_code == 200
self_ref = SelfRef(**response.json())
assert self_ref.id == 1
assert self_ref.name == "test"
assert self_ref.parent is None
assert self_ref.children == []
response = await client.post("/selfrefs/", json=test_selfref2)
assert response.status_code == 200
self_ref = SelfRef(**response.json())
assert self_ref.id == 2
assert self_ref.name == "test2"
assert self_ref.parent is None
assert self_ref.children == []
response = await client.post("/selfrefs/", json=test_selfref3)
assert response.status_code == 200
self_ref = SelfRef(**response.json())
assert self_ref.id == 3
assert self_ref.name == "test3"
assert self_ref.parent is None
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].model_dump() == {
"children": [],
"id": 4,
"name": "selfref",
"parent": {"id": 3, "name": "test3"},
}
response = await client.get("/selfrefs/2/")
assert response.status_code == 200
check_children = SelfRef(**response.json())
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,
}
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_excluding_fields.py 0000664 0000000 0000000 00000006622 15211567312 0026111 0 ustar 00root root 0000000 0000000 import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
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))
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(tablename="items")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
categories: list[Category] = ormar.ManyToMany(Category)
create_test_database = init_tests(base_ormar_config)
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
await item.save_related(follow=True, save_all=True)
return 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.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.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)
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.model_dump(exclude_through_models=True, exclude_primary_keys=True)
@pytest.mark.asyncio
async def test_all_endpoints():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
item = {
"name": "test",
"categories": [{"name": "test cat"}, {"name": "test cat2"}],
}
response = await client.post("/items/", json=item)
item_check = Item(**response.json())
assert item_check.id is not None
assert item_check.categories[0].id is not None
no_pk_item = (await client.get(f"/items/{item_check.id}")).json()
assert no_pk_item == item
no_pk_category = (
await client.get(f"/categories/{item_check.categories[0].id}")
).json()
assert no_pk_category == {
"items": [
{
"itemcategory": {"category": None, "id": 1, "item": None},
"name": "test",
}
],
"name": "test cat",
}
no_through_category = (
await client.get(f"/categories/nt/{item_check.categories[0].id}")
).json()
assert no_through_category == {
"id": 1,
"items": [{"id": 1, "name": "test"}],
"name": "test cat",
}
no_through_category = (
await client.get(f"/categories/ntp/{item_check.categories[0].id}")
).json()
assert no_through_category == {"items": [{"name": "test"}], "name": "test cat"}
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_extra_ignore_parameter.py 0000664 0000000 0000000 00000002213 15211567312 0027317 0 ustar 00root root 0000000 0000000 import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
from ormar import Extra
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))
class Item(ormar.Model):
ormar_config = base_ormar_config.copy(extra=Extra.ignore)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
create_test_database = init_tests(base_ormar_config)
@app.post("/item/", response_model=Item)
async def create_item(item: Item):
return await item.save()
@pytest.mark.asyncio
async def test_extra_parameters_in_request():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
data = {"name": "Name", "extraname": "to ignore"}
resp = await client.post("item/", json=data)
assert resp.status_code == 200
assert "name" in resp.json()
assert resp.json().get("name") == "Name"
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_fastapi_docs.py 0000664 0000000 0000000 00000011344 15211567312 0025235 0 ustar 00root root 0000000 0000000 import datetime
from typing import Optional, Union
import pydantic
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from pydantic import Field
import ormar
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))
class PTestA(pydantic.BaseModel):
c: str
d: bytes
e: datetime.datetime
class PTestP(pydantic.BaseModel):
a: int
b: Optional[PTestA]
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)
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)
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
@app.post("/items/", response_model=Item)
async def create_item(item: Item):
await item.save()
return item
@app.post("/items/add_category/", response_model=Item)
async def add_item_category(item: Item, category: Category):
await item.categories.add(category)
return item
@app.post("/categories/", response_model=Category)
async def create_category(category: Category):
await category.save()
return category
@pytest.mark.asyncio
async def test_all_endpoints():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, 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, "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.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.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
assert items[0].categories[0].name == "test cat"
assert items[0].categories[1].name == "test cat2"
response = await client.get("/docs")
assert response.status_code == 200
assert b"FastAPI - Swagger UI" in response.content
def test_schema_modification():
schema = Item.model_json_schema()
assert any(
x.get("type") == "array" for x in schema["properties"]["categories"]["anyOf"]
)
assert schema["properties"]["categories"]["title"] == "Categories"
assert schema["example"] == {
"categories": [{"id": 0, "name": "string"}],
"id": 0,
"name": "string",
"pydantic_int": 0,
"test_P": [{"a": 0, "b": {"c": "string", "d": "string", "e": "string"}}],
"test_P_or_A": (0, "string"),
}
schema = Category.model_json_schema()
assert schema["$defs"]["Category"]["example"] == {
"id": 0,
"name": "string",
"items": [
{
"id": 0,
"name": "string",
"pydantic_int": 0,
"test_P": [
{"a": 0, "b": {"c": "string", "d": "string", "e": "string"}}
],
"test_P_or_A": (0, "string"),
}
],
}
def test_schema_gen():
schema = app.openapi()
assert "Category" in schema["components"]["schemas"]
subschemas = [x.split("__")[-1] for x in schema["components"]["schemas"]]
assert "Item-Input" in subschemas
assert "Item-Output" in subschemas
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_fastapi_usage.py 0000664 0000000 0000000 00000003544 15211567312 0025414 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
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))
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(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
@pytest.mark.asyncio
async def test_read_main():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
response = await client.post(
"/items/", json={"name": "test", "id": 1, "category": {"name": "test cat"}}
)
assert response.status_code == 200
assert response.json() == {
"category": {
"id": None,
"items": [
{
"category": {"id": None, "name": "test cat"},
"id": 1,
"name": "test",
}
],
"name": "test cat",
},
"id": 1,
"name": "test",
}
item = Item(**response.json())
assert item.id == 1
assert item.category.items[0].id == 1
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_inheritance_concrete_fastapi.py 0000664 0000000 0000000 00000026157 15211567312 0030470 0 ustar 00root root 0000000 0000000 import datetime
from typing import Optional
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from pydantic import computed_field
import ormar
from ormar.relations.relation_proxy import RelationProxy
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))
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}"
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)
async def create_item(item: Subject):
return item
@app.post("/categories/", response_model=Category)
async def create_category(category: Category):
await category.save()
return category
@app.post("/buses/", response_model=Bus)
async def create_bus(bus: Bus):
await bus.save()
return bus
@app.get("/buses/{item_id}", response_model=Bus)
async def get_bus(item_id: int):
bus = await Bus.objects.select_related(["owner", "co_owner"]).get(pk=item_id)
return bus
@app.get("/buses/", response_model=list[Bus])
async def get_buses():
buses = await Bus.objects.select_related(["owner", "co_owner"]).all()
return buses
@app.post("/trucks/", response_model=Truck)
async def create_truck(truck: Truck):
await truck.save()
return truck
@app.post("/persons/", response_model=Person)
async def create_person(person: Person):
await person.save()
return person
@app.post("/buses2/", response_model=Bus2)
async def create_bus2(bus: Bus2):
await bus.save()
return bus
@app.post("/buses2/{item_id}/add_coowner/", response_model=Bus2)
async def add_bus_coowner(item_id: int, person: Person):
bus = await Bus2.objects.select_related(["owner", "co_owners"]).get(pk=item_id)
await bus.co_owners.add(person)
return bus
@app.get("/buses2/", response_model=list[Bus2])
async def get_buses2():
buses = await Bus2.objects.select_related(["owner", "co_owners"]).all()
return buses
@app.post("/trucks2/", response_model=Truck2)
async def create_truck2(truck: Truck2):
await truck.save()
return truck
@app.post("/trucks2/{item_id}/add_coowner/", response_model=Truck2)
async def add_truck_coowner(item_id: int, person: Person):
truck = await Truck2.objects.select_related(["owner", "co_owners"]).get(pk=item_id)
await truck.co_owners.add(person)
return truck
@pytest.mark.asyncio
async def test_read_main():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
test_category = dict(name="Foo", code=123, created_by="Sam", updated_by="Max")
test_subject = dict(name="Bar")
response = await client.post("/categories/", json=test_category)
assert response.status_code == 200
cat = Category(**response.json())
assert cat.name == "Foo"
assert cat.created_by == "Sam"
assert cat.created_date is not None
assert cat.id == 1
cat_dict = cat.model_dump()
cat_dict["updated_date"] = cat_dict["updated_date"].strftime(
"%Y-%m-%d %H:%M:%S.%f"
)
cat_dict["created_date"] = cat_dict["created_date"].strftime(
"%Y-%m-%d %H:%M:%S.%f"
)
test_subject["category"] = cat_dict
response = await client.post("/subjects/", json=test_subject)
assert response.status_code == 200
sub = Subject(**response.json())
assert sub.name == "Bar"
assert sub.category.pk == cat.pk
assert isinstance(sub.updated_date, datetime.datetime)
@pytest.mark.asyncio
async def test_inheritance_with_relation():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
sam = Person(**(await client.post("/persons/", json={"name": "Sam"})).json())
joe = Person(**(await client.post("/persons/", json={"name": "Joe"})).json())
truck_dict = dict(
name="Shelby wanna be",
max_capacity=1400,
owner=sam.model_dump(),
co_owner=joe.model_dump(),
)
bus_dict = 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())
assert shelby.name == "Shelby wanna be"
assert shelby.owner.name == "Sam"
assert shelby.co_owner.name == "Joe"
assert shelby.co_owner == joe
assert shelby.max_capacity == 1400
assert unicorn.name == "Unicorn"
assert unicorn.owner == sam
assert unicorn.owner.name == "Sam"
assert unicorn.co_owner.name == "Joe"
assert unicorn.max_persons == 50
unicorn2 = Bus(**(await client.get(f"/buses/{unicorn.pk}")).json())
assert unicorn2.name == "Unicorn"
assert unicorn2.owner == sam
assert unicorn2.owner.name == "Sam"
assert unicorn2.co_owner.name == "Joe"
assert unicorn2.max_persons == 50
buses = [Bus(**x) for x in (await client.get("/buses/")).json()]
assert len(buses) == 1
assert buses[0].name == "Unicorn"
@pytest.mark.asyncio
async def test_inheritance_with_m2m_relation():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
sam = Person(**(await client.post("/persons/", json={"name": "Sam"})).json())
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.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.model_dump()
)
).json()
)
unicorn = Bus2(
**(
await client.post(
f"/buses2/{unicorn.pk}/add_coowner/", json=alex.model_dump()
)
).json()
)
assert shelby.name == "Shelby wanna be"
assert shelby.owner.name == "Sam"
assert len(shelby.co_owners) == 0
assert shelby.max_capacity == 2000
assert unicorn.name == "Unicorn"
assert unicorn.owner == sam
assert unicorn.owner.name == "Sam"
assert unicorn.co_owners[0].name == "Joe"
assert unicorn.co_owners[1] == alex
assert unicorn.max_persons == 80
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.model_dump()
)
).json()
)
assert shelby.name == "Shelby wanna be"
assert shelby.owner.name == "Sam"
assert len(shelby.co_owners) == 2
assert shelby.co_owners[0] == alex
assert shelby.co_owners[1] == joe
assert shelby.max_capacity == 2000
buses = [Bus2(**x) for x in (await client.get("/buses2/")).json()]
assert len(buses) == 1
assert buses[0].name == "Unicorn"
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_inheritance_mixins_fastapi.py 0000664 0000000 0000000 00000005403 15211567312 0030164 0 ustar 00root root 0000000 0000000 import datetime
from typing import Optional
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
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))
class AuditMixin:
created_by: str = ormar.String(max_length=100)
updated_by: str = ormar.String(max_length=100, default="Sam")
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)
async def create_item(item: Subject):
return item
@app.post("/categories/", response_model=Category)
async def create_category(category: Category):
await category.save()
return category
@pytest.mark.asyncio
async def test_read_main():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
test_category = dict(name="Foo", code=123, created_by="Sam", updated_by="Max")
test_subject = dict(name="Bar")
response = await client.post("/categories/", json=test_category)
assert response.status_code == 200
cat = Category(**response.json())
assert cat.name == "Foo"
assert cat.created_by == "Sam"
assert cat.created_date is not None
assert cat.id == 1
cat_dict = cat.model_dump()
cat_dict["updated_date"] = cat_dict["updated_date"].strftime(
"%Y-%m-%d %H:%M:%S.%f"
)
cat_dict["created_date"] = cat_dict["created_date"].strftime(
"%Y-%m-%d %H:%M:%S.%f"
)
test_subject["category"] = cat_dict
response = await client.post("/subjects/", json=test_subject)
assert response.status_code == 200
sub = Subject(**response.json())
assert sub.name == "Bar"
assert sub.category.pk == cat.pk
assert isinstance(sub.updated_date, datetime.datetime)
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_json_field_fastapi.py 0000664 0000000 0000000 00000012403 15211567312 0026416 0 ustar 00root root 0000000 0000000 # type: ignore
import uuid
from typing import Optional
import pydantic
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
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))
class Thing(ormar.Model):
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()
@app.get("/things_with_sample", response_model=list[Thing])
async def read_things_sample():
await Thing(name="b", js=["asdf", "asdf", "bobby", "nigel"]).save()
await Thing(name="a", js='["lemon", "raspberry", "lime", "pumice"]').save()
return await Thing.objects.order_by("name").all()
@app.get("/things_with_sample_after_init", response_model=Thing)
async def read_things_init():
thing1 = Thing(js="{}")
thing1.name = "d"
thing1.js = ["js", "set", "after", "constructor"]
await thing1.save()
return thing1
@app.put("/update_thing", response_model=Thing)
async def update_things(thing: Thing):
thing.js = ["js", "set", "after", "update"] # type: ignore
await thing.update()
return thing
@app.post("/things", response_model=Thing)
async def create_things(thing: Thing):
thing = await thing.save()
return thing
@app.get("/things_untyped")
async def read_things_untyped():
return await Thing.objects.order_by("name").all()
@pytest.mark.asyncio
async def test_json_is_required_if_not_nullable():
with pytest.raises(pydantic.ValidationError):
Thing()
@pytest.mark.asyncio
async def test_json_is_not_required_if_nullable():
class Thing2(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="things2")
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4)
name: str = ormar.Text(default="")
js: Optional[pydantic.Json] = ormar.JSON(nullable=True)
Thing2()
@pytest.mark.asyncio
async def test_setting_values_after_init():
async with base_ormar_config.database:
t1 = Thing(id="67a82813-d90c-45ff-b546-b4e38d7030d7", name="t1", js=["thing1"])
assert '["thing1"]' in t1.model_dump_json()
await t1.save()
t1.model_dump_json()
assert '["thing1"]' in t1.model_dump_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)).model_dump_json()
@pytest.mark.asyncio
async def test_read_main():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
response = await client.get("/things_with_sample")
assert response.status_code == 200
# check if raw response not double encoded
assert '["lemon","raspberry","lime","pumice"]' in response.text
# parse json and check that we get lists not strings
resp = response.json()
assert resp[0].get("js") == ["lemon", "raspberry", "lime", "pumice"]
assert resp[1].get("js") == ["asdf", "asdf", "bobby", "nigel"]
# create a new one
response = await client.post(
"/things", json={"js": ["test", "test2"], "name": "c"}
)
assert response.json().get("js") == ["test", "test2"]
# get all with new one
response = await client.get("/things")
assert response.status_code == 200
assert '["test","test2"]' in response.text
resp = response.json()
assert resp[0].get("js") == ["lemon", "raspberry", "lime", "pumice"]
assert resp[1].get("js") == ["asdf", "asdf", "bobby", "nigel"]
assert resp[2].get("js") == ["test", "test2"]
response = await client.get("/things_with_sample_after_init")
assert response.status_code == 200
resp = response.json()
assert resp.get("js") == ["js", "set", "after", "constructor"]
# test new with after constructor
response = await client.get("/things")
resp = response.json()
assert resp[0].get("js") == ["lemon", "raspberry", "lime", "pumice"]
assert resp[1].get("js") == ["asdf", "asdf", "bobby", "nigel"]
assert resp[2].get("js") == ["test", "test2"]
assert resp[3].get("js") == ["js", "set", "after", "constructor"]
response = await client.put("/update_thing", json=resp[3])
assert response.status_code == 200
resp = response.json()
assert resp.get("js") == ["js", "set", "after", "update"]
# test new with after constructor
response = await client.get("/things_untyped")
resp = response.json()
assert resp[0].get("js") == ["lemon", "raspberry", "lime", "pumice"]
assert resp[1].get("js") == ["asdf", "asdf", "bobby", "nigel"]
assert resp[2].get("js") == ["test", "test2"]
assert resp[3].get("js") == ["js", "set", "after", "update"]
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_m2m_forwardref.py 0000664 0000000 0000000 00000004730 15211567312 0025513 0 ustar 00root root 0000000 0000000 from typing import ForwardRef, Optional
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from starlette import status
import ormar
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))
CityRef = ForwardRef("City")
CountryRef = ForwardRef("Country")
# models.py
class Country(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="countries")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=128, unique=True)
iso2: str = ormar.String(max_length=3)
iso3: str = ormar.String(max_length=4, unique=True)
population: int = ormar.Integer(maximum=10000000000)
demonym: str = ormar.String(max_length=128)
native_name: str = ormar.String(max_length=128)
capital: Optional[CityRef] = ormar.ForeignKey( # type: ignore
CityRef, related_name="capital_city", nullable=True
)
borders: list[Optional[CountryRef]] = ormar.ManyToMany( # type: ignore
CountryRef, nullable=True, skip_reverse=True
)
class City(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="cities")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=128)
country: Country = ormar.ForeignKey(
Country, related_name="cities", skip_reverse=True
)
Country.update_forward_refs()
create_test_database = init_tests(base_ormar_config)
@app.post("/", response_model=Country, status_code=status.HTTP_201_CREATED)
async def create_country(country: Country): # if this is ormar
result = await country.upsert() # it's already initialized as ormar model
return result
@pytest.mark.asyncio
async def test_payload():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
payload = {
"name": "Thailand",
"iso2": "TH",
"iso3": "THA",
"population": 23123123,
"demonym": "Thai",
"native_name": "Thailand",
}
resp = await client.post(
"/", json=payload, headers={"application-type": "json"}
)
# print(resp.content)
assert resp.status_code == 201
resp_country = Country(**resp.json())
assert resp_country.name == "Thailand"
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_more_reallife_fastapi.py 0000664 0000000 0000000 00000007554 15211567312 0027122 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
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))
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(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.get("/items", response_model=list[Item])
async def get_items():
items = await Item.objects.select_related("category").all()
return items
@app.get("/items/raw", response_model=list[Item])
async def get_raw_items():
items = await Item.objects.all()
return items
@app.post("/items", response_model=Item)
async def create_item(item: Item):
await item.save()
return item
@app.post("/categories", response_model=Category)
async def create_category(category: Category):
await category.save()
return category
@app.get("/items/{item_id}")
async def get_item(item_id: int):
item = await Item.objects.get(pk=item_id)
return item
@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.model_dump())
@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
item_db = await Item.objects.get(pk=item_id)
return {"deleted_rows": await item_db.delete()}
@pytest.mark.asyncio
async def test_all_endpoints():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
response = await client.post("/categories", json={"name": "test cat"})
category = response.json()
response = await client.post(
"/items", json={"name": "test", "id": 1, "category": category}
)
item = Item(**response.json())
assert item.pk is not None
response = await client.get("/items")
items = [Item(**item) for item in response.json()]
assert items[0] == item
item.name = "New name"
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()]
assert items[0].name == "New name"
response = await client.get("/items/raw")
items = [Item(**item) for item in response.json()]
assert items[0].name == "New name"
assert items[0].category.name is None
response = await client.get(f"/items/{item.pk}")
new_item = Item(**response.json())
assert new_item == item
response = await client.delete(f"/items/{item.pk}")
assert response.json().get("deleted_rows", "__UNDEFINED__") != "__UNDEFINED__"
response = await client.get("/items")
items = response.json()
assert len(items) == 0
await client.post(
"/items", json={"name": "test_2", "id": 2, "category": category}
)
response = await client.get("/items")
items = response.json()
assert len(items) == 1
item = Item(**items[0])
response = await client.delete(f"/items/{item.pk}")
assert response.json().get("deleted_rows", "__UNDEFINED__") != "__UNDEFINED__"
response = await client.get("/docs")
assert response.status_code == 200
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_nested_saving.py 0000664 0000000 0000000 00000011030 15211567312 0025417 0 ustar 00root root 0000000 0000000 from typing import Any, Optional, Union, cast
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
from ormar.queryset.utils import translate_list_to_dict
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"}
class Department(ormar.Model):
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):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
course_name: str = ormar.String(max_length=100)
completed: bool = ormar.Boolean()
department: Optional[Department] = ormar.ForeignKey(Department)
class Student(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
courses = ormar.ManyToMany(Course)
to_exclude = {
"id": ...,
"courses": {
"__all__": {"id": ..., "students": {"__all__": {"id", "studentcourse"}}}
},
}
exclude_all = {"id": ..., "courses": {"__all__"}}
to_exclude_ormar = {
"id": ...,
"courses": {"id": ..., "students": {"id", "studentcourse"}},
}
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():
to_exclude[key] = auto_exclude_id_field(to_exclude[key])
to_exclude["id"] = Ellipsis
return to_exclude
else:
return {"id"}
def generate_exclude_for_ids(model: type[ormar.Model]) -> dict:
to_exclude_base = translate_list_to_dict(model._iterate_related_models())
return cast(dict, auto_exclude_id_field(to_exclude=to_exclude_base))
to_exclude_auto = generate_exclude_for_ids(model=Department)
@app.post("/departments/", response_model=Department)
async def create_department(department: Department):
await department.save_related(follow=True, save_all=True)
return department
@app.get("/departments/{department_name}")
async def get_department(department_name: str):
department = await Department.objects.select_all(follow=True).get(
department_name=department_name
)
return department.model_dump(exclude=to_exclude)
@app.get("/departments/{department_name}/second")
async def get_department_exclude(department_name: str):
department = await Department.objects.select_all(follow=True).get(
department_name=department_name
)
return department.model_dump(exclude=to_exclude_ormar)
@app.get("/departments/{department_name}/exclude")
async def get_department_exclude_all(department_name: str):
department = await Department.objects.select_all(follow=True).get(
department_name=department_name
)
return department.model_dump(exclude=exclude_all)
@pytest.mark.asyncio
async def test_saving_related_in_fastapi():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
payload = {
"department_name": "Ormar",
"courses": [
{
"course_name": "basic1",
"completed": True,
"students": [{"name": "Jack"}, {"name": "Abi"}],
},
{
"course_name": "basic2",
"completed": True,
"students": [{"name": "Kate"}, {"name": "Miranda"}],
},
],
}
response = await client.post("/departments/", json=payload, headers=headers)
department = Department(**response.json())
assert department.id is not None
assert len(department.courses) == 2
assert department.department_name == "Ormar"
assert department.courses[0].course_name == "basic1"
assert department.courses[0].completed
assert department.courses[1].course_name == "basic2"
assert department.courses[1].completed
response = await client.get("/departments/Ormar")
response2 = await client.get("/departments/Ormar/second")
assert response.json() == response2.json() == payload
response3 = await client.get("/departments/Ormar/exclude")
assert response3.json() == {"department_name": "Ormar"}
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_recursion_error.py 0000664 0000000 0000000 00000007201 15211567312 0026015 0 ustar 00root root 0000000 0000000 import uuid
from datetime import datetime
from typing import Optional
import pytest
from asgi_lifespan import LifespanManager
from fastapi import Depends, FastAPI
from httpx import ASGITransport, AsyncClient
from pydantic import BaseModel, Json
import ormar
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"}
class User(ormar.Model):
"""
The user model
"""
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4)
email: str = ormar.String(unique=True, max_length=100)
username: str = ormar.String(unique=True, max_length=100)
password: str = ormar.String(unique=True, max_length=100)
verified: bool = ormar.Boolean(default=False)
verify_key: Optional[str] = ormar.String(unique=True, max_length=100, nullable=True)
created_at: datetime = ormar.DateTime(default=datetime.now())
ormar_config = base_ormar_config.copy(tablename="users")
class UserSession(ormar.Model):
"""
The user session model
"""
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4)
user: User = ormar.ForeignKey(User)
session_key: str = ormar.String(unique=True, max_length=64)
created_at: datetime = ormar.DateTime(default=datetime.now())
ormar_config = base_ormar_config.copy(tablename="user_sessions")
class QuizAnswer(BaseModel):
right: bool
answer: str
class QuizQuestion(BaseModel):
question: str
answers: list[QuizAnswer]
class QuizInput(BaseModel):
title: str
description: str
questions: list[QuizQuestion]
class Quiz(ormar.Model):
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4)
title: str = ormar.String(max_length=100)
description: Optional[str] = ormar.String(max_length=300, nullable=True)
created_at: datetime = ormar.DateTime(default=datetime.now())
updated_at: datetime = ormar.DateTime(default=datetime.now())
user_id: uuid.UUID = ormar.UUID(foreign_key=User.id)
questions: Json = ormar.JSON(nullable=False)
ormar_config = base_ormar_config.copy(tablename="quiz")
create_test_database = init_tests(base_ormar_config)
async def get_current_user():
return await User(email="mail@example.com", username="aa", password="pass").save()
@router.post("/create", response_model=Quiz)
async def create_quiz_lol(
quiz_input: QuizInput, user: User = Depends(get_current_user)
):
quiz = Quiz(**quiz_input.model_dump(), user_id=user.id)
return await quiz.save()
@pytest.mark.asyncio
async def test_quiz_creation():
transport = ASGITransport(app=router)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(router):
payload = {
"title": "Some test question",
"description": "A description",
"questions": [
{
"question": "Is ClassQuiz cool?",
"answers": [
{"right": True, "answer": "Yes"},
{"right": False, "answer": "No"},
],
},
{
"question": "Do you like open source?",
"answers": [
{"right": True, "answer": "Yes"},
{"right": False, "answer": "No"},
{"right": False, "answer": "Maybe"},
],
},
],
}
response = await client.post("/create", json=payload)
assert response.status_code == 200
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_relations_with_nested_defaults.py 0000664 0000000 0000000 00000007652 15211567312 0031071 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import pytest_asyncio
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
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))
class Country(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
rating: int = ormar.Integer(default=0)
country: Optional[Country] = ormar.ForeignKey(Country)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
title: str = ormar.String(max_length=100)
year: Optional[int] = ormar.Integer(nullable=True)
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture
async def sample_data():
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(
id=1, author=author, title="Bug caused by default value", year=2021
).save()
@app.get("/books/{book_id}", response_model=Book)
async def get_book_by_id(book_id: int):
book = await Book.objects.get(id=book_id)
return book
@app.get("/books_with_author/{book_id}", response_model=Book)
async def get_book_with_author_by_id(book_id: int):
book = await Book.objects.select_related("author").get(id=book_id)
return book
@pytest.mark.asyncio
async def test_related_with_defaults(sample_data):
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
response = await client.get("/books/1")
assert response.json() == {
"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,
}
response = await client.get("/books_with_author/1")
assert response.json() == {
"author": {
"books": [
{
"author": {"id": 1},
"id": 1,
"title": "Bug caused by default value",
"year": 2021,
}
],
"country": {
"authors": [
{
"books": [
{
"author": {"id": 1},
"id": 1,
"title": "Bug caused by default value",
"year": 2021,
}
],
"country": {"id": 1},
"id": 1,
"name": "bug",
"rating": 5,
}
],
"id": 1,
},
"id": 1,
"name": "bug",
"rating": 5,
},
"id": 1,
"title": "Bug caused by default value",
"year": 2021,
}
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_schema_not_allowed_params.py 0000664 0000000 0000000 00000001220 15211567312 0027760 0 ustar 00root root 0000000 0000000 import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
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.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}"
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_skip_reverse_models.py 0000664 0000000 0000000 00000010654 15211567312 0026645 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
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"}
class Author(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=80)
last_name: str = ormar.String(max_length=80)
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=40)
class Category2(Category):
model_config = dict(extra="forbid")
class Post(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories = ormar.ManyToMany(Category, skip_reverse=True)
author: Optional[Author] = ormar.ForeignKey(Author, skip_reverse=True)
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)
async def create_category(category: Category):
await category.save()
await category.save_related(follow=True, save_all=True)
return category
@app.post("/posts/", response_model=Post)
async def create_post(post: Post):
if post.author:
await post.author.save()
await post.save()
await post.save_related(follow=True, save_all=True)
for category in [cat for cat in post.categories]:
await post.categories.add(category)
return post
@app.get("/categories/", response_model=list[Category])
async def get_categories():
return await Category.objects.select_related("posts").all()
@app.get("/posts/", response_model=list[Post])
async def get_posts():
posts = await Post.objects.select_related(["categories", "author"]).all()
return posts
@pytest.mark.asyncio
async def test_queries():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client as client, LifespanManager(app):
right_category = {"name": "Test category"}
wrong_category = {"name": "Test category2", "posts": [{"title": "Test Post"}]}
# cannot add posts if skipped, will be ignored (with extra=ignore by default)
response = await client.post(
"/categories/", json=wrong_category, headers=headers
)
assert response.status_code == 200
response = await client.get("/categories/")
assert response.status_code == 200
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"
response = await client.post(
"/categories/", json=right_category, headers=headers
)
assert response.status_code == 200
response = await client.get("/categories/")
assert response.status_code == 200
categories = [Category(**x) for x in response.json()]
assert categories[1] is not None
assert categories[1].name == "Test category"
right_post = {
"title": "ok post",
"author": {"first_name": "John", "last_name": "Smith"},
"categories": [{"name": "New cat"}],
}
response = await client.post("/posts/", json=right_post, headers=headers)
assert response.status_code == 200
Category.model_config["extra"] = "allow"
response = await client.get("/posts/")
assert response.status_code == 200
posts = [Post(**x) for x in response.json()]
assert posts[0].title == "ok post"
assert posts[0].author.first_name == "John"
assert posts[0].categories[0].name == "New cat"
wrong_category = {"name": "Test category3", "posts": [{"title": "Test Post"}]}
# cannot add posts if skipped, will be error with extra forbid
assert Category2.model_config["extra"] == "forbid"
response = await client.post("/categories/forbid/", json=wrong_category)
assert response.status_code == 422
ormar-orm-ormar-a7e7fde/tests/test_fastapi/test_wekref_exclusion.py 0000664 0000000 0000000 00000010156 15211567312 0026152 0 ustar 00root root 0000000 0000000 from typing import Optional
from uuid import UUID, uuid4
import pydantic
import pytest
from asgi_lifespan import LifespanManager
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import ormar
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))
class OtherThing(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="other_things")
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.Text(default="")
ot_contents: str = ormar.Text(default="")
class Thing(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="things")
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.Text(default="")
js: Optional[pydantic.Json] = ormar.JSON(nullable=True)
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
ot = await OtherThing(ot_contents="otc").save()
await Thing(other_thing=ot, name="t1").save()
await Thing(other_thing=ot, name="t2").save()
await Thing(other_thing=ot, name="t3").save()
# if you do not care about returned object you can even go with bulk_create
# all of them are created in one transaction
# things = [Thing(other_thing=ot, name='t1'),
# Thing(other_thing=ot, name="t2"),
# Thing(other_thing=ot, name="t3")]
# await Thing.objects.bulk_create(things)
@app.get("/test/2", response_model=list[Thing])
async def get_test_2():
# if you only query for one use get or first
ot = await OtherThing.objects.get()
ts = await ot.things.all()
# specifically null out the relation on things before return
for t in ts:
t.remove(ot, name="other_thing")
return ts
@app.get("/test/3", response_model=list[Thing])
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.model_dump(exclude={"other_thing"}) for t in ot.things]
@app.get("/test/4", response_model=list[Thing], response_model_exclude={"other_thing"})
async def get_test_4():
ot = await OtherThing.objects.get()
# query from the active side
return await Thing.objects.all(other_thing=ot)
@app.get("/get_ot/", response_model=OtherThing)
async def get_ot():
return await OtherThing.objects.get()
# more real life (usually) is not getting some random OT and get it's Things
# but query for a specific one by some kind of id
@app.get(
"/test/5/{thing_id}",
response_model=list[Thing],
response_model_exclude={"other_thing"},
)
async def get_test_5(thing_id: UUID):
return await Thing.objects.all(other_thing__id=thing_id)
@app.get(
"/test/error", response_model=list[Thing], response_model_exclude={"other_thing"}
)
async def get_weakref():
ots = await OtherThing.objects.all()
ot = ots[0]
ts = await ot.things.all()
return ts
@pytest.mark.asyncio
async def test_endpoints():
transport = ASGITransport(app=app)
client = AsyncClient(transport=transport, base_url="http://testserver")
async with client, LifespanManager(app):
resp = await client.post("/test/1")
assert resp.status_code == 200
resp2 = await client.get("/test/2")
assert resp2.status_code == 200
assert len(resp2.json()) == 3
resp3 = await client.get("/test/3")
assert resp3.status_code == 200
assert len(resp3.json()) == 3
resp4 = await client.get("/test/4")
assert resp4.status_code == 200
assert len(resp4.json()) == 3
ot = OtherThing(**(await client.get("/get_ot/")).json())
resp5 = await client.get(f"/test/5/{ot.id}")
assert resp5.status_code == 200
assert len(resp5.json()) == 3
resp6 = await client.get("/test/error")
assert resp6.status_code == 200
assert len(resp6.json()) == 3
ormar-orm-ormar-a7e7fde/tests/test_hashes/ 0000775 0000000 0000000 00000000000 15211567312 0021006 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_hashes/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0023105 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_hashes/test_many_to_many.py 0000664 0000000 0000000 00000005467 15211567312 0025125 0 ustar 00root root 0000000 0000000 from typing import ForwardRef, Optional
import pytest
import pytest_asyncio
import sqlalchemy
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config(force_rollback=True)
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=40)
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=40)
class Post(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="posts")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[list[Category]] = ormar.ManyToMany(Category)
authors: Optional[list[Author]] = ormar.ManyToMany(
Author, through=ForwardRef("AuthorXPosts")
)
class AuthorXPosts(ormar.Model):
ormar_config = base_ormar_config.copy(
tablename="authors_x_posts", constraints=[ormar.UniqueColumns("author", "post")]
)
id: int = ormar.Integer(primary_key=True)
author: Optional[int] = ormar.Integer(default=None)
post: Optional[int] = ormar.Integer(default=None)
Post.update_forward_refs()
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(scope="function", autouse=True)
async def cleanup():
yield
async with base_ormar_config.database:
await Post.ormar_config.model_fields["categories"].through.objects.delete(
each=True
)
await Post.ormar_config.model_fields["authors"].through.objects.delete(
each=True
)
await Post.objects.delete(each=True)
await Category.objects.delete(each=True)
await Author.objects.delete(each=True)
@pytest.mark.asyncio
async def test_adding_same_m2m_model_twice():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
post = await Post.objects.create(title="Hello, M2M")
news = await Category(name="News").save()
await post.categories.add(news)
await post.categories.add(news)
categories = await post.categories.all()
assert categories == [news]
@pytest.mark.asyncio
async def test_adding_same_m2m_model_twice_with_unique():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
post = await Post.objects.create(title="Hello, M2M")
redactor = await Author(name="News").save()
await post.authors.add(redactor)
with pytest.raises((sqlalchemy.exc.IntegrityError)):
await post.authors.add(redactor)
ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation/ 0000775 0000000 0000000 00000000000 15211567312 0026754 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0031053 0 ustar 00root root 0000000 0000000 test_foreign_key_name_in_inheritance.py 0000664 0000000 0000000 00000002413 15211567312 0036646 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class FkInhParent(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="fk_inh_parents")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class FkInhBase(ormar.Model):
ormar_config = base_ormar_config.copy(abstract=True)
id: int = ormar.Integer(primary_key=True)
parent = ormar.ForeignKey(
FkInhParent,
related_name="kids",
foreign_key_name="fk_custom_parent",
)
class FkInhChildA(FkInhBase):
ormar_config = base_ormar_config.copy(tablename="fk_inh_child_a")
class FkInhChildB(FkInhBase):
ormar_config = base_ormar_config.copy(tablename="fk_inh_child_b")
create_test_database = init_tests(base_ormar_config)
def _fk_names(table):
return [fk.name for col in table.c for fk in col.foreign_keys]
def test_foreign_key_name_is_suffixed_per_subclass_to_avoid_conflicts():
a_names = _fk_names(FkInhChildA.ormar_config.table)
b_names = _fk_names(FkInhChildB.ormar_config.table)
assert "fk_custom_parent_fk_inh_child_a" in a_names
assert "fk_custom_parent_fk_inh_child_b" in b_names
assert set(a_names).isdisjoint(set(b_names))
test_geting_pydantic_models.py 0000664 0000000 0000000 00000032156 15211567312 0035030 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation from typing import ForwardRef, Optional
import pydantic
from pydantic import computed_field
from pydantic_core import PydanticUndefined
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
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")
SelfRef.update_forward_refs()
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)
class CommentTopic(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="comment_topics")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
@computed_field
def upper_name(self) -> str:
return self.name.upper()
class Comment(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="comments")
id: int = ormar.Integer(primary_key=True)
body: str = ormar.String(max_length=200, default="")
topic: Optional[CommentTopic] = ormar.ForeignKey(CommentTopic, nullable=True)
@computed_field
def shouted_body(self) -> str:
return self.body.upper()
@computed_field
def trimmed_body(self) -> str:
return self.body.strip()
class MutualA(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="mutual_b")
id: int = ormar.Integer(primary_key=True)
name = ormar.String(max_length=100, default="test")
mutual_a = ormar.ForeignKey(MutualA, related_name="mutuals_b")
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.model_fields.keys()} == {"items", "id", "name"}
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.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.__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.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():
data = {
"id": 1,
"name": "test",
"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.model_dump() == data
data = {"id": 1, "name": "test"}
cat = PydanticCategory(**data)
assert cat.model_dump() == {**data, "items": None}
def test_getting_pydantic_model_include():
PydanticCategory = Category.get_pydantic(include={"id", "name"})
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.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.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.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.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.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.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.model_fields) == 4
assert set(PydanticSelfRef.model_fields.keys()) == {
"id",
"name",
"parent",
"children",
}
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"}
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.model_fields) == 4
assert set(PydanticSelfRef.model_fields.keys()) == {
"id",
"name",
"parent",
"children",
}
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.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.model_fields) == 3
assert set(MutualAPydantic.model_fields.keys()) == {"id", "mutual_b", "mutuals_b"}
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.model_fields) == 3
assert set(MutualAPydantic.model_fields.keys()) == {"id", "mutual_b", "mutuals_b"}
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.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
def test_getting_pydantic_model_includes_computed_fields():
PydanticComment = Comment.get_pydantic()
assert set(PydanticComment.model_computed_fields.keys()) == {
"shouted_body",
"trimmed_body",
}
instance = PydanticComment(id=1, body=" hi ")
dumped = instance.model_dump()
assert dumped["shouted_body"] == " HI "
assert dumped["trimmed_body"] == "hi"
def test_getting_pydantic_model_excludes_computed_field_by_name():
PydanticComment = Comment.get_pydantic(exclude={"trimmed_body"})
assert set(PydanticComment.model_computed_fields.keys()) == {"shouted_body"}
instance = PydanticComment(id=1, body=" hi ")
dumped = instance.model_dump()
assert "trimmed_body" not in dumped
assert dumped["shouted_body"] == " HI "
def test_getting_pydantic_model_include_filters_computed_fields():
PydanticComment = Comment.get_pydantic(include={"id", "body", "shouted_body"})
assert set(PydanticComment.model_fields.keys()) == {"id", "body"}
assert set(PydanticComment.model_computed_fields.keys()) == {"shouted_body"}
def test_getting_pydantic_model_propagates_computed_fields_through_relations():
PydanticComment = Comment.get_pydantic()
InnerTopic = PydanticComment.__pydantic_core_schema__["schema"]["fields"]["topic"][
"schema"
]["schema"]["schema"]["cls"]
assert set(InnerTopic.model_computed_fields.keys()) == {"upper_name"}
inner = InnerTopic(id=1, name="news")
assert inner.model_dump()["upper_name"] == "NEWS"
ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py 0000664 0000000 0000000 00000046011 15211567312 0034542 0 ustar 00root root 0000000 0000000 import datetime
from collections import Counter
from typing import Optional
import pydantic
import pytest
import sqlalchemy as sa
from pydantic import computed_field
import ormar
import ormar.fields.constraints
from ormar import ModelDefinitionError
from ormar.exceptions import ModelError
from ormar.models.metaclass import get_constraint_copy
from ormar.relations.relation_proxy import RelationProxy
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
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}"
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 Category(DateFieldsModel, AuditModel):
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()
@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 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 Truck(Car):
ormar_config = ormar.OrmarConfig()
max_capacity: int = ormar.Integer()
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 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 Truck2(Car2):
ormar_config = base_ormar_config.copy(tablename="trucks2")
max_capacity: int = ormar.Integer()
class Bus2(Car2):
ormar_config = base_ormar_config.copy(tablename="buses2")
max_persons: int = ormar.Integer()
class ImmutablePerson(Person):
model_config = dict(frozen=True, validate_assignment=False)
create_test_database = init_tests(base_ormar_config)
def test_init_of_abstract_model() -> None:
with pytest.raises(ModelError):
DateFieldsModel()
def test_duplicated_related_name_on_different_model() -> None:
with pytest.raises(ModelDefinitionError):
class Bus3(Car2): # pragma: no cover
ormar_config = ormar.OrmarConfig(tablename="buses3")
owner: Person = ormar.ForeignKey(Person, related_name="buses")
max_persons: int = ormar.Integer()
def test_field_redefining_in_concrete_models() -> None:
class RedefinedField(DateFieldsModel):
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",
) # type: ignore
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.ormar_config.table.columns
)
assert isinstance(
RedefinedField.ormar_config.table.columns["creation_date"].type,
sa.sql.sqltypes.String,
)
def test_model_subclassing_that_redefines_constraints_column_names() -> None:
with pytest.raises(ModelDefinitionError):
class WrongField2(DateFieldsModel): # pragma: no cover
ormar_config = base_ormar_config.copy(tablename="wrongs")
id: int = ormar.Integer(primary_key=True)
created_date: str = ormar.String(max_length=200) # type: ignore
def test_model_subclassing_non_abstract_raises_error() -> None:
with pytest.raises(ModelDefinitionError):
class WrongField2(DateFieldsModelNoSubclass): # pragma: no cover
ormar_config = base_ormar_config.copy(tablename="wrongs")
id: int = ormar.Integer(primary_key=True)
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.ormar_config.constraints))
assert constraints[ormar.fields.constraints.UniqueColumns] == 2
assert constraints[ormar.fields.constraints.IndexColumns] == 0
assert constraints[ormar.fields.constraints.CheckColumns] == 1
def round_date_to_seconds(
date: datetime.datetime,
) -> datetime.datetime: # pragma: no cover
if date.microsecond >= 500000:
date = date + datetime.timedelta(seconds=1)
return date.replace(microsecond=0)
@pytest.mark.asyncio
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"]
mixin_db_columns = ["creation_date", "modification_date"]
mixin2_columns = ["created_by", "updated_by"]
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.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.ormar_config.model_fields for field in mixin2_columns
)
assert all(
field not in Subject.ormar_config.model_fields
for field in mixin2_columns
)
async with base_ormar_config.engine.connect() as conn:
table_names = await conn.run_sync(
lambda sync_conn: sa.inspect(sync_conn).get_table_names()
)
categories_column_names = await conn.run_sync(
lambda sync_conn: sa.inspect(sync_conn).get_columns("categories")
)
subjects_column_names = await conn.run_sync(
lambda sync_conn: sa.inspect(sync_conn).get_columns("subjects")
)
assert "categories" in table_names
table_columns = [x.get("name") for x in categories_column_names]
assert all(
col in table_columns for col in mixin_db_columns
) # + mixin2_columns)
assert "subjects" in table_names
table_columns = [x.get("name") for x in subjects_column_names]
assert all(col in table_columns for col in mixin_db_columns)
sub2 = (
await Subject.objects.select_related("category")
.order_by("-created_date")
.exclude_fields("updated_date")
.get()
)
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
) == round_date_to_seconds(cat.created_date)
assert sub2.updated_date is None
assert sub2.category.created_by == "Sam"
assert sub2.category.updated_by == cat.updated_by
sub3 = (
await Subject.objects.prefetch_related("category")
.order_by("-created_date")
.exclude_fields({"updated_date": ..., "category": {"updated_date"}})
.get()
)
assert round_date_to_seconds(sub3.created_date) == round_date_to_seconds(
sub.created_date
)
assert sub3.category.updated_date is None
assert round_date_to_seconds(
sub3.category.created_date
) == round_date_to_seconds(cat.created_date)
assert sub3.updated_date is None
assert sub3.category.created_by == "Sam"
assert sub3.category.updated_by == cat.updated_by
@pytest.mark.asyncio
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(
name="Shelby wanna be", max_capacity=1400, owner=sam, co_owner=joe
).save()
await Bus(name="Unicorn", max_persons=50, owner=sam, co_owner=joe).save()
shelby = await Truck.objects.select_related(["owner", "co_owner"]).get()
assert shelby.name == "Shelby wanna be"
assert shelby.owner.name == "Sam"
assert shelby.co_owner.name == "Joe"
assert shelby.max_capacity == 1400
unicorn = await Bus.objects.select_related(["owner", "co_owner"]).get()
assert unicorn.name == "Unicorn"
assert unicorn.owner.name == "Sam"
assert unicorn.co_owner.name == "Joe"
assert unicorn.max_persons == 50
joe_check = await Person.objects.select_related(
["coowned_trucks", "coowned_buses"]
).get(name="Joe")
assert joe_check.pk == joe.pk
assert joe_check.coowned_trucks[0] == shelby
assert joe_check.coowned_trucks[0].created_date is not None
assert joe_check.coowned_buses[0] == unicorn
assert joe_check.coowned_buses[0].created_date is not None
joe_check = (
await Person.objects.exclude_fields(
{
"coowned_trucks": {"created_date"},
"coowned_buses": {"created_date"},
}
)
.prefetch_related(["coowned_trucks", "coowned_buses"])
.get(name="Joe")
)
assert joe_check.pk == joe.pk
assert joe_check.coowned_trucks[0] == shelby
assert joe_check.coowned_trucks[0].created_date is None
assert joe_check.coowned_buses[0] == unicorn
assert joe_check.coowned_buses[0].created_date is None
@pytest.mark.asyncio
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()
truck = await Truck2(
name="Shelby wanna be 2", max_capacity=1400, owner=sam
).save()
await truck.co_owners.add(joe)
await truck.co_owners.add(alex)
bus3 = await Bus2(name="Unicorn 3", max_persons=30, owner=joe).save()
await bus3.co_owners.add(sam)
bus = await Bus2(name="Unicorn 2", max_persons=50, owner=sam).save()
await bus.co_owners.add(joe)
await bus.co_owners.add(alex)
shelby = await Truck2.objects.select_related(["owner", "co_owners"]).get()
assert shelby.name == "Shelby wanna be 2"
assert shelby.owner.name == "Sam"
assert shelby.co_owners[0].name == "Joe"
assert len(shelby.co_owners) == 2
assert shelby.max_capacity == 1400
unicorn = await Bus2.objects.select_related(["owner", "co_owners"]).get(
name="Unicorn 2"
)
assert unicorn.name == "Unicorn 2"
assert unicorn.owner.name == "Sam"
assert unicorn.co_owners[0].name == "Joe"
assert len(unicorn.co_owners) == 2
assert unicorn.max_persons == 50
unicorn = (
await Bus2.objects.select_related(["owner", "co_owners"])
.order_by("-co_owners__name")
.get()
)
assert unicorn.name == "Unicorn 2"
assert unicorn.owner.name == "Sam"
assert len(unicorn.co_owners) == 2
assert unicorn.co_owners[0].name == "Joe"
unicorn = (
await Bus2.objects.select_related(["owner", "co_owners"])
.order_by("co_owners__name")
.get()
)
assert unicorn.name == "Unicorn 2"
assert unicorn.owner.name == "Sam"
assert len(unicorn.co_owners) == 2
assert unicorn.co_owners[0].name == "Alex"
joe_check = await Person.objects.select_related(
["coowned_trucks2", "coowned_buses2"]
).get(name="Joe")
assert joe_check.pk == joe.pk
assert joe_check.coowned_trucks2[0] == shelby
assert joe_check.coowned_trucks2[0].created_date is not None
assert joe_check.coowned_buses2[0] == unicorn
assert joe_check.coowned_buses2[0].created_date is not None
joe_check = (
await Person.objects.exclude_fields(
{
"coowned_trucks2": {"created_date"},
"coowned_buses2": {"created_date"},
}
)
.prefetch_related(["coowned_trucks2", "coowned_buses2"])
.get(name="Joe")
)
assert joe_check.pk == joe.pk
assert joe_check.coowned_trucks2[0] == shelby
assert joe_check.coowned_trucks2[0].created_date is None
assert joe_check.coowned_buses2[0] == unicorn
assert joe_check.coowned_buses2[0].created_date is None
await shelby.co_owners.remove(joe)
await shelby.co_owners.remove(alex)
await Truck2.objects.delete(name="Shelby wanna be 2")
unicorn = (
await Bus2.objects.select_related(["owner", "co_owners"])
.filter(co_owners__name="Joe")
.get()
)
assert unicorn.name == "Unicorn 2"
assert unicorn.owner.name == "Sam"
assert unicorn.co_owners[0].name == "Joe"
assert len(unicorn.co_owners) == 1
assert unicorn.max_persons == 50
unicorn = (
await Bus2.objects.select_related(["owner", "co_owners"])
.exclude(co_owners__name="Joe")
.get()
)
assert unicorn.name == "Unicorn 2"
assert unicorn.owner.name == "Sam"
assert unicorn.co_owners[0].name == "Alex"
assert len(unicorn.co_owners) == 1
assert unicorn.max_persons == 50
unicorn = await Bus2.objects.get()
assert unicorn.name == "Unicorn 2"
assert unicorn.owner.name is None
assert len(unicorn.co_owners) == 0
await unicorn.co_owners.all()
assert len(unicorn.co_owners) == 2
assert unicorn.co_owners[0].name == "Joe"
await unicorn.owner.load()
assert unicorn.owner.name == "Sam"
unicorns = (
await Bus2.objects.select_related(["owner", "co_owners"])
.filter(name__contains="Unicorn")
.order_by("-name")
.all()
)
assert unicorns[0].name == "Unicorn 3"
assert unicorns[0].owner.name == "Joe"
assert len(unicorns[0].co_owners) == 1
assert unicorns[0].co_owners[0].name == "Sam"
assert unicorns[1].name == "Unicorn 2"
assert unicorns[1].owner.name == "Sam"
assert len(unicorns[1].co_owners) == 2
assert unicorns[1].co_owners[0].name == "Joe"
unicorns = (
await Bus2.objects.select_related(["owner", "co_owners"])
.filter(name__contains="Unicorn")
.order_by("-name")
.limit(2, limit_raw_sql=True)
.all()
)
assert len(unicorns) == 2
assert unicorns[1].name == "Unicorn 2"
assert len(unicorns[1].co_owners) == 1
def test_custom_config() -> None:
# Custom config inherits defaults
assert ImmutablePerson.model_config["from_attributes"] is True
# Custom config can override defaults
assert ImmutablePerson.model_config["validate_assignment"] is False
sam = ImmutablePerson(name="Sam")
with pytest.raises(pydantic.ValidationError):
sam.name = "Not Sam"
def test_get_constraint_copy() -> None:
with pytest.raises(ValueError):
get_constraint_copy("INVALID CONSTRAINT") # type: ignore
ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation/test_inheritance_mixins.py 0000664 0000000 0000000 00000013226 15211567312 0034251 0 ustar 00root root 0000000 0000000 import datetime
from typing import Optional
import pytest
import sqlalchemy as sa
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class AuditMixin:
created_by: str = ormar.String(max_length=100)
updated_by: str = ormar.String(max_length=100, default="Sam")
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)
def test_field_redefining() -> None:
class RedefinedField(ormar.Model, DateFieldsMixins):
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.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
)
def test_field_redefining_in_second() -> None:
class RedefinedField2(ormar.Model, DateFieldsMixins):
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",
) # type: ignore
assert (
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 isinstance(
RedefinedField2.ormar_config.table.columns["creation_date"].type,
sa.sql.sqltypes.String,
)
def round_date_to_seconds(
date: datetime.datetime,
) -> datetime.datetime: # pragma: no cover
if date.microsecond >= 500000:
date = date + datetime.timedelta(seconds=1)
return date.replace(microsecond=0)
@pytest.mark.asyncio
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.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.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.ormar_config.model_fields for field in mixin2_columns
)
assert all(
field not in Subject.ormar_config.model_fields
for field in mixin2_columns
)
async with base_ormar_config.engine.connect() as conn:
table_names = await conn.run_sync(
lambda sync_conn: sa.inspect(sync_conn).get_table_names()
)
categories_column_names = await conn.run_sync(
lambda sync_conn: sa.inspect(sync_conn).get_columns("categories")
)
subjects_column_names = await conn.run_sync(
lambda sync_conn: sa.inspect(sync_conn).get_columns("subjects")
)
assert "categories" in table_names
table_columns = [x.get("name") for x in categories_column_names]
assert all(col in table_columns for col in mixin_columns + mixin2_columns)
assert "subjects" in table_names
table_columns = [x.get("name") for x in subjects_column_names]
assert all(col in table_columns for col in mixin_columns)
sub2 = (
await Subject.objects.select_related("category")
.order_by("-created_date")
.exclude_fields("updated_date")
.get()
)
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
) == round_date_to_seconds(cat.created_date)
assert sub2.updated_date is None
assert sub2.category.created_by == "Sam"
assert sub2.category.updated_by == cat.updated_by
test_inheritance_of_property_fields.py 0000664 0000000 0000000 00000003600 15211567312 0036554 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation from pydantic import computed_field
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class BaseFoo(ormar.Model):
ormar_config = base_ormar_config.copy(abstract=True)
name: str = ormar.String(max_length=100)
@computed_field
def prefixed_name(self) -> str:
return "prefix_" + self.name
class Foo(BaseFoo):
ormar_config = base_ormar_config.copy()
@computed_field
def double_prefixed_name(self) -> str:
return "prefix2_" + self.name
id: int = ormar.Integer(primary_key=True)
class Bar(BaseFoo):
ormar_config = base_ormar_config.copy()
@computed_field
def prefixed_name(self) -> str:
return "baz_" + self.name
id: int = ormar.Integer(primary_key=True)
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.model_dump() == {
"name": "foo",
"id": None,
"double_prefixed_name": "prefix2_foo",
"prefixed_name": "prefix_foo",
}
bar = Bar(name="bar")
assert bar.prefixed_name == "baz_bar"
assert bar.model_dump() == {"name": "bar", "id": None, "prefixed_name": "baz_bar"}
def test_inherited_property_fields_are_exposed_in_get_pydantic():
PydanticFoo = Foo.get_pydantic()
assert set(PydanticFoo.model_computed_fields.keys()) == {
"prefixed_name",
"double_prefixed_name",
}
foo_dump = PydanticFoo(name="foo").model_dump()
assert foo_dump["prefixed_name"] == "prefix_foo"
assert foo_dump["double_prefixed_name"] == "prefix2_foo"
PydanticBar = Bar.get_pydantic()
assert set(PydanticBar.model_computed_fields.keys()) == {"prefixed_name"}
bar_dump = PydanticBar(name="bar").model_dump()
assert bar_dump["prefixed_name"] == "baz_bar"
test_inheritance_proxy_models.py 0000664 0000000 0000000 00000022050 15211567312 0035402 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation """Tests for proxy model inheritance: child shares the parent table/columns/pk."""
from datetime import datetime
from typing import Optional
import pytest
from pydantic import computed_field
import ormar
from ormar import ModelDefinitionError
from ormar.queryset import QuerySet
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
ONUPDATE_CALLABLE_RESULT = "callable-on-update"
ONUPDATE_TIMESTAMP = datetime(2030, 6, 15, 12, 0, 0)
def _make_callable_value() -> str:
return ONUPDATE_CALLABLE_RESULT
def _make_timestamp() -> datetime:
return ONUPDATE_TIMESTAMP
class Human(ormar.Model):
"""Concrete base model with a real table."""
ormar_config = base_ormar_config.copy(tablename="humans")
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=50)
last_name: str = ormar.String(max_length=50)
class CustomQuerySet(QuerySet):
"""Marker queryset to verify proxy can override queryset_class."""
class User(Human):
"""Proxy model adding a method without a separate table."""
ormar_config = base_ormar_config.copy(proxy=True, queryset_class=CustomQuerySet)
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
class Admin(User):
"""Two-level proxy chain — should still resolve back to Human's table."""
ormar_config = base_ormar_config.copy(proxy=True)
def label(self) -> str:
return f"admin:{self.full_name()}"
class HumanWithComputed(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="humans_computed")
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=50)
last_name: str = ormar.String(max_length=50)
class UserWithComputed(HumanWithComputed):
ormar_config = base_ormar_config.copy(proxy=True)
@computed_field
def display_name(self) -> str:
return f"{self.first_name} {self.last_name}".upper()
class Audited(ormar.Model):
"""Parent with on_update fields — used to verify on_update fires via proxy."""
ormar_config = base_ormar_config.copy(tablename="audited")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50)
revision: int = ormar.Integer(default=0, on_update=99)
note: str = ormar.String(
max_length=50, default="initial", on_update=_make_callable_value
)
updated_at: Optional[datetime] = ormar.DateTime(
default=None, nullable=True, on_update=_make_timestamp
)
class AuditedProxy(Audited):
"""Proxy that should inherit the parent's on_update behavior."""
ormar_config = base_ormar_config.copy(proxy=True)
create_test_database = init_tests(base_ormar_config)
def test_proxy_shares_parent_table() -> None:
assert User.ormar_config.table is Human.ormar_config.table
assert User.ormar_config.tablename == Human.ormar_config.tablename
assert User.ormar_config.pkname == Human.ormar_config.pkname
assert User.ormar_config.columns is Human.ormar_config.columns
assert User.ormar_config.model_fields is Human.ormar_config.model_fields
assert User.ormar_config.metadata is Human.ormar_config.metadata
assert User.ormar_config.database is Human.ormar_config.database
assert User.ormar_config.proxy is True
assert Human.ormar_config.proxy is False
def test_proxy_signals_are_independent() -> None:
assert User.ormar_config.signals is not Human.ormar_config.signals
def test_proxy_uses_overridden_queryset_class() -> None:
assert isinstance(User.objects, CustomQuerySet)
assert not isinstance(Human.objects, CustomQuerySet)
def test_two_level_proxy_chain_shares_root_table() -> None:
assert Admin.ormar_config.table is Human.ormar_config.table
assert Admin.ormar_config.proxy is True
@pytest.mark.asyncio
async def test_proxy_can_query_parent_rows() -> None:
async with base_ormar_config.database:
await Human.objects.create(first_name="foo", last_name="bar")
users = await User.objects.all()
assert len(users) == 1
assert isinstance(users[0], User)
assert users[0].full_name() == "foo bar"
admins = await Admin.objects.all()
assert len(admins) == 1
assert isinstance(admins[0], Admin)
assert admins[0].label() == "admin:foo bar"
@pytest.mark.asyncio
async def test_proxy_save_persists_to_shared_table() -> None:
async with base_ormar_config.database:
await User.objects.create(first_name="alice", last_name="liddell")
humans = await Human.objects.filter(first_name="alice").all()
assert len(humans) == 1
assert humans[0].last_name == "liddell"
assert isinstance(humans[0], Human)
def test_proxy_cannot_add_new_fields() -> None:
with pytest.raises(ModelDefinitionError, match="cannot declare new ormar fields"):
class BadProxy(Human): # pragma: no cover
ormar_config = base_ormar_config.copy(proxy=True)
extra: str = ormar.String(max_length=10)
def test_proxy_cannot_inherit_from_abstract() -> None:
class AbstractBase(ormar.Model):
ormar_config = base_ormar_config.copy(abstract=True)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=10)
with pytest.raises(ModelDefinitionError, match="cannot inherit from abstract"):
class BadProxy(AbstractBase): # pragma: no cover
ormar_config = base_ormar_config.copy(proxy=True)
def test_proxy_and_abstract_are_mutually_exclusive() -> None:
with pytest.raises(ModelDefinitionError, match="cannot be both proxy and abstract"):
class BadProxy(Human): # pragma: no cover
ormar_config = base_ormar_config.copy(proxy=True, abstract=True)
def test_proxy_property_field_extends_parent() -> None:
user = UserWithComputed(id=1, first_name="alice", last_name="liddell")
dumped = user.model_dump()
assert dumped["display_name"] == "ALICE LIDDELL"
base = HumanWithComputed(id=1, first_name="alice", last_name="liddell")
assert "display_name" not in base.model_dump()
def test_proxy_copy_preserves_proxy_flag() -> None:
cfg = base_ormar_config.copy(proxy=True)
again = cfg.copy()
assert again.proxy is True
def test_proxy_without_concrete_parent_rejected() -> None:
with pytest.raises(ModelDefinitionError, match="no concrete ormar parent"):
class FloatingProxy(ormar.Model): # pragma: no cover
ormar_config = base_ormar_config.copy(proxy=True)
def test_proxy_with_multiple_concrete_bases_different_tables_rejected() -> None:
class TableA(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="proxy_a")
id: int = ormar.Integer(primary_key=True)
label: str = ormar.String(max_length=10)
class TableB(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="proxy_b")
id: int = ormar.Integer(primary_key=True)
label: str = ormar.String(max_length=10)
with pytest.raises(
ModelDefinitionError,
match="cannot inherit from multiple concrete ormar models",
):
class CrossProxy(TableA, TableB): # pragma: no cover
ormar_config = base_ormar_config.copy(proxy=True)
def test_proxy_inherits_parent_onupdate_field_set() -> None:
assert AuditedProxy._onupdate_fields == Audited._onupdate_fields
assert {"revision", "note", "updated_at"} <= AuditedProxy._onupdate_fields
@pytest.mark.asyncio
async def test_proxy_update_applies_parent_static_on_update() -> None:
async with base_ormar_config.database:
record = await AuditedProxy.objects.create(name="alpha")
assert record.revision == 0
await record.update()
refreshed = await Audited.objects.get(id=record.id)
assert refreshed.revision == 99
@pytest.mark.asyncio
async def test_proxy_update_invokes_parent_callable_on_update() -> None:
async with base_ormar_config.database:
record = await AuditedProxy.objects.create(name="beta")
assert record.note == "initial"
assert record.updated_at is None
await record.update()
refreshed = await Audited.objects.get(id=record.id)
assert refreshed.note == ONUPDATE_CALLABLE_RESULT
assert refreshed.updated_at == ONUPDATE_TIMESTAMP
@pytest.mark.asyncio
async def test_proxy_update_with_explicit_value_skips_on_update() -> None:
async with base_ormar_config.database:
record = await AuditedProxy.objects.create(name="gamma")
await record.update(revision=7)
refreshed = await Audited.objects.get(id=record.id)
assert refreshed.revision == 7
assert refreshed.note == ONUPDATE_CALLABLE_RESULT
@pytest.mark.asyncio
async def test_parent_update_unaffected_by_proxy_definition() -> None:
"""Defining a proxy must not regress on_update behavior on the parent class."""
async with base_ormar_config.database:
record = await Audited.objects.create(name="delta")
await record.update()
refreshed = await Audited.objects.get(id=record.id)
assert refreshed.revision == 99
assert refreshed.note == ONUPDATE_CALLABLE_RESULT
test_inheritance_with_default.py 0000664 0000000 0000000 00000002756 15211567312 0035350 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation import datetime
import uuid
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class BaseModel(ormar.Model):
ormar_config = base_ormar_config.copy(abstract=True)
id: uuid.UUID = ormar.UUID(
primary_key=True, default=uuid.uuid4, uuid_format="string"
)
created_at: datetime.datetime = ormar.DateTime(default=datetime.datetime.utcnow())
updated_at: datetime.datetime = ormar.DateTime(default=datetime.datetime.utcnow())
class Member(BaseModel):
ormar_config = base_ormar_config.copy(tablename="members")
first_name: str = ormar.String(max_length=50)
last_name: str = ormar.String(max_length=50)
create_test_database = init_tests(base_ormar_config)
def test_model_structure():
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.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 base_ormar_config.database:
await Member(first_name="foo", last_name="bar").save()
await Member.objects.create(first_name="foo", last_name="bar")
test_inherited_class_is_not_abstract_by_default.py 0000664 0000000 0000000 00000002570 15211567312 0041106 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation import datetime
from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class TableBase(ormar.Model):
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")
created_at: datetime.datetime = ormar.DateTime(
timezone=True, default=datetime.datetime.now
)
last_modified_by: Optional[str] = ormar.String(max_length=20, nullable=True)
last_modified_at: Optional[datetime.datetime] = ormar.DateTime(
timezone=True, nullable=True
)
class NationBase(ormar.Model):
ormar_config = base_ormar_config.copy(abstract=True)
name: str = ormar.String(max_length=50)
alpha2_code: str = ormar.String(max_length=2)
region: str = ormar.String(max_length=30)
subregion: str = ormar.String(max_length=30)
class Nation(NationBase, TableBase):
ormar_config = base_ormar_config.copy()
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_model_is_not_abstract_by_default():
async with base_ormar_config.database:
sweden = await Nation(
name="Sweden", alpha2_code="SE", region="Europe", subregion="Scandinavia"
).save()
assert sweden.id is not None
test_nested_models_pydantic.py 0000664 0000000 0000000 00000004017 15211567312 0035030 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Library(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Package(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
library: Library = ormar.ForeignKey(Library, related_name="packages")
version: str = ormar.String(max_length=100)
class Ticket(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
number: int = ormar.Integer()
status: str = ormar.String(max_length=100)
class TicketPackage(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
status: str = ormar.String(max_length=100)
ticket: Ticket = ormar.ForeignKey(Ticket, related_name="packages")
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.model_fields
PydanticPackage = TicketPackageOut.__pydantic_core_schema__["schema"]["fields"][
"package"
]["schema"]["schema"]["schema"]["cls"]
assert "library" in PydanticPackage.model_fields
def test_casts_properly():
payload = {
"id": 0,
"status": "string",
"ticket": {"id": 0, "number": 0, "status": "string"},
"package": {
"version": "string",
"id": 0,
"library": {"id": 0, "name": "string"},
},
}
test_package = TicketPackage(**payload)
TicketPackageOut = TicketPackage.get_pydantic(exclude={"ticket"})
parsed = TicketPackageOut(**test_package.model_dump()).model_dump()
assert "ticket" not in parsed
assert "package" in parsed
assert "library" in parsed.get("package")
ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation/test_pydantic_fields_order.py0000664 0000000 0000000 00000001244 15211567312 0034722 0 ustar 00root root 0000000 0000000 import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class NewTestModel(ormar.Model):
ormar_config = base_ormar_config.copy()
a: int = ormar.Integer(primary_key=True)
b: str = ormar.String(max_length=1)
c: str = ormar.String(max_length=1)
d: str = ormar.String(max_length=1)
e: str = ormar.String(max_length=1)
f: str = ormar.String(max_length=1)
create_test_database = init_tests(base_ormar_config)
def test_model_field_order():
TestCreate = NewTestModel.get_pydantic(exclude={"a"})
assert list(TestCreate.model_fields.keys()) == ["b", "c", "d", "e", "f"]
test_reverse_relation_in_getting_pydantic.py 0000664 0000000 0000000 00000001425 15211567312 0037762 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation from tests.settings import create_config
base_ormar_config = create_config()
import ormar
ormar_config = base_ormar_config.copy()
class Person(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="person")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50)
class Pet(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="pet")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50)
owner: Person = ormar.ForeignKey(Person, ondelete="CASCADE")
def test_fields_exist():
PetPydantic = Pet.get_pydantic()
PersonPydantic = Person.get_pydantic(include={"id", "name", "pets"})
assert "owner" in PetPydantic.model_fields
assert "pets" in PersonPydantic.model_fields
test_validators_are_inherited.py 0000664 0000000 0000000 00000003345 15211567312 0035345 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation import enum
import pytest
from pydantic import ValidationError, field_validator
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class BaseModel(ormar.Model):
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):
A = "A"
B = "B"
C = "C"
class ModelExample(BaseModel):
ormar_config = base_ormar_config.copy(tablename="examples")
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:
ModelExample(str_field="aaaaaaa", enum_field="A")
assert "must contain a space" in str(e)
with pytest.raises(ValidationError) as e:
ModelExample(str_field="a aaaaaaa", enum_field="Z")
assert "Input should be 'A', 'B' or 'C'" in str(e)
def test_pydantic_validator():
ModelExampleCreate(str_field="a aaaaaa", enum_field="A")
with pytest.raises(ValidationError) as e:
ModelExampleCreate(str_field="aaaaaaa", enum_field="A")
assert "must contain a space" in str(e)
with pytest.raises(ValidationError) as e:
ModelExampleCreate(str_field="a aaaaaaa", enum_field="Z")
assert "Input should be 'A', 'B' or 'C'" in str(e)
test_validators_in_generated_pydantic.py 0000664 0000000 0000000 00000003215 15211567312 0037056 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_inheritance_and_pydantic_generation import enum
import pytest
from pydantic import ValidationError, field_validator
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class EnumExample(str, enum.Enum):
A = "A"
B = "B"
C = "C"
class ModelExample(ormar.Model):
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.Enum(nullable=False, enum_class=EnumExample)
@field_validator("str_field")
def validate_str_field(cls, v):
if " " not in v:
raise ValueError("must contain a space")
return v
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:
ModelExample(str_field="aaaaaaa", enum_field="A")
assert "must contain a space" in str(e)
with pytest.raises(ValidationError) as e:
ModelExample(str_field="a aaaaaaa", enum_field="Z")
assert "Input should be 'A', 'B' or 'C'" in str(e)
def test_pydantic_validator():
ModelExampleCreate(str_field="a aaaaaa", enum_field="A")
with pytest.raises(ValidationError) as e:
ModelExampleCreate(str_field="aaaaaaa", enum_field="A")
assert "must contain a space" in str(e)
with pytest.raises(ValidationError) as e:
ModelExampleCreate(str_field="a aaaaaaa", enum_field="Z")
assert "Input should be 'A', 'B' or 'C'" in str(e)
ormar-orm-ormar-a7e7fde/tests/test_meta_constraints/ 0000775 0000000 0000000 00000000000 15211567312 0023110 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_meta_constraints/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0025207 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_meta_constraints/test_check_constraints.py 0000664 0000000 0000000 00000002433 15211567312 0030227 0 ustar 00root root 0000000 0000000 import pytest
import sqlalchemy
import ormar.fields.constraints
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Product(ormar.Model):
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)
company: str = ormar.String(max_length=200)
inventory: int = ormar.Integer()
buffer: int = ormar.Integer()
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_check_columns_exclude_mysql():
async with base_ormar_config.database: # pragma: no cover
if Product.ormar_config.database.dialect.name != "mysql":
async with base_ormar_config.database.transaction(force_rollback=True):
await Product.objects.create(
name="Mars", company="Nestle", inventory=100, buffer=10
)
with pytest.raises((sqlalchemy.exc.IntegrityError)):
await Product.objects.create(
name="Cookies", company="Nestle", inventory=1, buffer=10
)
ormar-orm-ormar-a7e7fde/tests/test_meta_constraints/test_index_constraints.py 0000664 0000000 0000000 00000003730 15211567312 0030262 0 ustar 00root root 0000000 0000000 import pytest
import ormar.fields.constraints
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Product(ormar.Model):
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)
company: str = ormar.String(max_length=200)
location: str = ormar.String(max_length=200)
company_type: str = ormar.String(max_length=200)
create_test_database = init_tests(base_ormar_config)
def test_table_structure():
assert len(Product.ormar_config.table.indexes) > 0
indexes = sorted(
list(Product.ormar_config.table.indexes), key=lambda x: x.name, reverse=True
)
test_index = indexes[0]
assert test_index.name == "my_index"
assert [col.name for col in test_index.columns] == ["company", "name"]
test_index = indexes[1]
assert test_index.name == "ix_products_location_company_type"
assert [col.name for col in test_index.columns] == ["location", "company_type"]
@pytest.mark.asyncio
async def test_index_is_not_unique():
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"
)
await Product.objects.create(
name="Mars", company="Mars", location="B", company_type="Z"
)
await Product.objects.create(
name="Mars", company="Nestle", location="C", company_type="X"
)
await Product.objects.create(
name="Mars", company="Mars", location="D", company_type="Y"
)
ormar-orm-ormar-a7e7fde/tests/test_meta_constraints/test_unique_constraints.py 0000664 0000000 0000000 00000002116 15211567312 0030456 0 ustar 00root root 0000000 0000000 import pytest
import sqlalchemy
import ormar.fields.constraints
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Product(ormar.Model):
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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_unique_columns():
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")
with pytest.raises((sqlalchemy.exc.IntegrityError)):
await Product.objects.create(name="Mars", company="Mars")
ormar-orm-ormar-a7e7fde/tests/test_model_definition/ 0000775 0000000 0000000 00000000000 15211567312 0023043 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_model_definition/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0025142 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_model_definition/pks_and_fks/ 0000775 0000000 0000000 00000000000 15211567312 0025325 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_model_definition/pks_and_fks/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0027424 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_model_definition/pks_and_fks/test_non_integer_pkey.py 0000664 0000000 0000000 00000002216 15211567312 0032276 0 ustar 00root root 0000000 0000000 import random
import pytest
import sqlalchemy
import ormar
from ormar.databases.connection import DatabaseConnection
from tests.settings import ASYNC_DATABASE_URL, DATABASE_URL
database = DatabaseConnection(ASYNC_DATABASE_URL)
metadata = sqlalchemy.MetaData()
def key():
return "".join(random.choice("abcdefgh123456") for _ in range(8))
class Model(ormar.Model):
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)
@pytest.fixture(autouse=True, scope="function")
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_pk_1():
async with database:
model = await Model.objects.create(name="NAME")
assert isinstance(model.id, str)
@pytest.mark.asyncio
async def test_pk_2():
async with database:
model = await Model.objects.create(name="NAME")
assert await Model.objects.all() == [model]
ormar-orm-ormar-a7e7fde/tests/test_model_definition/pks_and_fks/test_saving_string_pks.py 0000664 0000000 0000000 00000004136 15211567312 0032474 0 ustar 00root root 0000000 0000000 from random import choice
from string import ascii_uppercase
import pytest
import pytest_asyncio
import sqlalchemy
from sqlalchemy import create_engine
import ormar
from ormar import Float, String
from ormar.databases.connection import DatabaseConnection
from tests.settings import ASYNC_DATABASE_URL, DATABASE_URL
database = DatabaseConnection(ASYNC_DATABASE_URL)
metadata = sqlalchemy.MetaData()
def get_id() -> str:
return "".join(choice(ascii_uppercase) for _ in range(12))
base_ormar_config = ormar.OrmarConfig(
metadata=metadata,
database=database,
)
class PositionOrm(ormar.Model):
ormar_config = base_ormar_config.copy()
name: str = String(primary_key=True, max_length=50)
x: float = Float()
y: float = Float()
degrees: float = Float()
class PositionOrmDef(ormar.Model):
ormar_config = base_ormar_config.copy()
name: str = String(primary_key=True, max_length=50, default=get_id)
x: float = Float()
y: float = Float()
degrees: float = Float()
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = create_engine(DATABASE_URL)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@pytest_asyncio.fixture(scope="function")
async def cleanup():
yield
async with database:
await PositionOrm.objects.delete(each=True)
await PositionOrmDef.objects.delete(each=True)
@pytest.mark.asyncio
async def test_creating_a_position(cleanup):
async with database:
instance = PositionOrm(name="my_pos", x=1.0, y=2.0, degrees=3.0)
await instance.save()
assert instance.saved
assert instance.name == "my_pos"
instance2 = PositionOrmDef(x=1.0, y=2.0, degrees=3.0)
await instance2.save()
assert instance2.saved
assert instance2.name is not None
assert len(instance2.name) == 12
instance3 = PositionOrmDef(x=1.0, y=2.0, degrees=3.0)
await instance3.save()
assert instance3.saved
assert instance3.name is not None
assert len(instance3.name) == 12
assert instance2.name != instance3.name
ormar-orm-ormar-a7e7fde/tests/test_model_definition/pks_and_fks/test_uuid_fks.py 0000664 0000000 0000000 00000003647 15211567312 0030561 0 ustar 00root root 0000000 0000000 import uuid
import pytest
import sqlalchemy
from sqlalchemy import create_engine
import ormar
from ormar.databases.connection import DatabaseConnection
from tests.settings import ASYNC_DATABASE_URL, DATABASE_URL
metadata = sqlalchemy.MetaData()
db = DatabaseConnection(ASYNC_DATABASE_URL)
class User(ormar.Model):
ormar_config = ormar.OrmarConfig(
tablename="user",
metadata=metadata,
database=db,
)
id: uuid.UUID = ormar.UUID(
primary_key=True, default=uuid.uuid4, uuid_format="string"
)
username = ormar.String(index=True, unique=True, null=False, max_length=255)
email = ormar.String(index=True, unique=True, nullable=False, max_length=255)
hashed_password = ormar.String(null=False, max_length=255)
is_active = ormar.Boolean(default=True, nullable=False)
is_superuser = ormar.Boolean(default=False, nullable=False)
class Token(ormar.Model):
ormar_config = ormar.OrmarConfig(
tablename="token",
metadata=metadata,
database=db,
)
id = ormar.Integer(primary_key=True)
text = ormar.String(max_length=4, unique=True)
user = ormar.ForeignKey(User, related_name="tokens")
created_at = 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)
@pytest.mark.asyncio
async def test_uuid_fk():
async with db:
async with db.transaction(force_rollback=True):
user = await User.objects.create(
username="User1",
email="email@example.com",
hashed_password="^$EDACVS(&A&Y@2131aa",
is_active=True,
is_superuser=False,
)
await Token.objects.create(text="AAAA", user=user)
await Token.objects.order_by("-created_at").all()
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_aliases.py 0000664 0000000 0000000 00000013413 15211567312 0026077 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Child(ormar.Model):
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)
last_name: str = ormar.String(name="lname", max_length=100)
born_year: Optional[int] = ormar.Integer(name="year_born", nullable=True)
class Artist(ormar.Model):
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)
last_name: str = ormar.String(name="lname", max_length=100)
born_year: int = ormar.Integer(name="year")
children: Optional[list[Child]] = ormar.ManyToMany(Child)
class Album(ormar.Model):
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")
create_test_database = init_tests(base_ormar_config)
def test_table_structure():
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 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
)
await Album.objects.create(name="Aunt Robin", artist=artist)
await artist.children.create(
first_name="Son", last_name="1", born_year=1990
)
await artist.children.create(
first_name="Son", last_name="2", born_year=1995
)
await artist.children.create(
first_name="Son", last_name="3", born_year=1998
)
album = await Album.objects.select_related("artist").first()
assert album.artist.last_name == "Mosbey"
assert album.artist.id is not None
assert album.artist.first_name == "Ted"
assert album.artist.born_year == 1975
assert album.name == "Aunt Robin"
artist = await Artist.objects.select_related("children").get()
assert len(artist.children) == 3
assert artist.children[0].first_name == "Son"
assert artist.children[1].last_name == "2"
assert artist.children[2].last_name == "3"
await artist.update(last_name="Bundy")
await Artist.objects.filter(pk=artist.pk).update(born_year=1974)
artist = await Artist.objects.select_related("children").get()
assert artist.last_name == "Bundy"
assert artist.born_year == 1974
artist = (
await Artist.objects.select_related("children")
.fields(
[
"first_name",
"last_name",
"born_year",
"children__first_name",
"children__last_name",
]
)
.get()
)
assert artist.children[0].born_year is None
@pytest.mark.asyncio
async def test_bulk_operations_and_fields():
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])
children = await Child.objects.filter(first_name="Daughter").all()
assert len(children) == 2
assert children[0].last_name == "1"
for child in children:
child.born_year = child.born_year - 100
await Child.objects.bulk_update(children)
children = await Child.objects.filter(first_name="Daughter").all()
assert len(children) == 2
assert children[0].born_year == 1890
children = await Child.objects.fields(["first_name", "last_name"]).all()
assert len(children) == 2
for child in children:
assert child.born_year is None
await children[0].load()
await children[0].delete()
children = await Child.objects.all()
assert len(children) == 1
@pytest.mark.asyncio
async def test_working_with_aliases_get_or_create():
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
)
assert artist.pk is not None
assert created is True
artist2, created = await Artist.objects.get_or_create(
first_name="Teddy", last_name="Bear", born_year=2020
)
assert artist == artist2
assert created is False
art3 = artist2.model_dump()
art3["born_year"] = 2019
await Artist.objects.update_or_create(**art3)
artist3 = await Artist.objects.get(last_name="Bear")
assert artist3.born_year == 2019
artists = await Artist.objects.all()
assert len(artists) == 1
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_columns.py 0000664 0000000 0000000 00000010510 15211567312 0026131 0 ustar 00root root 0000000 0000000 import datetime
from enum import Enum
from typing import Optional
import pydantic
import pytest
import ormar
from ormar import ModelDefinitionError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config(force_rollback=True)
def time():
return datetime.datetime.now().time()
class MyEnum(Enum):
SMALL = 1
BIG = 2
class Example(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="example")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=200, default="aaa")
created: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)
created_day: datetime.date = ormar.Date(default=datetime.date.today)
created_time: datetime.time = ormar.Time(default=time)
description: Optional[str] = ormar.Text(nullable=True)
value: Optional[float] = ormar.Float(nullable=True)
data: pydantic.Json = ormar.JSON(default={})
size: MyEnum = ormar.Enum(enum_class=MyEnum, default=MyEnum.SMALL)
class ExampleNonAutoIncrement(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="example_nonautoincrement")
id: int = ormar.Integer(primary_key=True, autoincrement=False)
name: str = ormar.String(max_length=200, default="aaa")
class EnumExample(ormar.Model):
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_enum_column_type():
assert Example.model_fields["size"].__type__ == MyEnum
def test_accepts_only_proper_enums():
class WrongEnum(Enum):
A = 1
B = 2
with pytest.raises(pydantic.ValidationError):
Example(size=WrongEnum.A)
@pytest.mark.asyncio
async def test_non_auto_increment_pk():
async with base_ormar_config.database:
example = ExampleNonAutoIncrement(id=1)
await example.save()
assert (
ExampleNonAutoIncrement.ormar_config.table.columns["id"].autoincrement
is False
)
@pytest.mark.asyncio
async def test_enum_bulk_operations():
async with base_ormar_config.database:
examples = [EnumExample(), EnumExample()]
await EnumExample.objects.bulk_create(examples)
check = await EnumExample.objects.all()
assert all(x.size == MyEnum.SMALL for x in check)
for x in check:
x.size = MyEnum.BIG
await EnumExample.objects.bulk_update(check)
check2 = await EnumExample.objects.all()
assert all(x.size == MyEnum.BIG for x in check2)
@pytest.mark.asyncio
async def test_enum_filter():
async with base_ormar_config.database:
examples = [EnumExample(), EnumExample(size=MyEnum.BIG)]
await EnumExample.objects.bulk_create(examples)
check = await EnumExample.objects.all(size=MyEnum.SMALL)
assert len(check) == 1
check = await EnumExample.objects.all(size=MyEnum.BIG)
assert len(check) == 1
@pytest.mark.asyncio
async def test_model_crud():
async with base_ormar_config.database:
example = Example()
await example.save()
await example.load()
assert example.created.year == datetime.datetime.now().year
assert example.created_day == datetime.date.today()
assert example.description is None
assert example.value is None
assert example.data == {}
assert example.size == MyEnum.SMALL
await example.update(data={"foo": 123}, value=123.456, size=MyEnum.BIG)
await example.load()
assert example.value == 123.456
assert example.data == {"foo": 123}
assert example.size == MyEnum.BIG
await example.update(data={"foo": 123}, value=123.456)
await example.load()
assert example.value == 123.456
assert example.data == {"foo": 123}
await example.delete()
@pytest.mark.asyncio
async def test_invalid_enum_field() -> None:
async with base_ormar_config.database:
with pytest.raises(ModelDefinitionError):
class Example2(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="example2")
id: int = ormar.Integer(primary_key=True)
size: MyEnum = ormar.Enum(enum_class=[]) # type: ignore
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_create_uses_init_for_consistency.py 0000664 0000000 0000000 00000002653 15211567312 0033276 0 ustar 00root root 0000000 0000000 import uuid
from typing import ClassVar
import pytest
from pydantic import model_validator
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Mol(ormar.Model):
# fixed namespace to generate always unique uuid from the smiles
_UUID_NAMESPACE: ClassVar[uuid.UUID] = uuid.UUID(
"12345678-abcd-1234-abcd-123456789abc"
)
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)
def __init__(self, **kwargs):
# this is required to generate id from smiles in init, if id is not given
if "id" not in kwargs:
kwargs["id"] = self._UUID_NAMESPACE
super().__init__(**kwargs)
@model_validator(mode="before")
def make_canonical_smiles_and_uuid(cls, values):
values["id"], values["smiles"] = cls.uuid(values["smiles"])
return values
@classmethod
def uuid(cls, smiles):
id_ = uuid.uuid5(cls._UUID_NAMESPACE, smiles)
return id_, smiles
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_json_column():
async with base_ormar_config.database:
await Mol.objects.create(smiles="Cc1ccccc1")
count = await Mol.objects.count()
assert count == 1
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_dates_with_timezone.py 0000664 0000000 0000000 00000007401 15211567312 0030523 0 ustar 00root root 0000000 0000000 from datetime import date, datetime, time, timedelta, timezone
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class DateFieldsModel(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
created_date: datetime = ormar.DateTime(
default=datetime.now(tz=timezone(timedelta(hours=3))), timezone=True
)
updated_date: datetime = ormar.DateTime(
default=datetime.now(tz=timezone(timedelta(hours=3))),
name="modification_date",
timezone=True,
)
class SampleModel(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
updated_at: datetime = ormar.DateTime()
class TimeModel(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
elapsed: time = ormar.Time()
class DateModel(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
creation_date: date = ormar.Date()
class MyModel(ormar.Model):
id: int = ormar.Integer(primary_key=True)
created_at: datetime = ormar.DateTime(timezone=True, nullable=False)
ormar_config = base_ormar_config.copy(tablename="mymodels")
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_model_crud_with_timezone():
async with base_ormar_config.database:
datemodel = await DateFieldsModel().save()
assert datemodel.created_date is not None
assert datemodel.updated_date is not None
@pytest.mark.asyncio
async def test_query_with_datetime_in_filter():
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)
current_dt = datetime(2021, 5, 19, 0, 0, 0, 0)
outdated_samples = await SampleModel.objects.filter(
updated_at__lt=current_dt
).all()
assert outdated_samples[0] == sample
@pytest.mark.asyncio
async def test_query_with_date_in_filter():
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))
sample4 = await TimeModel.objects.create(elapsed=time(0, 21, 15))
threshold = time(0, 20, 0)
samples = await TimeModel.objects.filter(TimeModel.elapsed >= threshold).all()
assert len(samples) == 2
assert samples[0] == sample
assert samples[1] == sample4
@pytest.mark.asyncio
async def test_query_with_time_in_filter():
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))
outdated_samples = await DateModel.objects.filter(
creation_date__in=[date(2021, 5, 19), date(2021, 5, 20)]
).all()
assert len(outdated_samples) == 2
assert outdated_samples[0] == sample2
assert outdated_samples[1] == sample3
@pytest.mark.asyncio
async def test_filtering_by_timezone_with_timedelta():
async with base_ormar_config.database:
now_utc = datetime.now(timezone.utc)
object = MyModel(created_at=now_utc)
await object.save()
one_hour_ago = datetime.now(timezone.utc) - timedelta(hours=1)
created_since_one_hour_ago = await MyModel.objects.filter(
created_at__gte=one_hour_ago
).all()
assert len(created_since_one_hour_ago) == 1
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_equality_and_hash.py 0000664 0000000 0000000 00000002564 15211567312 0030145 0 ustar 00root root 0000000 0000000 # type: ignore
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Song(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="songs")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_equality():
async with base_ormar_config.database:
song1 = await Song.objects.create(name="Song")
song2 = await Song.objects.create(name="Song")
song3 = Song(name="Song")
song4 = Song(name="Song")
assert song1 == song1
assert song3 == song4
assert song1 != song2
assert song1 != song3
assert song3 != song1
assert song1 is not None
@pytest.mark.asyncio
async def test_hash_doesnt_change_with_fields_if_pk():
async with base_ormar_config.database:
song1 = await Song.objects.create(name="Song")
prev_hash = hash(song1)
await song1.update(name="Song 2")
assert hash(song1) == prev_hash
@pytest.mark.asyncio
async def test_hash_changes_with_fields_if_no_pk():
async with base_ormar_config.database:
song1 = Song(name="Song")
prev_hash = hash(song1)
song1.name = "Song 2"
assert hash(song1) != prev_hash
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_extra_ignore_parameter.py 0000664 0000000 0000000 00000001430 15211567312 0031200 0 ustar 00root root 0000000 0000000 import ormar
from ormar import Extra
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config(force_rollback=True)
class Child(ormar.Model):
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"
assert child.last_name == "Name"
assert not hasattr(child, "extra_param")
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_field_quoting.py 0000664 0000000 0000000 00000006230 15211567312 0027306 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config(force_rollback=True)
class SchoolClass(ormar.Model):
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):
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):
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"
)
create_test_database = init_tests(base_ormar_config)
async def create_data():
class1 = await SchoolClass.objects.create(name="Math")
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
)
@pytest.mark.asyncio
async def test_quotes_left_join():
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")
).all()
for student in students:
assert (
student.schoolclass.name == "Math"
or student.category.name == "Foreign"
)
@pytest.mark.asyncio
async def test_quotes_reverse_join():
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:
for student in schoolclass.students:
assert student.gpa > 3
@pytest.mark.asyncio
async def test_quotes_deep_join():
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()
for schoolclass in schoolclasses:
for student in schoolclass.students:
assert student.category.name == "Domestic"
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_fields_access.py 0000664 0000000 0000000 00000027440 15211567312 0027252 0 ustar 00root root 0000000 0000000 import pytest
import ormar
from ormar import BaseField
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class PriceList(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
price_lists = ormar.ManyToMany(PriceList, related_name="categories")
class Product(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="product")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
rating: float = ormar.Float(minimum=1, maximum=5)
category = ormar.ForeignKey(Category)
class Supplier(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="suppliers")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Item(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="items")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
supplier = ormar.ForeignKey(Supplier, name="supplier_id")
create_test_database = init_tests(base_ormar_config)
def test_fields_access():
# basic access
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"
assert Product.id._source_model == Product
# nested models
curr_field = Product.category.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.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.ormar_config.model_fields["rating"]
assert curr_field._access_chain == "categories__products__rating"
assert curr_field._source_model == PriceList
# FK accessor accepts the same operators as a regular field
sample_category = Category(id=7, name="x")
assert (Product.category == 3)._kwargs_dict == {"category__exact": 3}
assert (Product.category == sample_category)._kwargs_dict == {
"category__exact": sample_category
}
assert (Product.category >= 3)._kwargs_dict == {"category__gte": 3}
assert (Product.category <= 3)._kwargs_dict == {"category__lte": 3}
assert (Product.category > 3)._kwargs_dict == {"category__gt": 3}
assert (Product.category < 3)._kwargs_dict == {"category__lt": 3}
assert (Product.category << [1, 2])._kwargs_dict == {"category__in": [1, 2]}
assert Product.category.in_([1, 2])._kwargs_dict == {"category__in": [1, 2]}
assert (Product.category >> None)._kwargs_dict == {"category__isnull": True}
assert Product.category.isnull(False)._kwargs_dict == {"category__isnull": False}
# FK accessor with an explicit db alias (name="supplier_id") still works
# because the check keys on the ormar field registry, not on table.columns
sample_supplier = Supplier(id=9, name="acme")
assert (Item.supplier == 2)._kwargs_dict == {"supplier__exact": 2}
assert (Item.supplier == sample_supplier)._kwargs_dict == {
"supplier__exact": sample_supplier
}
assert (Item.supplier << [sample_supplier, 5])._kwargs_dict == {
"supplier__in": [sample_supplier, 5]
}
assert (Item.supplier >= 2)._kwargs_dict == {"supplier__gte": 2}
# m2m accessor has no own column - comparison still raises
with pytest.raises(AttributeError):
assert Category.price_lists >= 3
# reverse FK accessor (virtual relation) - comparison still raises
with pytest.raises(AttributeError):
assert Category.products >= 3
@pytest.mark.parametrize(
"method, expected, expected_value",
[
("__eq__", "exact", "Test"),
("__lt__", "lt", "Test"),
("__le__", "lte", "Test"),
("__ge__", "gte", "Test"),
("__gt__", "gt", "Test"),
("iexact", "iexact", "Test"),
("contains", "contains", "Test"),
("icontains", "icontains", "Test"),
("startswith", "startswith", "Test"),
("istartswith", "istartswith", "Test"),
("endswith", "endswith", "Test"),
("iendswith", "iendswith", "Test"),
("isnull", "isnull", "Test"),
("in_", "in", "Test"),
("__lshift__", "in", "Test"),
("__rshift__", "isnull", True),
("__mod__", "contains", "Test"),
],
)
def test_operator_return_proper_filter_action(method, expected, expected_value):
group_ = getattr(Product.name, method)("Test")
assert group_._kwargs_dict == {f"name__{expected}": expected_value}
group_ = getattr(Product.category.name, method)("Test")
assert group_._kwargs_dict == {f"category__name__{expected}": expected_value}
group_ = getattr(PriceList.categories.products.rating, method)("Test")
assert group_._kwargs_dict == {
f"categories__products__rating__{expected}": expected_value
}
@pytest.mark.parametrize("method, expected_direction", [("asc", ""), ("desc", "desc")])
def test_operator_return_proper_order_action(method, expected_direction):
action = getattr(Product.name, method)()
assert action.source_model == Product
assert action.target_model == Product
assert action.direction == expected_direction
assert action.is_source_model_order
action = getattr(Product.category.name, method)()
assert action.source_model == Product
assert action.target_model == Category
assert action.direction == expected_direction
assert not action.is_source_model_order
action = getattr(PriceList.categories.products.rating, method)()
assert action.source_model == PriceList
assert action.target_model == Product
assert action.direction == expected_direction
assert not action.is_source_model_order
def test_combining_groups_together():
group = (Product.name == "Test") & (Product.rating >= 3.0)
group.resolve(model_cls=Product)
assert len(group._nested_groups) == 2
assert str(
group.get_text_clause().compile(compile_kwargs={"literal_binds": True})
) == ("((product.name = 'Test') AND (product.rating >= 3.0))")
group = ~((Product.name == "Test") & (Product.rating >= 3.0))
group.resolve(model_cls=Product)
assert len(group._nested_groups) == 2
assert str(
group.get_text_clause().compile(compile_kwargs={"literal_binds": True})
) == ("NOT ((product.name = 'Test') AND (product.rating >= 3.0))")
group = ((Product.name == "Test") & (Product.rating >= 3.0)) | (
Product.category.name << (["Toys", "Books"])
)
group.resolve(model_cls=Product)
assert len(group._nested_groups) == 2
assert len(group._nested_groups[0]._nested_groups) == 2
group_str = str(
group.get_text_clause().compile(compile_kwargs={"literal_binds": True})
)
category_prefix = group._nested_groups[1].actions[0].table_prefix
assert group_str == (
"(((product.name = 'Test') AND (product.rating >= 3.0)) "
f"OR ({category_prefix}_categories.name IN ('Toys', 'Books')))"
)
group = (Product.name % "Test") | (
(Product.category.price_lists.name.startswith("Aa"))
| (Product.category.name << (["Toys", "Books"]))
)
group.resolve(model_cls=Product)
assert len(group._nested_groups) == 2
assert len(group._nested_groups[1]._nested_groups) == 2
group_str = str(
group.get_text_clause().compile(compile_kwargs={"literal_binds": True})
)
price_list_prefix = (
group._nested_groups[1]._nested_groups[0].actions[0].table_prefix
)
category_prefix = group._nested_groups[1]._nested_groups[1].actions[0].table_prefix
assert group_str == (
f"((product.name LIKE '%Test%') "
f"OR ({price_list_prefix}_price_lists.name LIKE 'Aa%') "
f"OR ({category_prefix}_categories.name IN ('Toys', 'Books')))"
)
group = (Product.name % "Test") & (
(Product.category.price_lists.name.startswith("Aa"))
| (Product.category.name << (["Toys", "Books"]))
)
group.resolve(model_cls=Product)
assert len(group._nested_groups) == 2
assert len(group._nested_groups[1]._nested_groups) == 2
group_str = str(
group.get_text_clause().compile(compile_kwargs={"literal_binds": True})
)
price_list_prefix = (
group._nested_groups[1]._nested_groups[0].actions[0].table_prefix
)
category_prefix = group._nested_groups[1]._nested_groups[1].actions[0].table_prefix
assert group_str == (
f"((product.name LIKE '%Test%') "
f"AND (({price_list_prefix}_price_lists.name LIKE 'Aa%') "
f"OR ({category_prefix}_categories.name IN ('Toys', 'Books'))))"
)
@pytest.mark.asyncio
async def test_filtering_by_field_access():
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
).save()
check = await Product.objects.get(Product.name == "My Little Pony")
assert check == product2
@pytest.mark.asyncio
async def test_filtering_fk_by_field_access():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
toys = await Category(name="Toys").save()
books = await Category(name="Books").save()
pony = await Product(
name="My Little Pony", rating=3.8, category=toys
).save()
await Product(name="Novel", rating=4.2, category=books).save()
# by scalar PK - should match kwargs form exactly
via_accessor = await Product.objects.filter(
Product.category == toys.pk
).all()
via_kwargs = await Product.objects.filter(category=toys.pk).all()
assert {p.pk for p in via_accessor} == {pony.pk}
assert {p.pk for p in via_accessor} == {p.pk for p in via_kwargs}
# by model instance
via_instance = await Product.objects.filter(Product.category == toys).all()
assert {p.pk for p in via_instance} == {pony.pk}
# `in_` / `<<` returns matches for several PKs
all_products = await Product.objects.all()
via_in = await Product.objects.filter(
Product.category << [toys.pk, books.pk]
).all()
assert {p.pk for p in via_in} == {p.pk for p in all_products}
# aliased FK field (name="supplier_id")
sup = await Supplier(name="Acme").save()
other_sup = await Supplier(name="Globex").save()
gadget = await Item(name="gadget", supplier=sup).save()
await Item(name="widget", supplier=other_sup).save()
via_aliased_pk = await Item.objects.filter(Item.supplier == sup.pk).all()
via_aliased_instance = await Item.objects.filter(Item.supplier == sup).all()
via_aliased_in = await Item.objects.filter(
Item.supplier << [sup.pk, other_sup.pk]
).all()
assert {i.pk for i in via_aliased_pk} == {gadget.pk}
assert {i.pk for i in via_aliased_instance} == {gadget.pk}
assert len(via_aliased_in) == 2
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_foreign_key_value_used_for_related_model.py0000664 0000000 0000000 00000003736 15211567312 0034730 0 ustar 00root root 0000000 0000000 import uuid
from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class PageLink(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="pagelinks")
id: int = ormar.Integer(primary_key=True)
value: str = ormar.String(max_length=2048)
country: str = ormar.String(max_length=1000)
class Post(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="posts")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=500)
link: PageLink = ormar.ForeignKey(
PageLink, related_name="posts", ondelete="CASCADE"
)
class Department(ormar.Model):
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):
ormar_config = base_ormar_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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_pass_int_values_as_fk():
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()
assert post_check.link == link
@pytest.mark.asyncio
async def test_pass_uuid_value_as_fk():
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()
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_iterate.py 0000664 0000000 0000000 00000021326 15211567312 0026115 0 ustar 00root root 0000000 0000000 import uuid
import pytest
import ormar
from ormar.exceptions import QueryDefinitionError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class User(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="users4")
id: uuid.UUID = ormar.UUID(
uuid_format="string", primary_key=True, default=uuid.uuid4
)
name: str = ormar.String(max_length=100, default="")
class Task(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="tasks")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, default="")
user: User = ormar.ForeignKey(to=User)
class Task2(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="tasks2")
id: uuid.UUID = ormar.UUID(
uuid_format="string", primary_key=True, default=uuid.uuid4
)
name: str = ormar.String(max_length=100, default="")
user: User2 = ormar.ForeignKey(to=User2)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_empty_result():
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 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")
async for user in User.objects.iterate():
assert user in (tom, jane, lucy)
@pytest.mark.asyncio
async def test_model_iterator_filter():
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")
async for user in User.objects.iterate(name="Tom"):
assert user.name == tom.name
@pytest.mark.asyncio
async def test_model_iterator_relations():
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")
for user in tom, jane, lucy:
await Task.objects.create(name="task1", user=user)
await Task.objects.create(name="task2", user=user)
results = []
async for user in User.objects.select_related(User.tasks).iterate():
assert len(user.tasks) == 2
results.append(user)
assert len(results) == 3
@pytest.mark.asyncio
async def test_model_iterator_relations_queryset_proxy():
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")
for user in tom, jane:
await Task.objects.create(name="task1", user=user)
await Task.objects.create(name="task2", user=user)
tom_tasks = []
async for task in tom.tasks.iterate():
assert task.name in ("task1", "task2")
tom_tasks.append(task)
assert len(tom_tasks) == 2
jane_tasks = []
async for task in jane.tasks.iterate():
assert task.name in ("task1", "task2")
jane_tasks.append(task)
assert len(jane_tasks) == 2
@pytest.mark.asyncio
async def test_model_iterator_uneven_number_of_relations():
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")
for user in tom, jane:
await Task.objects.create(name="task1", user=user)
await Task.objects.create(name="task2", user=user)
await Task.objects.create(name="task3", user=lucy)
expected_counts = {"Tom": 2, "Jane": 2, "Lucy": 1}
results = []
async for user in User.objects.select_related(User.tasks).iterate():
assert len(user.tasks) == expected_counts[user.name]
results.append(user)
assert len(results) == 3
@pytest.mark.asyncio
async def test_model_iterator_uuid_pk():
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")
async for user in User2.objects.iterate():
assert user in (tom, jane, lucy)
@pytest.mark.asyncio
async def test_model_iterator_filter_uuid_pk():
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")
async for user in User2.objects.iterate(name="Tom"):
assert user.name == tom.name
@pytest.mark.asyncio
async def test_model_iterator_relations_uuid_pk():
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")
for user in tom, jane, lucy:
await Task2.objects.create(name="task1", user=user)
await Task2.objects.create(name="task2", user=user)
results = []
async for user in User2.objects.select_related(User2.task2s).iterate():
assert len(user.task2s) == 2
results.append(user)
assert len(results) == 3
@pytest.mark.asyncio
async def test_model_iterator_relations_queryset_proxy_uuid_pk():
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")
for user in tom, jane:
await Task2.objects.create(name="task1", user=user)
await Task2.objects.create(name="task2", user=user)
tom_tasks = []
async for task in tom.task2s.iterate():
assert task.name in ("task1", "task2")
tom_tasks.append(task)
assert len(tom_tasks) == 2
jane_tasks = []
async for task in jane.task2s.iterate():
assert task.name in ("task1", "task2")
jane_tasks.append(task)
assert len(jane_tasks) == 2
@pytest.mark.asyncio
async def test_model_iterator_uneven_number_of_relations_uuid_pk():
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")
for user in tom, jane:
await Task2.objects.create(name="task1", user=user)
await Task2.objects.create(name="task2", user=user)
await Task2.objects.create(name="task3", user=lucy)
expected_counts = {"Tom": 2, "Jane": 2, "Lucy": 1}
results = []
async for user in User2.objects.select_related(User2.task2s).iterate():
assert len(user.task2s) == expected_counts[user.name]
results.append(user)
assert len(results) == 3
@pytest.mark.asyncio
async def test_model_iterator_with_prefetch_raises_error():
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
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_json_fields.py 0000664 0000000 0000000 00000002126 15211567312 0026754 0 ustar 00root root 0000000 0000000 import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
author: Author = ormar.ForeignKey(Author, name="author_id")
my_data: dict | None = ormar.JSON(nullable=True)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_construct_with_empty_relation():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
author = await Author.objects.create()
await Book.objects.create(author=author, my_data={"aa": 1})
# this should not error out
await Author.objects.select_related(Author.books).all()
# this should also not error out
await Author.objects.select_all().all()
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_model_construct.py 0000664 0000000 0000000 00000005617 15211567312 0027671 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class NickNames(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="nicks_x_hq")
class HQ(ormar.Model):
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")
nicks: list[NickNames] = ormar.ManyToMany(NickNames, through=NicksHq)
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, nullable=False, name="company_name")
founded: Optional[int] = ormar.Integer(nullable=True)
hq: HQ = ormar.ForeignKey(HQ)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_construct_with_empty_relation():
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.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 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.model_construct(**dict(name="Banzai", hq=hq, founded=1988))
assert comp.model_dump() == comp2.model_dump()
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 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.model_construct(**dict(name="Main", nicks=[n1, n2]))
assert hq.model_dump() == hq2.model_dump()
hq3 = HQ.model_construct(
**dict(name="Main", nicks=[n1.model_dump(), n2.model_dump()])
)
assert hq.model_dump() == hq3.model_dump()
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_model_definition.py 0000664 0000000 0000000 00000015320 15211567312 0027765 0 ustar 00root root 0000000 0000000 # type: ignore
import datetime
import decimal
import typing
import pydantic
import pytest
import sqlalchemy
import ormar
from ormar.exceptions import ModelDefinitionError
from ormar.models import Model
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class ExampleModel(Model):
ormar_config = base_ormar_config.copy(tablename="example")
test: int = ormar.Integer(primary_key=True)
test_string: str = ormar.String(max_length=250)
test_text: str = ormar.Text(default="")
test_bool: bool = ormar.Boolean(nullable=False)
test_float = ormar.Float(nullable=True)
test_datetime = ormar.DateTime(default=datetime.datetime.now)
test_date = ormar.Date(default=datetime.date.today)
test_time = ormar.Time(default=datetime.time)
test_json = ormar.JSON(default={})
test_bigint: int = ormar.BigInteger(default=0)
test_smallint: int = ormar.SmallInteger(default=0)
test_decimal = ormar.Decimal(scale=2, precision=10)
test_decimal2 = ormar.Decimal(max_digits=10, decimal_places=2)
fields_to_check = [
"test",
"test_text",
"test_string",
"test_datetime",
"test_date",
"test_text",
"test_float",
"test_bigint",
"test_json",
]
class ExampleModel2(Model):
ormar_config = base_ormar_config.copy(tablename="example2")
test: int = ormar.Integer(primary_key=True)
test_string: str = ormar.String(max_length=250)
class User(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="users")
id: int = ormar.Integer(primary_key=True)
class Account(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="accounts")
id: int = ormar.Integer(primary_key=True)
user: User = ormar.ForeignKey(User, index=False)
class Purchase(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="purchases")
id: int = ormar.Integer(primary_key=True)
user: User = ormar.ForeignKey(User, index=True)
create_test_database = init_tests(base_ormar_config)
@pytest.fixture()
def example():
return ExampleModel(
pk=1,
test_string="test",
test_bool=True,
test_decimal=decimal.Decimal(3.5),
test_decimal2=decimal.Decimal(5.5),
)
def test_not_nullable_field_is_required():
with pytest.raises(pydantic.ValidationError):
ExampleModel(test=1, test_string="test")
def test_model_attribute_access(example):
assert example.test == 1
assert example.test_string == "test"
assert example.test_datetime.year == datetime.datetime.now().year
assert example.test_date == datetime.date.today()
assert example.test_text == ""
assert example.test_float is None
assert example.test_bigint == 0
assert example.test_json == {}
assert example.test_decimal == 3.5
assert example.test_decimal2 == 5.5
example.test = 12
assert example.test == 12
example._orm_saved = True
assert example._orm_saved
def test_model_attribute_json_access(example):
example.test_json = dict(aa=12)
assert example.test_json == dict(aa=12)
def test_missing_metadata():
with pytest.raises(ModelDefinitionError):
class JsonSample2(ormar.Model):
ormar_config = ormar.OrmarConfig(
tablename="jsons2",
database=base_ormar_config.database,
)
id: int = ormar.Integer(primary_key=True)
test_json = ormar.JSON(nullable=True)
def test_missing_database():
with pytest.raises(ModelDefinitionError):
class JsonSample3(ormar.Model):
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)
def test_non_existing_attr(example):
with pytest.raises(ValueError):
example.new_attr = 12
def test_primary_key_access_and_setting(example):
assert example.pk == 1
example.pk = 2
assert example.pk == 2
assert example.test == 2
def test_pydantic_model_is_created(example):
assert issubclass(example.__class__, pydantic.BaseModel)
assert all([field in example.__class__.model_fields for field in fields_to_check])
assert example.test == 1
def test_sqlalchemy_table_is_created(example):
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
def test_no_pk_in_model_definition():
with pytest.raises(ModelDefinitionError): # type: ignore
class ExampleModel2(Model): # type: ignore
ormar_config = base_ormar_config.copy(tablename="example2")
test_string: str = ormar.String(max_length=250) # type: ignore
@typing.no_type_check
def test_two_pks_in_model_definition():
with pytest.raises(ModelDefinitionError):
@typing.no_type_check
class ExampleModel2(Model):
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_decimal_error_in_model_definition():
with pytest.raises(ModelDefinitionError):
class ExampleModel2(Model):
ormar_config = base_ormar_config.copy(tablename="example5")
test: decimal.Decimal = ormar.Decimal(primary_key=True)
@typing.no_type_check
def test_binary_error_without_length_model_definition():
with pytest.raises(ModelDefinitionError):
class ExampleModel2(Model):
ormar_config = base_ormar_config.copy(tablename="example6")
test: bytes = ormar.LargeBinary(primary_key=True, max_length=-1)
@typing.no_type_check
def test_string_error_in_model_definition():
with pytest.raises(ModelDefinitionError):
class ExampleModel2(Model):
ormar_config = base_ormar_config.copy(tablename="example6")
test: str = ormar.String(primary_key=True, max_length=0)
@typing.no_type_check
def test_json_conversion_in_model():
with pytest.raises(pydantic.ValidationError):
ExampleModel(
test_json=datetime.datetime.now(),
test=1,
test_string="test",
test_bool=True,
)
def test_foreign_key_index():
assert Account.ormar_config.table.columns.user.index is False
assert Purchase.ormar_config.table.columns.user.index is True
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_models.py 0000664 0000000 0000000 00000052620 15211567312 0025744 0 ustar 00root root 0000000 0000000 import asyncio
import base64
import datetime
import os
import uuid
from enum import Enum
from typing import Optional
import pydantic
import pytest
import sqlalchemy
import ormar
from ormar.exceptions import ModelError, NoMatch, QueryDefinitionError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class JsonSample(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="jsons")
id: int = ormar.Integer(primary_key=True)
test_json = ormar.JSON(nullable=True)
blob = b"test"
blob2 = b"test2icac89uc98"
class LargeBinarySample(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="my_bolbs")
id: int = ormar.Integer(primary_key=True)
test_binary: bytes = ormar.LargeBinary(max_length=100000)
blob3 = os.urandom(64)
blob4 = os.urandom(100)
class LargeBinaryStr(ormar.Model):
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, represent_as_base64_str=True
)
class LargeBinaryNullableStr(ormar.Model):
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,
represent_as_base64_str=True,
nullable=True,
)
class UUIDSample(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="uuids2")
id: uuid.UUID = ormar.UUID(
primary_key=True, default=uuid.uuid4, uuid_format="string"
)
test_text: str = ormar.Text()
class User(ormar.Model):
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):
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):
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.date.today)
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):
ormar_config = base_ormar_config.copy(tablename="country")
id: int = ormar.Integer(primary_key=True)
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):
ormar_config = base_ormar_config.copy(tablename="country2")
id: int = ormar.Integer(primary_key=True)
name: Optional[CountryNameEnum] = ormar.Enum(
enum_class=CountryNameEnum, nullable=True
)
class NotNullableCountry(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="country3")
id: int = ormar.Integer(primary_key=True)
name: CountryNameEnum = ormar.Enum(enum_class=CountryNameEnum, nullable=False)
create_test_database = init_tests(base_ormar_config)
def test_model_class():
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():
with pytest.raises(ModelError):
User(non_existing_pk=1)
def test_model_pk():
user = User(pk=1)
assert user.pk == 1
assert user.id == 1
@pytest.mark.asyncio
async def test_json_column():
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}')
items = await JsonSample.objects.all()
assert len(items) == 2
assert items[0].test_json == dict(aa=12)
assert items[1].test_json == dict(aa=12)
items[0].test_json = "[1, 2, 3]"
assert items[0].test_json == [1, 2, 3]
@pytest.mark.asyncio
async def test_binary_column():
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)
items = await LargeBinarySample.objects.all()
assert len(items) == 2
assert items[0].test_binary == blob
assert items[1].test_binary == blob2
items[0].test_binary = "test2icac89uc98"
assert items[0].test_binary == b"test2icac89uc98"
@pytest.mark.asyncio
async def test_binary_str_column():
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)
items = await LargeBinaryStr.objects.all()
assert len(items) == 2
assert items[0].test_binary == base64.b64encode(blob3).decode()
items[0].test_binary = base64.b64encode(blob4).decode()
assert items[0].test_binary == base64.b64encode(blob4).decode()
assert items[1].test_binary == base64.b64encode(blob4).decode()
assert items[1].__dict__["test_binary"] == blob4
@pytest.mark.asyncio
async def test_binary_nullable_str_column():
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()
assert len(items) == 2
items[0].test_binary = blob3
items[1].test_binary = blob4
await LargeBinaryNullableStr.objects.bulk_update(items)
items = await LargeBinaryNullableStr.objects.all()
assert len(items) == 2
assert items[0].test_binary == base64.b64encode(blob3).decode()
items[0].test_binary = base64.b64encode(blob4).decode()
assert items[0].test_binary == base64.b64encode(blob4).decode()
assert items[1].test_binary == base64.b64encode(blob4).decode()
assert items[1].__dict__["test_binary"] == blob4
await LargeBinaryNullableStr.objects.bulk_create(
[LargeBinaryNullableStr(), LargeBinaryNullableStr(test_binary=blob3)]
)
items = await LargeBinaryNullableStr.objects.all()
assert len(items) == 4
await items[0].update(test_binary=blob4)
check_item = await LargeBinaryNullableStr.objects.get(id=items[0].id)
assert check_item.test_binary == base64.b64encode(blob4).decode()
@pytest.mark.asyncio
async def test_uuid_column():
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")
items = await UUIDSample.objects.all()
assert len(items) == 2
assert isinstance(items[0].id, uuid.UUID)
assert isinstance(items[1].id, uuid.UUID)
assert items[0].id in (u1.id, u2.id)
assert items[1].id in (u1.id, u2.id)
assert items[0].id != items[1].id
item = await UUIDSample.objects.filter(id=u1.id).get()
assert item.id == u1.id
item2 = await UUIDSample.objects.first()
item3 = await UUIDSample.objects.get(pk=item2.id)
assert item2.id == item3.id
assert isinstance(item3.id, uuid.UUID)
u3 = await UUIDSample2(**u1.model_dump()).save()
u1_2 = await UUIDSample.objects.get(pk=u3.id)
assert u1_2 == u1
u4 = await UUIDSample2.objects.get(pk=u3.id)
assert u3 == u4
@pytest.mark.asyncio
async def test_model_crud():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
users = await User.objects.all()
assert users == []
user = await User.objects.create(name="Tom")
users = await User.objects.all()
assert user.name == "Tom"
assert user.pk is not None
assert users == [user]
lookup = await User.objects.get()
assert lookup == user
await user.update(name="Jane")
users = await User.objects.all()
assert user.name == "Jane"
assert user.pk is not None
assert users == [user]
await user.delete()
users = await User.objects.all()
assert users == []
@pytest.mark.asyncio
async def test_model_get():
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()
assert await User.objects.get_or_none() is None
user = await User.objects.create(name="Tom")
lookup = await User.objects.get()
assert lookup == user
user2 = await User.objects.create(name="Jane")
await User.objects.create(name="Jane")
with pytest.raises(ormar.MultipleMatches):
await User.objects.get(name="Jane")
same_user = await User.objects.get(pk=user2.id)
assert same_user.id == user2.id
assert same_user.pk == user2.pk
assert await User.objects.order_by("-name").get() == user
@pytest.mark.asyncio
async def test_model_filter():
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")
user = await User.objects.get(name="Lucy")
assert user.name == "Lucy"
with pytest.raises(ormar.NoMatch):
await User.objects.get(name="Jim")
await Product.objects.create(name="T-Shirt", rating=5, in_stock=True)
await Product.objects.create(name="Dress", rating=4)
await Product.objects.create(name="Coat", rating=3, in_stock=True)
product = await Product.objects.get(name__iexact="t-shirt", rating=5)
assert product.pk is not None
assert product.name == "T-Shirt"
assert product.rating == 5
assert product.last_delivery == datetime.datetime.now().date()
products = await Product.objects.all(rating__gte=2, in_stock=True)
assert len(products) == 2
products = await Product.objects.all(name__icontains="T")
assert len(products) == 2
products = await Product.objects.exclude(rating__gte=4).all()
assert len(products) == 1
products = await Product.objects.exclude(rating__gte=4, in_stock=True).all()
assert len(products) == 2
products = await Product.objects.exclude(in_stock=True).all()
assert len(products) == 1
products = await Product.objects.exclude(name__icontains="T").all()
assert len(products) == 1
# Test escaping % character from icontains, contains, and iexact
await Product.objects.create(name="100%-Cotton", rating=3)
await Product.objects.create(name="Cotton-100%-Egyptian", rating=3)
await Product.objects.create(name="Cotton-100%", rating=3)
products = Product.objects.filter(name__iexact="100%-cotton")
assert await products.count() == 1
products = Product.objects.filter(name__contains="%")
assert await products.count() == 3
products = Product.objects.filter(name__icontains="%")
assert await products.count() == 3
@pytest.mark.asyncio
async def test_wrong_query_contains_model():
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()
@pytest.mark.asyncio
async def test_model_exists():
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
@pytest.mark.asyncio
async def test_model_count():
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")
assert await User.objects.count() == 3
assert await User.objects.filter(name__icontains="T").count() == 1
@pytest.mark.asyncio
async def test_model_limit():
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")
assert len(await User.objects.limit(2).all()) == 2
@pytest.mark.asyncio
async def test_model_limit_with_filter():
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")
assert (
len(await User.objects.limit(2).filter(name__iexact="Tom").all()) == 2
)
@pytest.mark.asyncio
async def test_offset():
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")
users = await User.objects.offset(1).limit(1).all()
assert users[0].name == "Jane"
@pytest.mark.asyncio
async def test_model_first():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
with pytest.raises(ormar.NoMatch):
await User.objects.first()
assert await User.objects.first_or_none() is None
tom = await User.objects.create(name="Tom")
jane = await User.objects.create(name="Jane")
assert await User.objects.first() == tom
assert await User.objects.first(name="Jane") == jane
assert await User.objects.filter(name="Jane").first() == jane
with pytest.raises(NoMatch):
await User.objects.filter(name="Lucy").first()
assert await User.objects.first_or_none(name="Lucy") is None
assert await User.objects.filter(name="Lucy").first_or_none() is None
assert await User.objects.order_by("name").first() == jane
@pytest.mark.asyncio
async def test_model_last():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
with pytest.raises(ormar.NoMatch):
await User.objects.last()
assert await User.objects.last_or_none() is None
tom = await User.objects.create(name="Tom")
jane = await User.objects.create(name="Jane")
assert await User.objects.last() == jane
assert await User.objects.last(name="Tom") == tom
assert await User.objects.filter(name="Tom").last() == tom
with pytest.raises(NoMatch):
await User.objects.filter(name="Lucy").last()
assert await User.objects.last_or_none(name="Lucy") is None
assert await User.objects.filter(name="Lucy").last_or_none() is None
assert await User.objects.order_by("name").last() == tom
@pytest.mark.asyncio
async def test_model_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),
Country.objects.create(name="Algeria"),
)
with pytest.raises(ValueError):
name, taxed, country_code = "Saudi Arabia", True, 1
await Country.objects.create(
name=name, taxed=taxed, country_code=country_code
)
with pytest.raises(ValueError):
name, taxed, country_code = "Algeria", True, 967
await Country.objects.create(
name=name, taxed=taxed, country_code=country_code
)
# test setting after init also triggers validation
with pytest.raises(ValueError):
name, taxed, country_code = "Algeria", True, 967
country = Country()
country.country_code = country_code
with pytest.raises(ValueError):
name, taxed, country_code = "Saudi Arabia", True, 1
country = Country()
country.name = name
# check also update from queryset
with pytest.raises(ValueError):
await Country(name="Belize").save()
await Country.objects.filter(name="Belize").update(name="Vietnam")
@pytest.mark.asyncio
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
with pytest.raises(ValueError):
await NotNullableCountry(name=None).save()
@pytest.mark.asyncio
async def test_start_and_end_filters():
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")
await User.objects.create(name="Louis Figo")
await User.objects.create(name="Loordi Kami")
await User.objects.create(name="Yuuki Sami")
users = await User.objects.filter(name__startswith="Mar").all()
assert len(users) == 1
users = await User.objects.filter(name__istartswith="ma").all()
assert len(users) == 3
users = await User.objects.filter(name__istartswith="Maq").all()
assert len(users) == 2
users = await User.objects.filter(name__iendswith="AMI").all()
assert len(users) == 2
users = await User.objects.filter(name__endswith="Uj").all()
assert len(users) == 1
users = await User.objects.filter(name__endswith="igo").all()
assert len(users) == 2
@pytest.mark.asyncio
async def test_get_and_first():
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")
await User.objects.create(name="Zack")
await User.objects.create(name="Ula")
user = await User.objects.get()
assert user.name == "Ula"
user = await User.objects.first()
assert user.name == "Tom"
await User2.objects.create(id="Tom", name="Tom")
await User2.objects.create(id="Jane", name="Jane")
await User2.objects.create(id="Lucy", name="Lucy")
await User2.objects.create(id="Zack", name="Zack")
await User2.objects.create(id="Ula", name="Ula")
user = await User2.objects.get()
assert user.name == "Zack"
user = await User2.objects.first()
assert user.name == "Jane"
def test_constraints():
with pytest.raises(pydantic.ValidationError) as e:
Product(name="T-Shirt", rating=50, in_stock=True)
assert "Input should be less than or equal to 5 " in str(e.value)
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_models_are_pickable.py 0000664 0000000 0000000 00000002610 15211567312 0030417 0 ustar 00root root 0000000 0000000 import pickle
from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class User(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="users")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
properties = ormar.JSON(nullable=True)
class Post(ormar.Model):
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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_dumping_and_loading_model_works():
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)
pickled_value = pickle.dumps(user)
python_value = pickle.loads(pickled_value)
assert isinstance(python_value, User)
assert python_value.name == "Test"
assert python_value.properties == {"aa": "bb"}
assert python_value.posts[0].name == "Test post"
await python_value.load()
await python_value.update(name="Test2")
check = await User.objects.get()
assert check.name == "Test2"
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_overwriting_pydantic_field_type.py 0000664 0000000 0000000 00000003641 15211567312 0033136 0 ustar 00root root 0000000 0000000 from typing import Optional
import pydantic
import pytest
from pydantic import Json, PositiveInt, ValidationError
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class OverwriteTest(ormar.Model):
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
class User(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="users")
id: int = ormar.Integer(primary_key=True)
email: str = ormar.String(
max_length=255,
unique=True,
nullable=False,
overwrite_pydantic_type=pydantic.EmailStr,
)
create_test_database = init_tests(base_ormar_config)
def test_constraints():
with pytest.raises(ValidationError, match="Input should be greater than 0"):
OverwriteTest(my_int=-10)
with pytest.raises(
ValidationError,
match="Input should be a valid integer, unable to parse string as an integer",
):
OverwriteTest(my_int=10, constraint_dict={"aa": "ab"})
with pytest.raises(
ValidationError,
match=(
r"The email address is not valid. It must have exactly one @-sign|"
r"An email address must have an @-sign"
),
):
User(email="wrong")
@pytest.mark.asyncio
async def test_saving():
async with base_ormar_config.database:
await OverwriteTest(my_int=5, constraint_dict={"aa": 123}).save()
test = await OverwriteTest.objects.get()
assert test.my_int == 5
assert test.constraint_dict == {"aa": 123}
await User(email="test@as.eu").save()
test = await User.objects.get()
assert test.email == "test@as.eu"
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_overwriting_sql_nullable.py 0000664 0000000 0000000 00000002223 15211567312 0031567 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import sqlalchemy
from sqlalchemy import text
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class PrimaryModel(ormar.Model):
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)
some_text: Optional[str] = ormar.Text(nullable=True, sql_nullable=False)
some_other_text: Optional[str] = ormar.String(
max_length=255, nullable=True, sql_nullable=False, server_default=text("''")
)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_create_models():
async with base_ormar_config.database:
primary = await PrimaryModel(
name="Foo", some_text="Bar", some_other_text="Baz"
).save()
assert primary.id == 1
primary2 = await PrimaryModel(name="Foo2", some_text="Bar2").save()
assert primary2.id == 2
with pytest.raises((sqlalchemy.exc.IntegrityError)):
await PrimaryModel(name="Foo3").save()
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_pk_field_is_always_not_null.py 0000664 0000000 0000000 00000001516 15211567312 0032221 0 ustar 00root root 0000000 0000000 from typing import Optional
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class AutoincrementModel(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
class NonAutoincrementModel(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True, autoincrement=False)
class ExplicitNullableModel(ormar.Model):
ormar_config = base_ormar_config.copy()
id: Optional[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.ormar_config.table.c.get("id").nullable
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_properties.py 0000664 0000000 0000000 00000004012 15211567312 0026645 0 ustar 00root root 0000000 0000000 # type: ignore
import pytest
from pydantic import PydanticUserError, computed_field
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Song(ormar.Model):
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()
@computed_field
def sorted_name(self) -> str:
return f"{self.sort_order}: {self.name}"
@computed_field
def sample(self) -> str:
return "sample"
@computed_field
def sample2(self) -> str:
return "sample2"
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_sort_order_on_main_model():
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.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.model_dump_json() for song in songs]
assert all("sorted_name" in x for x in song_json)
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].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(PydanticUserError):
class WrongModel(ormar.Model): # pragma: no cover
@computed_field
def test(self, aa=10, bb=30):
pass
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_pydantic_fields.py 0000664 0000000 0000000 00000007061 15211567312 0027621 0 ustar 00root root 0000000 0000000 import random
from typing import Optional
import pytest
from pydantic import BaseModel, Field, HttpUrl
from pydantic_extra_types.payment import PaymentCardNumber
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class ModelTest(ormar.Model):
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] = None
CARD_NUMBERS = [
"123456789007",
"123456789015",
"123456789023",
"123456789031",
"123456789049",
]
def get_number():
return random.choice(CARD_NUMBERS)
class ModelTest2(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=200)
url: HttpUrl = "https://www.example2.com" # type: ignore
number: PaymentCardNumber = Field(default_factory=get_number)
class PydanticTest(BaseModel):
aa: str
bb: int
class ModelTest3(ormar.Model):
ormar_config = base_ormar_config.copy()
def __init__(self, **kwargs):
kwargs["number"] = get_number()
kwargs["pydantic_test"] = PydanticTest(aa="random", bb=42)
super().__init__(**kwargs)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=200)
url: HttpUrl = "https://www.example3.com" # type: ignore
number: PaymentCardNumber
pydantic_test: PydanticTest
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_working_with_pydantic_fields():
async with base_ormar_config.database:
test = ModelTest(name="Test")
assert test.name == "Test"
assert test.url == "https://www.example.com"
assert test.number is None
test.number = "123456789015"
test.url = "https://www.sdta.ada.pt"
assert test.url == "https://www.sdta.ada.pt"
await test.save()
test_check = await ModelTest.objects.get()
assert test_check.name == "Test"
assert test_check.url == "https://www.example.com"
assert test_check.number is None
@pytest.mark.asyncio
async def test_default_factory_for_pydantic_fields():
async with base_ormar_config.database:
test = ModelTest2(name="Test2", number="4000000000000002")
assert test.name == "Test2"
assert test.url == "https://www.example2.com"
assert test.number == "4000000000000002"
test.url = "http://www.sdta.ada.pt"
assert test.url == "http://www.sdta.ada.pt"
await test.save()
test_check = await ModelTest2.objects.get()
assert test_check.name == "Test2"
assert test_check.url == "https://www.example2.com"
assert test_check.number in CARD_NUMBERS
assert test_check.number != test.number
@pytest.mark.asyncio
async def test_init_setting_for_pydantic_fields():
async with base_ormar_config.database:
test = ModelTest3(name="Test3")
assert test.name == "Test3"
assert test.url == "https://www.example3.com"
assert test.pydantic_test.bb == 42
test.url = "http://www.sdta.ada.pt"
assert test.url == "http://www.sdta.ada.pt"
await test.save()
test_check = await ModelTest3.objects.get()
assert test_check.name == "Test3"
assert test_check.url == "https://www.example3.com"
assert test_check.number in CARD_NUMBERS
assert test_check.pydantic_test.aa == "random"
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_pydantic_only_fields.py 0000664 0000000 0000000 00000004027 15211567312 0030661 0 ustar 00root root 0000000 0000000 import datetime
import pydantic
import pytest
from pydantic import computed_field
import ormar
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)
timestamp: datetime.datetime = pydantic.Field(default=None) # type: ignore
@computed_field
def name10(self) -> str:
return self.name + "_10"
@computed_field
def name20(self) -> str:
return self.name + "_20"
@property
def name30(self) -> str:
return self.name + "_30"
@computed_field
def name40(self) -> str:
return self.name + "_40"
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_pydantic_only_fields():
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
assert album.timestamp is None
album = await Album.objects.exclude_fields("timestamp").get()
assert album.timestamp is None
album = await Album.objects.fields({"name", "timestamp"}).get()
assert album.timestamp is None
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.model_dump()
assert "timestamp" in test_dict
assert test_dict["timestamp"] is not None
assert test_dict.get("name10") == "Hitchcock_10"
assert test_dict.get("name20") == "Hitchcock_20"
assert test_dict.get("name40") == "Hitchcock_40"
assert "name30" not in test_dict
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_pydantic_private_attributes.py 0000664 0000000 0000000 00000001322 15211567312 0032265 0 ustar 00root root 0000000 0000000 from pydantic import PrivateAttr
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Subscription(ormar.Model):
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)
_add_payments: list[str] = PrivateAttr(default_factory=list)
def add_payment(self, payment: str):
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")
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_save_status.py 0000664 0000000 0000000 00000016723 15211567312 0027026 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from ormar.exceptions import ModelPersistenceError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class NickNames(ormar.Model):
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")
is_lame: Optional[bool] = ormar.Boolean(nullable=True)
class NicksHq(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="nicks_x_hq")
class HQ(ormar.Model):
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")
nicks: list[NickNames] = ormar.ManyToMany(NickNames, through=NicksHq)
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, nullable=False, name="company_name")
founded: Optional[int] = ormar.Integer(nullable=True)
hq: HQ = ormar.ForeignKey(HQ)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_instantiation_false_save_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()
assert comp.saved
@pytest.mark.asyncio
async def test_saved_edited_not_saved():
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"
assert not comp.saved
await comp.update()
assert comp.saved
await comp.update(name="Banzai3")
assert comp.saved
comp.pk = 999
assert not comp.saved
await comp.update()
assert comp.saved
@pytest.mark.asyncio
async def test_adding_related_gets_dirty():
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
comp.hq = hq
assert not comp.saved
await comp.update()
assert comp.saved
comp = await Company.objects.select_related("hq").get(name="Banzai")
assert comp.saved
assert comp.hq.pk == hq.pk
assert comp.hq.saved
comp.hq.name = "Suburbs"
assert not comp.hq.saved
assert comp.saved
await comp.hq.update()
assert comp.hq.saved
@pytest.mark.asyncio
async def test_adding_many_to_many_does_not_gets_dirty():
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)
hq = await HQ.objects.create(name="Main")
assert hq.saved
await hq.nicks.add(nick1)
assert hq.saved
await hq.nicks.add(nick2)
assert hq.saved
hq = await HQ.objects.select_related("nicks").get(name="Main")
assert hq.saved
assert hq.nicks[0].saved
await hq.nicks.remove(nick1)
assert hq.saved
hq.nicks[0].name = "Kabucha"
assert not hq.nicks[0].saved
await hq.nicks[0].update()
assert hq.nicks[0].saved
@pytest.mark.asyncio
async def test_delete():
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()
assert not comp.saved
await comp.update()
assert comp.saved
@pytest.mark.asyncio
async def test_load():
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"
assert not comp.saved
await comp.load()
assert comp.saved
assert comp.name == "Banzai"
@pytest.mark.asyncio
async def test_queryset_methods():
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)
await Company.objects.create(name="Sumaaa", founded=1991)
comp = await Company.objects.get(name="Banzai")
assert comp.saved
comp = await Company.objects.first()
assert comp.saved
comps = await Company.objects.all()
assert [comp.saved for comp in comps]
comp2, created = await Company.objects.get_or_create(
name="Banzai_new", founded=2001
)
assert comp2.saved
assert created is True
comp3, created = await Company.objects.get_or_create(
name="Banzai", founded=1988
)
assert comp3.saved
assert comp3.pk == comp.pk
assert created is False
update_dict = comp.model_dump()
update_dict["founded"] = 2010
comp = await Company.objects.update_or_create(**update_dict)
assert comp.saved
assert comp.founded == 2010
create_dict = {"name": "Yoko", "founded": 2005}
comp = await Company.objects.update_or_create(**create_dict)
assert comp.saved
assert comp.founded == 2005
@pytest.mark.asyncio
async def test_bulk_methods():
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)
await Company.objects.bulk_create([c1, c2])
assert c1.saved
assert c2.saved
c1, c2 = await Company.objects.all()
c1.name = "Banzai2"
c2.name = "Yuhu2"
assert not c1.saved
assert not c2.saved
await Company.objects.bulk_update([c1, c2])
assert c1.saved
assert c2.saved
c3 = Company(name="Cobra", founded=2088)
assert not c3.saved
with pytest.raises(ModelPersistenceError):
await c3.update()
await c3.upsert()
assert c3.saved
c3.name = "Python"
assert not c3.saved
await c3.upsert()
assert c3.saved
assert c3.name == "Python"
await c3.upsert(founded=2077)
assert c3.saved
assert c3.founded == 2077
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_saving_nullable_fields.py 0000664 0000000 0000000 00000003127 15211567312 0031152 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class PrimaryModel(ormar.Model):
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)
some_text: str = ormar.Text()
# NOTE: Removing nullable=True makes the test pass.
some_other_text: Optional[str] = ormar.Text(nullable=True)
class SecondaryModel(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="secondary_models")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
primary_model: PrimaryModel = ormar.ForeignKey(
PrimaryModel, related_name="secondary_models"
)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_create_models():
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()
assert primary.id == 1
secondary = await SecondaryModel(name="Foo", primary_model=primary).save()
assert secondary.id == 1
assert secondary.primary_model.id == 1
secondary = await SecondaryModel.objects.get()
assert secondary.name == "Foo"
await secondary.update(name="Updated")
assert secondary.name == "Updated"
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_server_default.py 0000664 0000000 0000000 00000004446 15211567312 0027476 0 ustar 00root root 0000000 0000000 import time
from datetime import datetime
import pytest
from sqlalchemy import func, text
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Product(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="product")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
company: str = ormar.String(max_length=200, server_default="Acme")
sort_order: int = ormar.Integer(server_default=text("10"))
created: datetime = ormar.DateTime(server_default=func.now())
create_test_database = init_tests(base_ormar_config)
def test_table_defined_properly():
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 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()
await p1.load()
assert p1.created is not None
assert p1.company == "Acme"
assert p1.sort_order == 10
date = datetime.strptime("2020-10-27 11:30", "%Y-%m-%d %H:%M")
p3 = await Product.objects.create(
name="Test2", created=date, company="Roadrunner", sort_order=1
)
assert p3.created is not None
assert p3.created == date
assert p1.created != p3.created
assert p3.company == "Roadrunner"
assert p3.sort_order == 1
p3 = await Product.objects.get(name="Test2")
assert p3.company == "Roadrunner"
assert p3.sort_order == 1
time.sleep(1)
p2 = await Product.objects.create(name="Test3")
assert p2.created is not None
assert p2.company == "Acme"
assert p2.sort_order == 10
if Product.db_backend_name() != "postgresql":
# postgres use transaction timestamp so it will remain the same
assert p1.created != p2.created # pragma nocover
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_setting_comments_in_db.py 0000664 0000000 0000000 00000003635 15211567312 0031200 0 ustar 00root root 0000000 0000000 import pytest
import ormar
from ormar.models import Model
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(Model):
ormar_config = base_ormar_config.copy(tablename="comment_authors")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=80)
class Tag(Model):
ormar_config = base_ormar_config.copy(tablename="comment_tags")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=40)
class Comment(Model):
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")
author: Author = ormar.ForeignKey(Author, comment="author of the comment")
tags = ormar.ManyToMany(Tag, comment="tags attached to the comment")
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_comments_are_set_in_db():
columns = Comment.ormar_config.table.c
for c in columns:
assert c.comment == Comment.ormar_config.model_fields[c.name].comment
def test_foreign_key_comment_reaches_field_and_column():
assert (
Comment.ormar_config.model_fields["author"].comment == "author of the comment"
)
assert Comment.ormar_config.table.c.author.comment == "author of the comment"
def test_many_to_many_comment_stored_on_field():
# M2M has no column on the owner model's table and does not propagate the
# comment to the through model's columns, so the field attribute is the
# only observable side effect.
assert (
Comment.ormar_config.model_fields["tags"].comment
== "tags attached to the comment"
)
through = Comment.ormar_config.model_fields["tags"].through
assert all(c.comment is None for c in through.ormar_config.table.c)
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_table_comment.py 0000664 0000000 0000000 00000003760 15211567312 0027273 0 ustar 00root root 0000000 0000000 """Tests for ``OrmarConfig.comment`` forwarding to ``sqlalchemy.Table``."""
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class CommentedModel(ormar.Model):
ormar_config = base_ormar_config.copy(
tablename="commented_models",
comment="Stores commented things.",
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class UncommentedModel(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="uncommented_models")
id: int = ormar.Integer(primary_key=True)
create_test_database = init_tests(base_ormar_config)
def test_comment_forwarded_to_sqlalchemy_table():
assert CommentedModel.ormar_config.table.comment == "Stores commented things."
def test_comment_defaults_to_none():
assert UncommentedModel.ormar_config.comment is None
assert UncommentedModel.ormar_config.table.comment is None
def test_copy_inherits_parent_comment_when_omitted():
parent = ormar.OrmarConfig(
metadata=base_ormar_config.metadata,
database=base_ormar_config.database,
engine=base_ormar_config.engine,
comment="parent comment",
)
child = parent.copy(tablename="child")
assert child.comment == "parent comment"
def test_copy_can_explicitly_clear_parent_comment():
parent = ormar.OrmarConfig(
metadata=base_ormar_config.metadata,
database=base_ormar_config.database,
engine=base_ormar_config.engine,
comment="parent comment",
)
child = parent.copy(tablename="child", comment=None)
assert child.comment is None
def test_copy_can_override_parent_comment():
parent = ormar.OrmarConfig(
metadata=base_ormar_config.metadata,
database=base_ormar_config.database,
engine=base_ormar_config.engine,
comment="parent comment",
)
child = parent.copy(tablename="child", comment="child comment")
assert child.comment == "child comment"
ormar-orm-ormar-a7e7fde/tests/test_model_definition/test_through_model_relation_setup_on_clone.py 0000664 0000000 0000000 00000001422 15211567312 0034304 0 ustar 00root root 0000000 0000000 import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id = ormar.Integer(primary_key=True)
name = ormar.String(max_length=100)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="books")
id = ormar.Integer(primary_key=True)
title = ormar.String(max_length=100)
author = ormar.ManyToMany(
Author,
)
year = ormar.Integer(nullable=True)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_tables_are_created():
async with base_ormar_config.database:
assert await Book.objects.all() == []
ormar-orm-ormar-a7e7fde/tests/test_model_methods/ 0000775 0000000 0000000 00000000000 15211567312 0022356 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_model_methods/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0024455 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_model_methods/test_excludes_in_load_all.py 0000664 0000000 0000000 00000003645 15211567312 0030130 0 ustar 00root root 0000000 0000000 import uuid
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config(force_rollback=True)
class JimmyUser(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="jimmy_users")
id: uuid.UUID = ormar.UUID(
primary_key=True, default=uuid.uuid4(), uuid_format="string"
)
class JimmyProfile(ormar.Model):
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):
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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_excluding_one_relation():
async with base_ormar_config.database:
user = JimmyUser()
await user.save()
await JimmyAccount(user=user).save()
await JimmyProfile(user=user).save()
await user.load_all(exclude={"jimmyprofiles"})
assert hasattr(user.jimmyaccounts[0], "name")
assert len(user.jimmyprofiles) == 0
@pytest.mark.asyncio
async def test_excluding_other_relation():
async with base_ormar_config.database:
user = JimmyUser()
await user.save()
await JimmyAccount(user=user).save()
await JimmyProfile(user=user).save()
await user.load_all(exclude={"jimmyaccounts"})
assert await JimmyProfile.objects.get()
assert hasattr(user.jimmyprofiles[0], "name")
assert len(user.jimmyaccounts) == 0
ormar-orm-ormar-a7e7fde/tests/test_model_methods/test_load_all.py 0000664 0000000 0000000 00000021150 15211567312 0025535 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Language(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="languages")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
level: str = ormar.String(max_length=150, default="Beginner")
class CringeLevel(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="levels")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
language = ormar.ForeignKey(Language)
class NickName(ormar.Model):
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")
is_lame: Optional[bool] = ormar.Boolean(nullable=True)
level: CringeLevel = ormar.ForeignKey(CringeLevel)
class HQ(ormar.Model):
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")
nicks: list[NickName] = ormar.ManyToMany(NickName)
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, nullable=False, name="company_name")
founded: Optional[int] = ormar.Integer(nullable=True)
hq: HQ = ormar.ForeignKey(HQ, related_name="companies")
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_load_all_fk_rel():
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)
hq = await HQ.objects.get(name="Main")
await hq.load_all()
assert hq.companies[0] == company
assert hq.companies[0].name == "Banzai"
assert hq.companies[0].founded == 1988
hq2 = await HQ.objects.select_all().get(name="Main")
assert hq2.companies[0] == company
assert hq2.companies[0].name == "Banzai"
assert hq2.companies[0].founded == 1988
@pytest.mark.asyncio
async def test_load_all_many_to_many():
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")
await hq.nicks.add(nick1)
await hq.nicks.add(nick2)
hq = await HQ.objects.get(name="Main")
await hq.load_all()
assert hq.nicks[0] == nick1
assert hq.nicks[0].name == "BazingaO"
assert hq.nicks[1] == nick2
assert hq.nicks[1].name == "Bazinga20"
hq2 = await HQ.objects.select_all().get(name="Main")
assert hq2.nicks[0] == nick1
assert hq2.nicks[0].name == "BazingaO"
assert hq2.nicks[1] == nick2
assert hq2.nicks[1].name == "Bazinga20"
@pytest.mark.asyncio
async def test_load_all_with_order():
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")
await hq.nicks.add(nick1)
await hq.nicks.add(nick2)
hq = await HQ.objects.get(name="Main")
await hq.load_all(order_by="-nicks__name")
assert hq.nicks[0] == nick2
assert hq.nicks[0].name == "Joe"
assert hq.nicks[1] == nick1
assert hq.nicks[1].name == "Barry"
await hq.load_all()
assert hq.nicks[0] == nick1
assert hq.nicks[1] == nick2
hq2 = (
await HQ.objects.select_all().order_by("-nicks__name").get(name="Main")
)
assert hq2.nicks[0] == nick2
assert hq2.nicks[1] == nick1
hq3 = await HQ.objects.select_all().get(name="Main")
assert hq3.nicks[0] == nick1
assert hq3.nicks[1] == nick2
@pytest.mark.asyncio
async def test_loading_reversed_relation():
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)
company = await Company.objects.get(name="Banzai")
await company.load_all()
assert company.hq == hq
company2 = await Company.objects.select_all().get(name="Banzai")
assert company2.hq == hq
@pytest.mark.asyncio
async def test_loading_nested():
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)
nick1 = await NickName.objects.create(
name="BazingaO", is_lame=False, level=level
)
nick2 = await NickName.objects.create(
name="Bazinga20", is_lame=True, level=level2
)
hq = await HQ.objects.create(name="Main")
await hq.nicks.add(nick1)
await hq.nicks.add(nick2)
hq = await HQ.objects.get(name="Main")
await hq.load_all(follow=True)
assert hq.nicks[0] == nick1
assert hq.nicks[0].name == "BazingaO"
assert hq.nicks[0].level.name == "High"
assert hq.nicks[0].level.language.name == "English"
assert hq.nicks[1] == nick2
assert hq.nicks[1].name == "Bazinga20"
assert hq.nicks[1].level.name == "Low"
assert hq.nicks[1].level.language.name == "English"
hq2 = await HQ.objects.select_all(follow=True).get(name="Main")
assert hq2.nicks[0] == nick1
assert hq2.nicks[0].name == "BazingaO"
assert hq2.nicks[0].level.name == "High"
assert hq2.nicks[0].level.language.name == "English"
assert hq2.nicks[1] == nick2
assert hq2.nicks[1].name == "Bazinga20"
assert hq2.nicks[1].level.name == "Low"
assert hq2.nicks[1].level.language.name == "English"
hq5 = await HQ.objects.select_all().get(name="Main")
assert len(hq5.nicks) == 2
await hq5.nicks.select_all(follow=True).all()
assert hq5.nicks[0] == nick1
assert hq5.nicks[0].name == "BazingaO"
assert hq5.nicks[0].level.name == "High"
assert hq5.nicks[0].level.language.name == "English"
assert hq5.nicks[1] == nick2
assert hq5.nicks[1].name == "Bazinga20"
assert hq5.nicks[1].level.name == "Low"
assert hq5.nicks[1].level.language.name == "English"
await hq.load_all(follow=True, exclude="nicks__level__language")
assert len(hq.nicks) == 2
assert hq.nicks[0].level.language is None
assert hq.nicks[1].level.language is None
hq3 = (
await HQ.objects.select_all(follow=True)
.exclude_fields("nicks__level__language")
.get(name="Main")
)
assert len(hq3.nicks) == 2
assert hq3.nicks[0].level.language is None
assert hq3.nicks[1].level.language is None
await hq.load_all(follow=True, exclude="nicks__level__language__level")
assert len(hq.nicks) == 2
assert hq.nicks[0].level.language is not None
assert hq.nicks[0].level.language.level is None
assert hq.nicks[1].level.language is not None
assert hq.nicks[1].level.language.level is None
await hq.load_all(follow=True, exclude="nicks__level")
assert len(hq.nicks) == 2
assert hq.nicks[0].level is None
assert hq.nicks[1].level is None
await hq.load_all(follow=True, exclude="nicks")
assert len(hq.nicks) == 0
ormar-orm-ormar-a7e7fde/tests/test_model_methods/test_populate_default_values.py 0000664 0000000 0000000 00000001645 15211567312 0030711 0 ustar 00root root 0000000 0000000 from sqlalchemy import text
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Task(ormar.Model):
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
)
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():
new_kwargs = {
"id": None,
"name": "",
"points": 0,
}
result = Task.populate_default_values(new_kwargs)
assert result["id"] is None
assert result["name"] == ""
assert result["points"] == 0
assert result["score"] == 5
ormar-orm-ormar-a7e7fde/tests/test_model_methods/test_populate_onupdate_values.py 0000664 0000000 0000000 00000022054 15211567312 0031101 0 ustar 00root root 0000000 0000000 import base64
import enum
import uuid
from datetime import date, datetime, time
from decimal import Decimal
from typing import Optional
import pytest
from sqlalchemy import func
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
PAST = datetime(2000, 1, 1, 0, 0, 0)
def _past() -> datetime:
return PAST
class Task(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="tasks")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(
max_length=255,
on_update=lambda: "hello",
)
points: int = ormar.Integer(default=0, minimum=0, on_update=1)
year: int = ormar.Integer(default=1, on_update=2)
updated_at: Optional[datetime] = ormar.DateTime(
default=_past, server_default=func.now(), on_update=datetime.now
)
class Size(enum.Enum):
SMALL = "small"
LARGE = "large"
UPDATED_UUID = uuid.UUID("11111111-1111-1111-1111-111111111111")
class AllTypes(ormar.Model):
"""Covers every ormar field type so on_update is exercised against each."""
ormar_config = base_ormar_config.copy(tablename="all_types")
id: int = ormar.Integer(primary_key=True)
flag: bool = ormar.Boolean(default=False, on_update=True)
count: int = ormar.Integer(default=0, on_update=42)
big: int = ormar.BigInteger(default=0, on_update=9_000_000_000)
small: int = ormar.SmallInteger(default=0, on_update=7)
ratio: float = ormar.Float(default=0.0, on_update=3.14)
note: str = ormar.String(max_length=100, default="initial", on_update="updated_s")
body: str = ormar.Text(default="", on_update="updated_t")
amount: Decimal = ormar.Decimal(
max_digits=10,
decimal_places=2,
default=Decimal("0.00"),
on_update=Decimal("9.99"),
)
when: datetime = ormar.DateTime(default=_past, on_update=datetime(2030, 1, 1))
day: date = ormar.Date(default=date(2000, 1, 1), on_update=date(2030, 1, 1))
clock: time = ormar.Time(default=time(0, 0, 0), on_update=time(12, 30, 45))
meta: dict = ormar.JSON(default={"v": 0}, on_update={"v": 1})
token: uuid.UUID = ormar.UUID(
default=uuid.UUID("00000000-0000-0000-0000-000000000000"),
on_update=UPDATED_UUID,
)
blob: bytes = ormar.LargeBinary(
max_length=100, default=b"initial_blob", on_update=b"updated_blob"
)
blob64: str = ormar.LargeBinary(
max_length=100,
represent_as_base64_str=True,
default=b"initial_b64",
on_update=b"updated_b64",
)
size: Size = ormar.Enum(enum_class=Size, default=Size.SMALL, on_update=Size.LARGE)
secret: str = ormar.String(
max_length=100,
default="initial_secret",
on_update="updated_secret",
encrypt_secret="key_for_tests",
encrypt_backend=ormar.EncryptBackends.FERNET,
)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_onupdate_use_setattr_to_update():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
t1 = await Task.objects.create(name="123")
assert t1.name == "123"
assert t1.points == 0
assert t1.year == 1
assert t1.updated_at == PAST
t2 = await Task.objects.get(name="123")
t2.name = "explicit"
t2.year = 2024
await t2.update()
assert t2.name == "explicit"
assert t2.points == 1
assert t2.year == 2024
assert t2.updated_at > PAST
@pytest.mark.asyncio
async def test_onupdate_use_update_func_kwargs():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
t1 = await Task.objects.create(name="123")
assert t1.name == "123"
assert t1.points == 0
assert t1.year == 1
assert t1.updated_at == PAST
t2 = await Task.objects.get(name="123")
await t2.update(name="from_kwargs")
assert t2.name == "from_kwargs"
assert t2.points == 1
assert t2.year == 2
assert t2.updated_at > PAST
@pytest.mark.asyncio
async def test_onupdate_use_update_func_columns():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
t1 = await Task.objects.create(name="123")
assert t1.name == "123"
assert t1.points == 0
assert t1.year == 1
assert t1.updated_at == PAST
t2 = await Task.objects.get(name="123")
await t2.update(_columns=["year"], year=2024)
assert t2.name == "hello"
assert t2.points == 1
assert t2.year == 2024
assert t2.updated_at > PAST
@pytest.mark.asyncio
async def test_onupdate_queryset_update():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
t1 = await Task.objects.create(name="123")
assert t1.name == "123"
assert t1.points == 0
assert t1.year == 1
assert t1.updated_at == PAST
await Task.objects.filter(name="123").update(name="qs_update")
t2 = await Task.objects.get(name="qs_update")
assert t2.name == "qs_update"
assert t2.points == 1
assert t2.year == 2
assert t2.updated_at > PAST
@pytest.mark.asyncio
async def test_onupdate_upsert():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
t1 = await Task.objects.create(name="upsert_initial")
assert t1.updated_at == PAST
t1.name = "upsert_modified"
await t1.upsert()
assert t1.name == "upsert_modified"
assert t1.points == 1
assert t1.year == 2
assert t1.updated_at > PAST
@pytest.mark.asyncio
async def test_onupdate_bulk_update():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
t1 = await Task.objects.create(name="123")
assert t1.name == "123"
assert t1.points == 0
assert t1.year == 1
assert t1.updated_at == PAST
t2 = await Task.objects.get(name="123")
t2.name = "bulk_update"
await Task.objects.bulk_update([t2])
t3 = await Task.objects.get(name="bulk_update")
assert t3.name == "bulk_update"
assert t3.points == 1
assert t3.year == 2
assert t3.updated_at > PAST
t4 = await Task.objects.get(name="bulk_update")
t4.year = 2024
await Task.objects.bulk_update([t4], columns=["year"])
t5 = await Task.objects.get(year=2024)
assert t5.year == 2024
assert t5.points == 1
assert t5.name == "hello"
assert t5.updated_at > PAST
@pytest.mark.asyncio
async def test_onupdate_all_field_types():
"""Exercises on_update against every ormar field type."""
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
created = await AllTypes.objects.create(id=1)
assert created.flag is False
assert created.count == 0
assert created.big == 0
assert created.small == 0
assert created.ratio == 0.0
assert created.note == "initial"
assert created.body == ""
assert created.amount == Decimal("0.00")
assert created.when == PAST
assert created.day == date(2000, 1, 1)
assert created.clock == time(0, 0, 0)
assert created.meta == {"v": 0}
assert created.token == uuid.UUID("00000000-0000-0000-0000-000000000000")
assert created.blob == b"initial_blob"
assert created.size is Size.SMALL
assert created.secret == "initial_secret"
fetched = await AllTypes.objects.get(id=1)
await fetched.update()
reloaded = await AllTypes.objects.get(id=1)
assert reloaded.flag is True
assert reloaded.count == 42
assert reloaded.big == 9_000_000_000
assert reloaded.small == 7
assert reloaded.ratio == 3.14
assert reloaded.note == "updated_s"
assert reloaded.body == "updated_t"
assert reloaded.amount == Decimal("9.99")
assert reloaded.when == datetime(2030, 1, 1)
assert reloaded.day == date(2030, 1, 1)
assert reloaded.clock == time(12, 30, 45)
assert reloaded.meta == {"v": 1}
assert reloaded.token == UPDATED_UUID
assert reloaded.blob == b"updated_blob"
assert reloaded.blob64 == base64.b64encode(b"updated_b64").decode()
assert reloaded.size is Size.LARGE
assert reloaded.secret == "updated_secret"
ormar-orm-ormar-a7e7fde/tests/test_model_methods/test_save_related.py 0000664 0000000 0000000 00000016551 15211567312 0026435 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class CringeLevel(ormar.Model):
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):
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")
is_lame: Optional[bool] = ormar.Boolean(nullable=True)
level: CringeLevel = ormar.ForeignKey(CringeLevel)
class NicksHq(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="nicks_x_hq")
class HQ(ormar.Model):
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")
nicks: list[NickName] = ormar.ManyToMany(NickName, through=NicksHq)
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, nullable=False, name="company_name")
founded: Optional[int] = ormar.Integer(nullable=True)
hq: HQ = ormar.ForeignKey(HQ, related_name="companies")
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_saving_related_fk_rel():
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
count = await comp.save_related()
assert count == 0
comp.hq.name = "Suburbs"
assert not comp.hq.saved
assert comp.saved
count = await comp.save_related()
assert count == 1
assert comp.hq.saved
comp.hq.name = "Suburbs 2"
assert not comp.hq.saved
assert comp.saved
count = await comp.save_related(exclude={"hq"})
assert count == 0
assert not comp.hq.saved
@pytest.mark.asyncio
async def test_saving_many_to_many():
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")
assert hq.saved
await hq.nicks.add(nick1)
assert hq.saved
await hq.nicks.add(nick2)
assert hq.saved
count = await hq.save_related()
assert count == 0
count = await hq.save_related(save_all=True)
assert count == 3
hq.nicks[0].name = "Kabucha"
hq.nicks[1].name = "Kabucha2"
assert not hq.nicks[0].saved
assert not hq.nicks[1].saved
count = await hq.save_related()
assert count == 2
assert hq.nicks[0].saved
assert hq.nicks[1].saved
hq.nicks[0].name = "Kabucha a"
hq.nicks[1].name = "Kabucha2 a"
assert not hq.nicks[0].saved
assert not hq.nicks[1].saved
count = await hq.save_related(exclude={"nicks": ...})
assert count == 0
assert not hq.nicks[0].saved
assert not hq.nicks[1].saved
@pytest.mark.asyncio
async def test_saving_reversed_relation():
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)
hq = await HQ.objects.select_related("companies").get(name="Main")
assert hq.saved
assert hq.companies[0].saved
hq.companies[0].name = "Konichiwa"
assert not hq.companies[0].saved
count = await hq.save_related()
assert count == 1
assert hq.companies[0].saved
await Company.objects.create(name="Joshua", founded=1888, hq=hq)
hq = await HQ.objects.select_related("companies").get(name="Main")
assert hq.saved
assert hq.companies[0].saved
assert hq.companies[1].saved
hq.companies[0].name = hq.companies[0].name + "20"
assert not hq.companies[0].saved
# save only if not saved so now only one
count = await hq.save_related()
assert count == 1
assert hq.companies[0].saved
hq.companies[0].name = hq.companies[0].name + "20"
hq.companies[1].name = hq.companies[1].name + "30"
assert not hq.companies[0].saved
assert not hq.companies[1].saved
count = await hq.save_related()
assert count == 2
assert hq.companies[0].saved
assert hq.companies[1].saved
@pytest.mark.asyncio
async def test_saving_nested():
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(
name="BazingaO", is_lame=False, level=level
)
nick2 = await NickName.objects.create(
name="Bazinga20", is_lame=True, level=level2
)
hq = await HQ.objects.create(name="Main")
assert hq.saved
await hq.nicks.add(nick1)
assert hq.saved
await hq.nicks.add(nick2)
assert hq.saved
count = await hq.save_related()
assert count == 0
hq.nicks[0].level.name = "Medium"
assert not hq.nicks[0].level.saved
assert hq.nicks[0].saved
count = await hq.save_related(follow=True)
assert count == 1
assert hq.nicks[0].saved
assert hq.nicks[0].level.saved
hq.nicks[0].level.name = "Low"
hq.nicks[1].level.name = "Medium"
assert not hq.nicks[0].level.saved
assert not hq.nicks[1].level.saved
assert hq.nicks[0].saved
assert hq.nicks[1].saved
count = await hq.save_related(follow=True)
assert count == 2
assert hq.nicks[0].saved
assert hq.nicks[0].level.saved
assert hq.nicks[1].saved
assert hq.nicks[1].level.saved
hq.nicks[0].level.name = "Low 2"
hq.nicks[1].level.name = "Medium 2"
assert not hq.nicks[0].level.saved
assert not hq.nicks[1].level.saved
assert hq.nicks[0].saved
assert hq.nicks[1].saved
count = await hq.save_related(follow=True, exclude={"nicks": {"level"}})
assert count == 0
assert hq.nicks[0].saved
assert not hq.nicks[0].level.saved
assert hq.nicks[1].saved
assert not hq.nicks[1].level.saved
ormar-orm-ormar-a7e7fde/tests/test_model_methods/test_save_related_from_dict.py 0000664 0000000 0000000 00000021262 15211567312 0030456 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class CringeLevel(ormar.Model):
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):
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")
is_lame: Optional[bool] = ormar.Boolean(nullable=True)
level: CringeLevel = ormar.ForeignKey(CringeLevel)
class NicksHq(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="nicks_x_hq")
id: int = ormar.Integer(primary_key=True)
new_field: Optional[str] = ormar.String(max_length=200, nullable=True)
class HQ(ormar.Model):
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")
nicks: list[NickName] = ormar.ManyToMany(NickName, through=NicksHq)
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, nullable=False, name="company_name")
founded: Optional[int] = ormar.Integer(nullable=True)
hq: HQ = ormar.ForeignKey(HQ, related_name="companies")
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_saving_related_reverse_fk():
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)
assert count == 2
hq_check = await HQ.objects.select_related("companies").get()
assert hq_check.pk is not None
assert hq_check.name == "Main"
assert len(hq_check.companies) == 1
assert hq_check.companies[0].name == "Banzai"
assert hq_check.companies[0].pk is not None
@pytest.mark.asyncio
async def test_saving_related_reverse_fk_multiple():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
payload = {
"companies": [{"name": "Banzai"}, {"name": "Yamate"}],
"name": "Main",
}
hq = HQ(**payload)
count = await hq.save_related(follow=True, save_all=True)
assert count == 3
hq_check = await HQ.objects.select_related("companies").get()
assert hq_check.pk is not None
assert hq_check.name == "Main"
assert len(hq_check.companies) == 2
assert hq_check.companies[0].name == "Banzai"
assert hq_check.companies[0].pk is not None
assert hq_check.companies[1].name == "Yamate"
assert hq_check.companies[1].pk is not None
@pytest.mark.asyncio
async def test_saving_related_fk():
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)
assert count == 2
comp_check = await Company.objects.select_related("hq").get()
assert comp_check.pk is not None
assert comp_check.name == "Banzai"
assert comp_check.hq.name == "Main"
assert comp_check.hq.pk is not None
@pytest.mark.asyncio
async def test_saving_many_to_many_wo_through():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
payload = {
"name": "Main",
"nicks": [
{"name": "Bazinga0", "is_lame": False},
{"name": "Bazinga20", "is_lame": True},
],
}
hq = HQ(**payload)
count = await hq.save_related()
assert count == 3
hq_check = await HQ.objects.select_related("nicks").get()
assert hq_check.pk is not None
assert len(hq_check.nicks) == 2
assert hq_check.nicks[0].name == "Bazinga0"
assert hq_check.nicks[1].name == "Bazinga20"
@pytest.mark.asyncio
async def test_saving_many_to_many_with_through():
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": [
{
"name": "Bazinga0",
"is_lame": False,
"nickshq": {"new_field": "test"},
},
{
"name": "Bazinga20",
"is_lame": True,
"nickshq": {"new_field": "test2"},
},
],
}
hq = HQ(**payload)
count = await hq.save_related()
assert count == 3
hq_check = await HQ.objects.select_related("nicks").get()
assert hq_check.pk is not None
assert len(hq_check.nicks) == 2
assert hq_check.nicks[0].name == "Bazinga0"
assert hq_check.nicks[0].nickshq.new_field == "test"
assert hq_check.nicks[1].name == "Bazinga20"
assert hq_check.nicks[1].nickshq.new_field == "test2"
@pytest.mark.asyncio
async def test_saving_nested_with_m2m_and_rev_fk():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
payload = {
"name": "Main",
"nicks": [
{"name": "Bazinga0", "is_lame": False, "level": {"name": "High"}},
{"name": "Bazinga20", "is_lame": True, "level": {"name": "Low"}},
],
}
hq = HQ(**payload)
count = await hq.save_related(follow=True, save_all=True)
assert count == 5
hq_check = await HQ.objects.select_related("nicks__level").get()
assert hq_check.pk is not None
assert len(hq_check.nicks) == 2
assert hq_check.nicks[0].name == "Bazinga0"
assert hq_check.nicks[0].level.name == "High"
assert hq_check.nicks[1].name == "Bazinga20"
assert hq_check.nicks[1].level.name == "Low"
@pytest.mark.asyncio
async def test_saving_nested_with_m2m_and_rev_fk_and_through():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
payload = {
"hq": {
"name": "Yoko",
"nicks": [
{
"name": "Bazinga0",
"is_lame": False,
"nickshq": {"new_field": "test"},
"level": {"name": "High"},
},
{
"name": "Bazinga20",
"is_lame": True,
"nickshq": {"new_field": "test2"},
"level": {"name": "Low"},
},
],
},
"name": "Main",
}
company = Company(**payload)
count = await company.save_related(follow=True, save_all=True)
assert count == 6
company_check = await Company.objects.select_related(
"hq__nicks__level"
).get()
assert company_check.pk is not None
assert company_check.name == "Main"
assert company_check.hq.name == "Yoko"
assert len(company_check.hq.nicks) == 2
assert company_check.hq.nicks[0].name == "Bazinga0"
assert company_check.hq.nicks[0].nickshq.new_field == "test"
assert company_check.hq.nicks[0].level.name == "High"
assert company_check.hq.nicks[1].name == "Bazinga20"
assert company_check.hq.nicks[1].level.name == "Low"
assert company_check.hq.nicks[1].nickshq.new_field == "test2"
ormar-orm-ormar-a7e7fde/tests/test_model_methods/test_save_related_pk_only.py 0000664 0000000 0000000 00000001346 15211567312 0030164 0 ustar 00root root 0000000 0000000 import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class A(ormar.Model):
ormar_config = base_ormar_config.copy()
id = ormar.Integer(primary_key=True)
class B(ormar.Model):
ormar_config = base_ormar_config.copy()
id = ormar.Integer(primary_key=True)
a = ormar.ForeignKey(A)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_saving_related_pk_only():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
a = A()
await a.save()
await a.save_related(follow=True, save_all=True)
ormar-orm-ormar-a7e7fde/tests/test_model_methods/test_save_related_uuid.py 0000664 0000000 0000000 00000004023 15211567312 0027452 0 ustar 00root root 0000000 0000000 import uuid
from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Department(ormar.Model):
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):
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)
completed: bool = ormar.Boolean()
department: Optional[Department] = ormar.ForeignKey(Department)
class Student(ormar.Model):
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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_uuid_pk_in_save_related():
async with base_ormar_config.database:
to_save = {
"department_name": "Ormar",
"courses": [
{
"course_name": "basic1",
"completed": True,
"students": [{"name": "Abi"}, {"name": "Jack"}],
},
{
"course_name": "basic2",
"completed": True,
"students": [{"name": "Kate"}, {"name": "Miranda"}],
},
],
}
department = Department(**to_save)
await department.save_related(follow=True, save_all=True)
department_check = (
await Department.objects.select_all(follow=True)
.order_by(Department.courses.students.name.asc())
.get()
)
to_exclude = {
"id": ...,
"courses": {"id": ..., "students": {"id", "studentcourse"}},
}
assert department_check.model_dump(exclude=to_exclude) == to_save
ormar-orm-ormar-a7e7fde/tests/test_model_methods/test_save_server_default_pk.py 0000664 0000000 0000000 00000014626 15211567312 0030522 0 ustar 00root root 0000000 0000000 """Tests for ``Model.save()`` behavior around ``server_default`` on the pk.
Covers two closely related concerns:
1. N+1 fix (PR #919) — when the pk is the only ``server_default`` field, the
INSERT's RETURNING clause already provides the pk, so ``save()`` must not
issue a second SELECT to reload the model.
2. Pk-recovery loud-fail — on backends that cannot return a server-generated
pk (Oracle MySQL has no RETURNING), ``save()`` must raise
``ModelPersistenceError`` rather than silently storing a bogus pk (the old
behavior was to return ``rowcount`` from the executor, which ``save()``
then mistook for the pk).
"""
from typing import Any, List
import pytest
from sqlalchemy import event, text
import ormar
from ormar.exceptions import ModelPersistenceError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
_IS_MYSQL = "mysql" in base_ormar_config.database.url
class ServerDefaultPk(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="server_default_pk")
id: int = ormar.Integer(
primary_key=True, autoincrement=False, server_default=text("100")
)
name: str = ormar.String(max_length=100)
class ServerDefaultNonPk(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="server_default_nonpk")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
company: str = ormar.String(max_length=100, server_default="Acme")
class ServerDefaultPkAndNonPk(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="server_default_pk_and_nonpk")
id: int = ormar.Integer(
primary_key=True, autoincrement=False, server_default=text("200")
)
name: str = ormar.String(max_length=100)
company: str = ormar.String(max_length=100, server_default="Acme")
create_test_database = init_tests(base_ormar_config)
class _StatementCounter:
"""Records every statement executed on the sqlalchemy engine."""
def __init__(self) -> None:
self.statements: List[str] = []
def __enter__(self) -> "_StatementCounter":
sync_engine = base_ormar_config.database.engine.sync_engine
def before_cursor_execute(
conn: Any,
cursor: Any,
statement: str,
parameters: Any,
context: Any,
executemany: bool,
) -> None:
self.statements.append(statement)
self._listener = before_cursor_execute
self._sync_engine = sync_engine
event.listen(sync_engine, "before_cursor_execute", self._listener)
return self
def __exit__(self, *exc: Any) -> None:
event.remove(self._sync_engine, "before_cursor_execute", self._listener)
def _table_selects(statements: List[str], tablename: str) -> List[str]:
return [
s
for s in statements
if s.lstrip().upper().startswith("SELECT") and tablename in s
]
@pytest.mark.asyncio
@pytest.mark.skipif(
_IS_MYSQL,
reason=(
"Oracle MySQL has no RETURNING clause, so a server_default on a "
"non-AUTO_INCREMENT pk cannot be recovered — covered by "
"test_save_raises_when_server_default_pk_cannot_be_recovered instead."
),
)
async def test_save_does_not_reload_when_only_pk_has_server_default(): # noqa: E501 # pragma: no cover
"""INSERT returns the server-generated pk, so no SELECT should follow."""
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
with _StatementCounter() as counter:
instance = ServerDefaultPk(name="first")
await instance.save()
selects = _table_selects(
counter.statements, ServerDefaultPk.ormar_config.tablename
)
assert instance.pk is not None
assert selects == [], counter.statements
@pytest.mark.asyncio
async def test_save_still_reloads_when_non_pk_has_server_default():
"""Regression guard: non-pk server defaults must still trigger a reload."""
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
with _StatementCounter() as counter:
instance = ServerDefaultNonPk(id=1, name="first")
await instance.save()
selects = _table_selects(
counter.statements, ServerDefaultNonPk.ormar_config.tablename
)
assert instance.company == "Acme"
assert len(selects) == 1, counter.statements
@pytest.mark.asyncio
@pytest.mark.skipif(
_IS_MYSQL,
reason=(
"Oracle MySQL has no RETURNING clause, so a server_default on a "
"non-AUTO_INCREMENT pk cannot be recovered — covered by "
"test_save_raises_when_server_default_pk_cannot_be_recovered instead."
),
)
async def test_save_reloads_once_when_both_pk_and_non_pk_have_server_default(): # noqa: E501 # pragma: no cover
"""Mixed case: still need exactly one reload for the non-pk column."""
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
with _StatementCounter() as counter:
instance = ServerDefaultPkAndNonPk(name="first")
await instance.save()
selects = _table_selects(
counter.statements, ServerDefaultPkAndNonPk.ormar_config.tablename
)
assert instance.pk is not None
assert instance.company == "Acme"
assert len(selects) == 1, counter.statements
@pytest.mark.asyncio
@pytest.mark.skipif(
not _IS_MYSQL,
reason=(
"This loud-fail path only fires on backends that cannot return a "
"server-generated pk. RETURNING-capable backends (PostgreSQL, "
"SQLite 3.35+, MariaDB 10.5+) succeed here."
),
)
async def test_save_raises_when_server_default_pk_cannot_be_recovered(): # noqa: E501 # pragma: no cover
"""Oracle MySQL: no RETURNING → save() must raise, not silently store a
bogus pk (the old bug was to coerce rowcount into ``Model.pk``)."""
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
instance = ServerDefaultPk(name="first")
with pytest.raises(ModelPersistenceError, match="primary key"):
await instance.save()
assert instance.pk is None
ormar-orm-ormar-a7e7fde/tests/test_model_methods/test_update.py 0000664 0000000 0000000 00000005721 15211567312 0025256 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Director(ormar.Model):
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")
last_name: str = ormar.String(max_length=100, nullable=False, name="last_name")
class Movie(ormar.Model):
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")
year: int = ormar.Integer()
profit: float = ormar.Float()
director: Optional[Director] = ormar.ForeignKey(Director)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_updating_selected_columns():
async with base_ormar_config.database:
director1 = await Director(name="Peter", last_name="Jackson").save()
director2 = await Director(name="James", last_name="Cameron").save()
lotr = await Movie(
name="LOTR", year=2001, director=director1, profit=1.140
).save()
lotr.name = "Lord of The Rings"
lotr.year = 2003
lotr.profit = 1.212
await lotr.update(_columns=["name"])
# before reload the field has current value even if not saved
assert lotr.year == 2003
lotr = await Movie.objects.get()
assert lotr.name == "Lord of The Rings"
assert lotr.year == 2001
assert round(lotr.profit, 3) == 1.140
assert lotr.director.pk == director1.pk
lotr.year = 2003
lotr.profit = 1.212
lotr.director = director2
await lotr.update(_columns=["year", "profit"])
lotr = await Movie.objects.get()
assert lotr.year == 2003
assert round(lotr.profit, 3) == 1.212
assert lotr.director.pk == director1.pk
@pytest.mark.asyncio
async def test_not_passing_columns_or_empty_list_saves_all():
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
).save()
terminator.name = "Terminator 2"
terminator.year = 1991
terminator.profit = 0.520
await terminator.update(_columns=[])
terminator = await Movie.objects.get()
assert terminator.name == "Terminator 2"
assert terminator.year == 1991
assert round(terminator.profit, 3) == 0.520
terminator.name = "Terminator 3"
terminator.year = 2003
terminator.profit = 0.433
await terminator.update()
terminator = await terminator.load()
assert terminator.name == "Terminator 3"
assert terminator.year == 2003
assert round(terminator.profit, 3) == 0.433
ormar-orm-ormar-a7e7fde/tests/test_model_methods/test_upsert.py 0000664 0000000 0000000 00000003007 15211567312 0025311 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Director(ormar.Model):
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")
last_name: str = ormar.String(max_length=100, nullable=False, name="last_name")
class Movie(ormar.Model):
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")
year: int = ormar.Integer()
profit: float = ormar.Float()
director: Optional[Director] = ormar.ForeignKey(Director)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_updating_selected_columns():
async with base_ormar_config.database:
director1 = await Director(name="Peter", last_name="Jackson").save()
await Movie(
id=1, name="Lord of The Rings", year=2003, director=director1, profit=1.212
).upsert()
with pytest.raises(ormar.NoMatch):
await Movie.objects.get()
await Movie(
id=1, name="Lord of The Rings", year=2003, director=director1, profit=1.212
).upsert(__force_save__=True)
lotr = await Movie.objects.get()
assert lotr.year == 2003
assert lotr.name == "Lord of The Rings"
ormar-orm-ormar-a7e7fde/tests/test_ordering/ 0000775 0000000 0000000 00000000000 15211567312 0021344 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_ordering/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0023443 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_ordering/test_default_model_order.py 0000664 0000000 0000000 00000006266 15211567312 0026766 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import pytest_asyncio
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
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):
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)
title: str = ormar.String(max_length=100)
year: Optional[int] = ormar.Integer(nullable=True)
ranking: Optional[int] = ormar.Integer(nullable=True)
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(autouse=True, scope="function")
async def cleanup():
yield
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 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()
lewis = await Author(name="C.S Lewis").save()
authors = await Author.objects.all()
assert authors[0] == king
assert authors[1] == tolkien
assert authors[2] == lewis
assert authors[3] == sapkowski
authors = await Author.objects.order_by("name").all()
assert authors[3] == king
assert authors[2] == tolkien
assert authors[1] == lewis
assert authors[0] == sapkowski
@pytest.mark.asyncio
async def test_default_orders_is_applied_on_related():
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
).save()
lotr = await Book(
author=tolkien, title="The Lord of the Rings", year=1955
).save()
hobbit = await Book(author=tolkien, title="The Hobbit", year=1933).save()
await tolkien.books.all()
assert tolkien.books[0] == hobbit
assert tolkien.books[1] == lotr
assert tolkien.books[2] == silmarillion
await tolkien.books.order_by("-title").all()
assert tolkien.books[2] == hobbit
assert tolkien.books[1] == lotr
assert tolkien.books[0] == silmarillion
@pytest.mark.asyncio
async def test_default_orders_is_applied_on_related_two_fields():
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
).save()
bret = await Author(name="Peter V. Bret").save()
tds = await Book(
author=bret, title="The Desert Spear", year=2010, ranking=9
).save()
books = await Book.objects.all()
assert books[0] == twok
assert books[1] == tds
ormar-orm-ormar-a7e7fde/tests/test_ordering/test_default_relation_order.py 0000664 0000000 0000000 00000010306 15211567312 0027471 0 ustar 00root root 0000000 0000000 from typing import Optional
from uuid import UUID, uuid4
import pytest
import pytest_asyncio
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(
Author, orders_by=["name"], related_orders_by=["-year"]
)
title: str = ormar.String(max_length=100)
year: Optional[int] = ormar.Integer(nullable=True)
ranking: Optional[int] = ormar.Integer(nullable=True)
class Animal(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="animals")
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.String(max_length=200)
specie: str = ormar.String(max_length=200)
class Human(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="humans")
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.Text(default="")
pets: list[Animal] = ormar.ManyToMany(
Animal,
related_name="care_takers",
orders_by=["specie", "-name"],
related_orders_by=["name"],
)
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(autouse=True, scope="function")
async def cleanup():
yield
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 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(
author=tolkien, title="The Silmarillion", year=1977
).save()
lotr = await Book(
author=tolkien, title="The Lord of the Rings", year=1955
).save()
tolkien = await Author.objects.select_related("books").get()
assert tolkien.books[2] == hobbit
assert tolkien.books[1] == lotr
assert tolkien.books[0] == silmarillion
@pytest.mark.asyncio
async def test_default_orders_is_applied_from_relation():
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
).save()
sanders = await Author(name="Brandon Sanderson").save()
twok = await Book(
author=sanders, title="The Way of Kings", year=2010, ranking=10
).save()
books = await Book.objects.order_by("year").select_related("author").all()
assert books[0] == twok
assert books[1] == tds
@pytest.mark.asyncio
async def test_default_orders_is_applied_from_relation_on_m2m():
async with base_ormar_config.database:
alice = await Human(name="Alice").save()
spot = await Animal(name="Spot", specie="Cat").save()
zkitty = await Animal(name="ZKitty", specie="Cat").save()
noodle = await Animal(name="Noodle", specie="Anaconda").save()
await alice.pets.add(noodle)
await alice.pets.add(spot)
await alice.pets.add(zkitty)
await alice.load_all()
assert alice.pets[0] == noodle
assert alice.pets[1] == zkitty
assert alice.pets[2] == spot
@pytest.mark.asyncio
async def test_default_orders_is_applied_from_reverse_relation_on_m2m():
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()
julia = await Human(name="Julia").save()
await max.care_takers.add(joe)
await max.care_takers.add(zack)
await max.care_takers.add(julia)
await max.load_all()
assert max.care_takers[0] == joe
assert max.care_takers[1] == julia
assert max.care_takers[2] == zack
ormar-orm-ormar-a7e7fde/tests/test_ordering/test_default_through_relation_order.py 0000664 0000000 0000000 00000025465 15211567312 0031245 0 ustar 00root root 0000000 0000000 from typing import Any, Optional, cast
from uuid import UUID, uuid4
import pytest
import ormar
from ormar import (
Model,
ModelDefinitionError,
QuerySet,
pre_relation_remove,
pre_save,
pre_update,
)
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Animal(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="animals")
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.Text(default="")
# favoriteHumans
class Link(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="link_table")
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
animal_order: Optional[int] = ormar.Integer(nullable=True)
human_order: Optional[int] = ormar.Integer(nullable=True)
class Human(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="humans")
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.Text(default="")
favoriteAnimals: list[Animal] = ormar.ManyToMany(
Animal,
through=Link,
related_name="favoriteHumans",
orders_by=["link__animal_order"],
related_orders_by=["link__human_order"],
)
class Human2(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="humans2")
id: UUID = ormar.UUID(primary_key=True, default=uuid4)
name: str = ormar.Text(default="")
favoriteAnimals: list[Animal] = ormar.ManyToMany(
Animal, related_name="favoriteHumans2", orders_by=["link__animal_order__fail"]
)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_ordering_by_through_fail():
async with base_ormar_config.database:
alice = await Human2(name="Alice").save()
spot = await Animal(name="Spot").save()
await alice.favoriteAnimals.add(spot)
with pytest.raises(ModelDefinitionError):
await alice.load_all()
def _get_filtered_query(
sender: type[Model], instance: Model, to_class: type[Model]
) -> QuerySet:
"""
Helper function.
Gets the query filtered by the appropriate class name.
"""
pk = getattr(instance, f"{to_class.get_name()}").pk
filter_kwargs = {f"{to_class.get_name()}": pk}
query = sender.objects.filter(**filter_kwargs)
return query
def _get_through_model_relations(
sender: type[Model], instance: Model
) -> tuple[type[Model], type[Model]]:
relations = list(instance.extract_related_names())
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
async def _populate_order_on_insert(
sender: type[Model], instance: Model, from_class: type[Model], to_class: type[Model]
):
"""
Helper function.
Get max values from database for both orders and adds 1 (0 if max is None) if the
order is not provided. If the order is provided it reorders the existing links
to match the newly defined order.
Assumes names f"{model.get_name()}_order" like for Animal: animal_order.
"""
order_column = f"{from_class.get_name()}_order"
if getattr(instance, order_column) is None:
query = _get_filtered_query(sender, instance, to_class)
max_order = await query.max(order_column)
max_order = max_order + 1 if max_order is not None else 0
setattr(instance, order_column, max_order)
else:
await _reorder_on_update(
sender=sender,
instance=instance,
from_class=from_class,
to_class=to_class,
passed_args={order_column: getattr(instance, order_column)},
)
async def _reorder_on_update(
sender: type[Model],
instance: Model,
from_class: type[Model],
to_class: type[Model],
passed_args: dict,
):
"""
Helper function.
Actually reorders links by given order passed in add/update query to the link
model.
Assumes names f"{model.get_name()}_order" like for Animal: animal_order.
"""
order = f"{from_class.get_name()}_order"
if order in passed_args:
query = _get_filtered_query(sender, instance, to_class)
to_reorder = await query.exclude(pk=instance.pk).order_by(order).all()
new_order = passed_args.get(order)
if to_reorder and new_order is not None:
# can be more efficient - here we renumber all even if not needed.
for ind, link in enumerate(to_reorder):
if ind < new_order:
setattr(link, order, ind)
else:
setattr(link, order, ind + 1)
await sender.objects.bulk_update(
cast(list[Model], to_reorder), columns=[order]
)
@pre_save(Link)
async def order_link_on_insert(sender: type[Model], instance: Model, **kwargs: Any):
"""
Signal receiver registered on Link model, triggered every time before one is created
by calling save() on a model. Note that signal functions for pre_save signal accepts
sender class, instance and have to accept **kwargs even if it's empty as of now.
"""
rel_one, rel_two = _get_through_model_relations(sender, instance)
await _populate_order_on_insert(
sender=sender, instance=instance, from_class=rel_one, to_class=rel_two
)
await _populate_order_on_insert(
sender=sender, instance=instance, from_class=rel_two, to_class=rel_one
)
@pre_update(Link)
async def reorder_links_on_update(
sender: type[ormar.Model], instance: ormar.Model, passed_args: dict, **kwargs: Any
):
"""
Signal receiver registered on Link model, triggered every time before one is updated
by calling update() on a model. Note that signal functions for pre_update signal
accepts sender class, instance, passed_args which is a dict of kwargs passed to
update and have to accept **kwargs even if it's empty as of now.
"""
rel_one, rel_two = _get_through_model_relations(sender, instance)
await _reorder_on_update(
sender=sender,
instance=instance,
from_class=rel_one,
to_class=rel_two,
passed_args=passed_args,
)
await _reorder_on_update(
sender=sender,
instance=instance,
from_class=rel_two,
to_class=rel_one,
passed_args=passed_args,
)
@pre_relation_remove([Animal, Human])
async def reorder_links_on_remove(
sender: type[ormar.Model],
instance: ormar.Model,
child: ormar.Model,
relation_name: str,
**kwargs: Any,
):
"""
Signal receiver registered on Anima and Human models, triggered every time before
relation on a model is removed. Note that signal functions for pre_relation_remove
signal accepts sender class, instance, child, relation_name and have to accept
**kwargs even if it's empty as of now.
Note that if classes have many relations you need to check if current one is ordered
"""
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
child_pk = child.pk
filter_kwargs = {f"{sender.get_name()}": parent_pk, child.get_name(): child_pk}
through_instance = await through_class.objects.get(**filter_kwargs)
rel_one, rel_two = _get_through_model_relations(through_class, through_instance)
await _reorder_on_update(
sender=through_class,
instance=through_instance,
from_class=rel_one,
to_class=rel_two,
passed_args={f"{rel_one.get_name()}_order": 999999},
)
await _reorder_on_update(
sender=through_class,
instance=through_instance,
from_class=rel_two,
to_class=rel_one,
passed_args={f"{rel_two.get_name()}_order": 999999},
)
@pytest.mark.asyncio
async def test_ordering_by_through_on_m2m_field():
async with base_ormar_config.database:
def verify_order(instance, expected):
field_name = (
"favoriteAnimals" if isinstance(instance, Human) else "favoriteHumans"
)
order_field_name = (
"animal_order" if isinstance(instance, Human) else "human_order"
)
assert [x.name for x in getattr(instance, field_name)] == expected
assert [
getattr(x.link, order_field_name) for x in getattr(instance, field_name)
] == [i for i in range(len(expected))]
alice = await Human(name="Alice").save()
bob = await Human(name="Bob").save()
charlie = await Human(name="Charlie").save()
spot = await Animal(name="Spot").save()
kitty = await Animal(name="Kitty").save()
noodle = await Animal(name="Noodle").save()
await alice.favoriteAnimals.add(noodle)
await alice.favoriteAnimals.add(spot)
await alice.favoriteAnimals.add(kitty)
await alice.load_all()
verify_order(alice, ["Noodle", "Spot", "Kitty"])
await bob.favoriteAnimals.add(noodle)
await bob.favoriteAnimals.add(kitty)
await bob.favoriteAnimals.add(spot)
await bob.load_all()
verify_order(bob, ["Noodle", "Kitty", "Spot"])
await charlie.favoriteAnimals.add(kitty)
await charlie.favoriteAnimals.add(noodle)
await charlie.favoriteAnimals.add(spot)
await charlie.load_all()
verify_order(charlie, ["Kitty", "Noodle", "Spot"])
animals = [noodle, kitty, spot]
for animal in animals:
await animal.load_all()
verify_order(animal, ["Alice", "Bob", "Charlie"])
zack = await Human(name="Zack").save()
await noodle.favoriteHumans.add(zack, human_order=0)
await noodle.load_all()
verify_order(noodle, ["Zack", "Alice", "Bob", "Charlie"])
await zack.load_all()
verify_order(zack, ["Noodle"])
await noodle.favoriteHumans.filter(name="Zack").update(link=dict(human_order=1))
await noodle.load_all()
verify_order(noodle, ["Alice", "Zack", "Bob", "Charlie"])
await noodle.favoriteHumans.filter(name="Zack").update(link=dict(human_order=2))
await noodle.load_all()
verify_order(noodle, ["Alice", "Bob", "Zack", "Charlie"])
await noodle.favoriteHumans.filter(name="Zack").update(link=dict(human_order=3))
await noodle.load_all()
verify_order(noodle, ["Alice", "Bob", "Charlie", "Zack"])
await kitty.favoriteHumans.remove(bob)
await kitty.load_all()
assert [x.name for x in kitty.favoriteHumans] == ["Alice", "Charlie"]
bob = await noodle.favoriteHumans.get(pk=bob.pk)
assert bob.link.human_order == 1
await noodle.favoriteHumans.remove(
await noodle.favoriteHumans.filter(link__human_order=2).get()
)
await noodle.load_all()
verify_order(noodle, ["Alice", "Bob", "Zack"])
ormar-orm-ormar-a7e7fde/tests/test_ordering/test_proper_order_of_sorting_apply.py 0000664 0000000 0000000 00000003777 15211567312 0031133 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import pytest_asyncio
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="books", order_by=["-ranking"])
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(
Author, orders_by=["name"], related_orders_by=["-year"]
)
title: str = ormar.String(max_length=100)
year: Optional[int] = ormar.Integer(nullable=True)
ranking: Optional[int] = ormar.Integer(nullable=True)
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(autouse=True, scope="function")
async def cleanup():
yield
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 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(
author=tolkien, title="The Silmarillion", year=1977
).save()
lotr = await Book(
author=tolkien, title="The Lord of the Rings", year=1955
).save()
tolkien = await Author.objects.select_related("books").get()
assert tolkien.books[2] == hobbit
assert tolkien.books[1] == lotr
assert tolkien.books[0] == silmarillion
tolkien = (
await Author.objects.select_related("books").order_by("books__title").get()
)
assert tolkien.books[0] == hobbit
assert tolkien.books[1] == lotr
assert tolkien.books[2] == silmarillion
ormar-orm-ormar-a7e7fde/tests/test_queries/ 0000775 0000000 0000000 00000000000 15211567312 0021210 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_queries/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0023307 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_queries/test_adding_related.py 0000664 0000000 0000000 00000002114 15211567312 0025545 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Department(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Course(ormar.Model):
ormar_config = base_ormar_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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_adding_relation_to_reverse_saves_the_child():
async with base_ormar_config.database:
department = await Department(name="Science").save()
course = Course(name="Math", completed=False)
await department.courses.add(course)
assert course.pk is not None
assert course.department == department
assert department.courses[0] == course
ormar-orm-ormar-a7e7fde/tests/test_queries/test_aggr_functions.py 0000664 0000000 0000000 00000013233 15211567312 0025633 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import pytest_asyncio
import ormar
from ormar.exceptions import QueryDefinitionError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
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):
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)
title: str = ormar.String(max_length=100)
year: Optional[int] = ormar.Integer(nullable=True)
ranking: Optional[int] = ormar.Integer(nullable=True)
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(autouse=True, scope="function")
async def cleanup():
yield
async with base_ormar_config.database:
await Book.objects.delete(each=True)
await Author.objects.delete(each=True)
async def sample_data():
author = await Author(name="Author 1").save()
await Book(title="Book 1", year=1920, ranking=3, author=author).save()
await Book(title="Book 2", year=1930, ranking=1, author=author).save()
await Book(title="Book 3", year=1923, ranking=5, author=author).save()
@pytest.mark.asyncio
async def test_min_method():
async with base_ormar_config.database:
await sample_data()
assert await Book.objects.min("year") == 1920
result = await Book.objects.min(["year", "ranking"])
assert result == dict(year=1920, ranking=1)
assert await Book.objects.min("title") == "Book 1"
assert await Author.objects.select_related("books").min("books__year") == 1920
result = await Author.objects.select_related("books").min(
["books__year", "books__ranking"]
)
assert result == dict(books__year=1920, books__ranking=1)
assert (
await Author.objects.select_related("books")
.filter(books__year__gt=1925)
.min("books__year")
== 1930
)
@pytest.mark.asyncio
async def test_max_method():
async with base_ormar_config.database:
await sample_data()
assert await Book.objects.max("year") == 1930
result = await Book.objects.max(["year", "ranking"])
assert result == dict(year=1930, ranking=5)
assert await Book.objects.max("title") == "Book 3"
assert await Author.objects.select_related("books").max("books__year") == 1930
result = await Author.objects.select_related("books").max(
["books__year", "books__ranking"]
)
assert result == dict(books__year=1930, books__ranking=5)
assert (
await Author.objects.select_related("books")
.filter(books__year__lt=1925)
.max("books__year")
== 1923
)
@pytest.mark.asyncio
async def test_sum_method():
async with base_ormar_config.database:
await sample_data()
assert await Book.objects.sum("year") == 5773
result = await Book.objects.sum(["year", "ranking"])
assert result == dict(year=5773, ranking=9)
with pytest.raises(QueryDefinitionError):
await Book.objects.sum("title")
assert await Author.objects.select_related("books").sum("books__year") == 5773
result = await Author.objects.select_related("books").sum(
["books__year", "books__ranking"]
)
assert result == dict(books__year=5773, books__ranking=9)
assert (
await Author.objects.select_related("books")
.filter(books__year__lt=1925)
.sum("books__year")
== 3843
)
@pytest.mark.asyncio
async def test_avg_method():
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"])
assert round(float(result.get("year")), 2) == 1924.33
assert result.get("ranking") == 3.0
with pytest.raises(QueryDefinitionError):
await Book.objects.avg("title")
result = await Author.objects.select_related("books").avg("books__year")
assert round(float(result), 2) == 1924.33
result = await Author.objects.select_related("books").avg(
["books__year", "books__ranking"]
)
assert round(float(result.get("books__year")), 2) == 1924.33
assert result.get("books__ranking") == 3.0
assert (
await Author.objects.select_related("books")
.filter(books__year__lt=1925)
.avg("books__year")
== 1921.5
)
@pytest.mark.asyncio
async def test_queryset_method():
async with base_ormar_config.database:
await sample_data()
author = await Author.objects.select_related("books").get()
assert await author.books.min("year") == 1920
assert await author.books.max("year") == 1930
assert await author.books.sum("ranking") == 9
assert await author.books.avg("ranking") == 3.0
assert await author.books.max(["year", "title"]) == dict(
year=1930, title="Book 3"
)
@pytest.mark.asyncio
async def test_count_method():
async with base_ormar_config.database:
await sample_data()
count = await Author.objects.select_related("books").count()
assert count == 1
# The legacy functionality
count = await Author.objects.select_related("books").count(distinct=False)
assert count == 3
ormar-orm-ormar-a7e7fde/tests/test_queries/test_deep_relations_select_all.py 0000664 0000000 0000000 00000010400 15211567312 0030000 0 ustar 00root root 0000000 0000000 import pytest
from sqlalchemy import func
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Chart(ormar.Model):
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)
query_text = ormar.Text()
datasets = ormar.JSON()
layout = ormar.JSON()
data_config = ormar.JSON()
created_date = ormar.DateTime(server_default=func.now())
library = ormar.String(max_length=200, default="plotly")
used_filters = ormar.JSON()
class Report(ormar.Model):
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)
filters_position = ormar.String(max_length=200)
created_date = ormar.DateTime(server_default=func.now())
class Language(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="languages")
language_id = ormar.Integer(primary_key=True, autoincrement=True)
code = ormar.String(max_length=5)
name = ormar.String(max_length=200)
class TranslationNode(ormar.Model):
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):
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")
language = ormar.ForeignKey(Language, name="language_id")
value = ormar.String(max_length=500)
class Filter(ormar.Model):
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)
label = ormar.String(max_length=200)
query_text = ormar.Text()
allow_multiselect = ormar.Boolean(default=True)
created_date = ormar.DateTime(server_default=func.now())
is_dynamic = ormar.Boolean(default=True)
is_date = ormar.Boolean(default=False)
translation = ormar.ForeignKey(TranslationNode, name="translation_node_id")
class FilterValue(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="filter_values")
value_id = ormar.Integer(primary_key=True, autoincrement=True)
value = ormar.String(max_length=300)
label = ormar.String(max_length=300)
filter = ormar.ForeignKey(Filter, name="filter_id", related_name="values")
translation = ormar.ForeignKey(TranslationNode, name="translation_node_id")
class FilterXReport(ormar.Model):
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")
report = ormar.ForeignKey(Report, name="report_id", related_name="filters")
sort_order = ormar.Integer()
default_value = ormar.Text()
is_visible = ormar.Boolean()
class ChartXReport(ormar.Model):
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")
report = ormar.ForeignKey(Report, name="report_id", related_name="charts")
sort_order = ormar.Integer()
width = ormar.Integer()
class ChartColumn(ormar.Model):
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")
column_name = ormar.String(max_length=200)
column_type = ormar.String(max_length=200)
translation = ormar.ForeignKey(TranslationNode, name="translation_node_id")
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_saving_related_fk_rel():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await Report.objects.select_all(follow=True).all()
ormar-orm-ormar-a7e7fde/tests/test_queries/test_filter_groups.py 0000664 0000000 0000000 00000011070 15211567312 0025504 0 ustar 00root root 0000000 0000000 from typing import Optional
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
title: str = ormar.String(max_length=100)
year: Optional[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)
assert len(result.actions) == 2
assert result.actions[0].target_model == Author
assert result.actions[1].target_model == Book
assert (
str(result.get_text_clause().compile(compile_kwargs={"literal_binds": True}))
== f"(authors.name = 'aa' OR "
f"{result.actions[1].table_prefix}"
f"_books.title = 'bb')"
)
def test_and_group():
result = ormar.and_(name="aa", books__title="bb")
result.resolve(model_cls=Author)
assert len(result.actions) == 2
assert result.actions[0].target_model == Author
assert result.actions[1].target_model == Book
assert (
str(result.get_text_clause().compile(compile_kwargs={"literal_binds": True}))
== f"(authors.name = 'aa' AND "
f"{result.actions[1].table_prefix}"
f"_books.title = 'bb')"
)
def test_nested_and():
result = ormar.and_(
ormar.or_(name="aa", books__title="bb"), ormar.or_(name="cc", books__title="dd")
)
result.resolve(model_cls=Author)
assert len(result.actions) == 0
assert len(result._nested_groups) == 2
book_prefix = result._nested_groups[0].actions[1].table_prefix
assert (
str(result.get_text_clause().compile(compile_kwargs={"literal_binds": True}))
== f"((authors.name = 'aa' OR "
f"{book_prefix}"
f"_books.title = 'bb') AND "
f"(authors.name = 'cc' OR "
f"{book_prefix}"
f"_books.title = 'dd'))"
)
def test_nested_group_and_action():
result = ormar.and_(ormar.or_(name="aa", books__title="bb"), books__title="dd")
result.resolve(model_cls=Author)
assert len(result.actions) == 1
assert len(result._nested_groups) == 1
book_prefix = result._nested_groups[0].actions[1].table_prefix
assert (
str(result.get_text_clause().compile(compile_kwargs={"literal_binds": True}))
== f"((authors.name = 'aa' OR "
f"{book_prefix}"
f"_books.title = 'bb') AND "
f"{book_prefix}"
f"_books.title = 'dd')"
)
def test_deeply_nested_or():
result = ormar.or_(
ormar.and_(
ormar.or_(name="aa", books__title="bb"),
ormar.or_(name="cc", books__title="dd"),
),
ormar.and_(
ormar.or_(books__year__lt=1900, books__title="11"),
ormar.or_(books__year__gt="xx", books__title="22"),
),
)
result.resolve(model_cls=Author)
assert len(result.actions) == 0
assert len(result._nested_groups) == 2
assert len(result._nested_groups[0]._nested_groups) == 2
book_prefix = result._nested_groups[0]._nested_groups[0].actions[1].table_prefix
result_qry = str(
result.get_text_clause().compile(compile_kwargs={"literal_binds": True})
)
expected_qry = (
f"(((authors.name = 'aa' OR {book_prefix}_books.title = 'bb') AND "
f"(authors.name = 'cc' OR {book_prefix}_books.title = 'dd')) "
f"OR (({book_prefix}_books.year < 1900 OR {book_prefix}_books.title = '11') AND"
f" ({book_prefix}_books.year > 'xx' OR {book_prefix}_books.title = '22')))"
)
assert result_qry.replace("\n", "") == expected_qry.replace("\n", "")
def test_one_model_group():
result = ormar.and_(year__gt=1900, title="bb")
result.resolve(model_cls=Book)
assert len(result.actions) == 2
assert len(result._nested_groups) == 0
def test_one_model_nested_group():
result = ormar.and_(
ormar.or_(year__gt=1900, title="bb"), ormar.or_(year__lt=1800, title="aa")
)
result.resolve(model_cls=Book)
assert len(result.actions) == 0
assert len(result._nested_groups) == 2
def test_one_model_with_group():
result = ormar.or_(ormar.and_(year__gt=1900, title="bb"), title="uu")
result.resolve(model_cls=Book)
assert len(result.actions) == 1
assert len(result._nested_groups) == 1
ormar-orm-ormar-a7e7fde/tests/test_queries/test_get_or_create_concurrency.py 0000664 0000000 0000000 00000012124 15211567312 0030035 0 ustar 00root root 0000000 0000000 """Concurrent ``get_or_create`` race-recovery tests for issue #1016.
These tests run under AUTOCOMMIT (no surrounding transaction) so they
exercise the real-world scenario: separate coroutines, each pulling its
own connection from the pool, racing to insert a row.
The project-wide ``force_rollback=True`` config can't be used here:
PostgreSQL aborts the surrounding transaction on the first failed
statement, which makes the recovery ``get`` raise
``InFailedSQLTransactionError`` instead of returning the winner's row.
"""
import asyncio
from typing import Optional
import pytest
import sqlalchemy.exc
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
race_config = create_config()
class RaceLibrary(ormar.Model):
ormar_config = race_config.copy(tablename="race_libraries")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class RaceBook(ormar.Model):
ormar_config = race_config.copy(tablename="race_unique_books")
id: int = ormar.Integer(primary_key=True)
isbn: str = ormar.String(max_length=20, unique=True)
title: str = ormar.String(max_length=200)
library: Optional[RaceLibrary] = ormar.ForeignKey(
RaceLibrary, nullable=True, related_name="books"
)
create_test_database = init_tests(race_config)
async def _cleanup() -> None:
await RaceBook.objects.delete(each=True)
await RaceLibrary.objects.delete(each=True)
@pytest.mark.asyncio
async def test_get_or_create_recovers_from_concurrent_create():
"""Two concurrent ``get_or_create`` calls race for real: both
``SELECT``s return empty, then one ``INSERT`` wins and the other
raises ``IntegrityError``. The losing call must catch the error,
retry the ``get``, and return the winner's row with ``created=False``
rather than leaking the driver error.
Both calls returning the same pk with exactly one ``created=True``
proves the recovery path actually fired — if the IntegrityError had
propagated, ``asyncio.gather`` would have re-raised it.
"""
async with race_config.database:
try:
results = await asyncio.gather(
RaceBook.objects.get_or_create(isbn="978-0-00-000000-1", title="Race"),
RaceBook.objects.get_or_create(isbn="978-0-00-000000-1", title="Race"),
)
(book1, c1), (book2, c2) = results
assert book1.pk == book2.pk
assert book1.isbn == "978-0-00-000000-1"
assert sorted([c1, c2]) == [False, True]
assert await RaceBook.objects.count() == 1
finally:
await _cleanup()
@pytest.mark.asyncio
async def test_get_or_create_propagates_unrelated_integrity_error():
"""If ``create`` fails with ``IntegrityError`` and the retry ``get``
still finds nothing matching the caller's filter, the violation
isn't a race — it's a legitimate constraint conflict (e.g. a unique
column the caller isn't filtering by). The original ``IntegrityError``
must propagate instead of being swallowed.
"""
async with race_config.database:
try:
await RaceBook.objects.create(isbn="978-0-00-000000-2", title="Original")
with pytest.raises(sqlalchemy.exc.IntegrityError):
await RaceBook.objects.get_or_create(
isbn="978-0-00-000000-2", title="Different"
)
finally:
await _cleanup()
@pytest.mark.asyncio
async def test_proxy_get_or_create_recovers_from_concurrent_create():
"""Same real-concurrency race but via the m2m / reverse-fk proxy
``QuerysetProxy.get_or_create``, which has its own try/except around
the get/create pair.
"""
async with race_config.database:
try:
library = await RaceLibrary.objects.create(name="Main")
results = await asyncio.gather(
library.books.get_or_create(
isbn="978-0-00-000000-3", title="ProxyRace"
),
library.books.get_or_create(
isbn="978-0-00-000000-3", title="ProxyRace"
),
)
(book1, c1), (book2, c2) = results
assert book1.pk == book2.pk
assert sorted([c1, c2]) == [False, True]
assert await RaceBook.objects.count() == 1
finally:
await _cleanup()
@pytest.mark.asyncio
async def test_proxy_get_or_create_propagates_unrelated_integrity_error():
"""``QuerysetProxy.get_or_create`` re-raises the original
``IntegrityError`` when the retry ``get`` still doesn't match —
same semantics as ``QuerySet.get_or_create``.
"""
async with race_config.database:
try:
library = await RaceLibrary.objects.create(name="Main")
await RaceBook.objects.create(
isbn="978-0-00-000000-4", title="Original", library=library
)
with pytest.raises(sqlalchemy.exc.IntegrityError):
await library.books.get_or_create(
isbn="978-0-00-000000-4", title="Different"
)
finally:
await _cleanup()
ormar-orm-ormar-a7e7fde/tests/test_queries/test_indirect_relations_to_self.py 0000664 0000000 0000000 00000003321 15211567312 0030214 0 ustar 00root root 0000000 0000000 from datetime import datetime
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Node(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="node")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=120)
type: str = ormar.String(max_length=12, default="FLOW")
created_at: datetime = ormar.DateTime(timezone=True, default=datetime.now)
class Edge(ormar.Model):
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")
dst_node: Node = ormar.ForeignKey(Node, related_name="previous_edges")
order: int = ormar.Integer(default=1)
created_at: datetime = ormar.DateTime(timezone=True, default=datetime.now)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_sort_order_on_main_model():
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()
await Edge(id="Side 1", src_node=node1, dst_node=node2).save()
await Edge(id="Side 2", src_node=node2, dst_node=node3, order=2).save()
await Edge(id="Side 3", src_node=node3, dst_node=node1, order=3).save()
active_nodes = await Node.objects.select_related(
["next_edges", "next_edges__dst_node"]
).all()
assert len(active_nodes) == 3
assert active_nodes[0].next_edges[0].id == "Side 1"
assert active_nodes[0].next_edges[0].dst_node.type == "FLOW"
ormar-orm-ormar-a7e7fde/tests/test_queries/test_isnull_filter.py 0000664 0000000 0000000 00000006001 15211567312 0025471 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
title: str = ormar.String(max_length=100)
year: Optional[int] = ormar.Integer(nullable=True)
class JsonModel(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="jsons")
id = ormar.Integer(primary_key=True)
text_field = ormar.Text(nullable=True)
json_field = ormar.JSON(nullable=True)
json_not_null = ormar.JSON()
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_is_null():
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(
author=tolkien, title="The Lord of the Rings", year=1955
)
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
books = await Book.objects.all(year__isnull=True)
assert len(books) == 1
assert books[0].year is None
assert books[0].title == "The Hobbit"
books = await Book.objects.all(year__isnull=False)
assert len(books) == 2
tolkien = await Author.objects.select_related("books").get(
books__year__isnull=True
)
assert len(tolkien.books) == 1
assert tolkien.books[0].year is None
assert tolkien.books[0].title == "The Hobbit"
tolkien = (
await Author.objects.select_related("books")
.paginate(1, 10)
.get(books__year__isnull=True)
)
assert len(tolkien.books) == 1
assert tolkien.books[0].year is None
assert tolkien.books[0].title == "The Hobbit"
tolkien = await Author.objects.select_related("books").get(
books__year__isnull=False
)
assert len(tolkien.books) == 2
assert tolkien.books[0].year == 1955
assert tolkien.books[0].title == "The Lord of the Rings"
@pytest.mark.asyncio
async def test_isnull_json():
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)
assert len(non_null_text_fields) == 0
non_null_json_fields = await JsonModel.objects.all(json_field__isnull=False)
assert len(non_null_json_fields) == 0
non_null_json_fields = await JsonModel.objects.all(json_not_null__isnull=False)
assert len(non_null_json_fields) == 1
ormar-orm-ormar-a7e7fde/tests/test_queries/test_multi_fk_to_same_target_filter.py 0000664 0000000 0000000 00000005644 15211567312 0031066 0 ustar 00root root 0000000 0000000 import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Switch(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="switch")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Port(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="port")
id: int = ormar.Integer(primary_key=True)
switch: Switch = ormar.ForeignKey(Switch)
class Link(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="link")
id: int = ormar.Integer(primary_key=True)
a_port: Port = ormar.ForeignKey(Port, related_name="a_links")
z_port: Port = ormar.ForeignKey(Port, related_name="z_links")
create_test_database = init_tests(base_ormar_config)
def from_clause(expression) -> str:
"""Extracts the FROM ... (up to WHERE) part of the compiled SQL."""
sql = str(expression.compile(compile_kwargs={"literal_binds": True}))
return sql.split("FROM", 1)[1].split("WHERE")[0]
def has_cartesian_product(queryset) -> bool:
"""A comma in the FROM clause means an unjoined (cross-product) table."""
return "," in from_clause(queryset.build_select_expression())
@pytest.mark.asyncio
async def test_filter_on_two_fks_to_same_target_has_no_cartesian_product():
async with base_ormar_config.database:
queryset = Link.objects.filter(
a_port__switch__name__in=["a0", "a1"],
z_port__switch__name__in=["z0", "z1"],
)
assert not has_cartesian_product(queryset)
@pytest.mark.asyncio
async def test_repeated_query_not_corrupted_by_alias_manager_state():
async with base_ormar_config.database:
# Build the two-sided filter first; in the buggy version this pollutes
# the global alias manager and corrupts the next, simpler query.
Link.objects.filter(
a_port__switch__name__in=["a0", "a1"],
z_port__switch__name__in=["z0", "z1"],
).build_select_expression()
queryset = Link.objects.filter(
a_port__switch__in=[1, 2],
z_port__switch__name__in=["z0", "z1"],
)
assert not has_cartesian_product(queryset)
@pytest.mark.asyncio
async def test_each_filter_targets_its_own_branch():
async with base_ormar_config.database:
switch_a = await Switch(name="switch_a").save()
switch_z = await Switch(name="switch_z").save()
port_a = await Port(switch=switch_a).save()
port_z = await Port(switch=switch_z).save()
other = await Port(switch=switch_z).save()
wanted = await Link(a_port=port_a, z_port=port_z).save()
await Link(a_port=other, z_port=port_z).save()
result = await Link.objects.filter(
a_port__switch__name="switch_a",
z_port__switch__name="switch_z",
).all()
assert len(result) == 1
assert result[0].id == wanted.id
ormar-orm-ormar-a7e7fde/tests/test_queries/test_nested_reverse_relations.py 0000664 0000000 0000000 00000005510 15211567312 0027717 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class DataSource(ormar.Model):
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):
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)
source: Optional[DataSource] = ormar.ForeignKey(
DataSource, name="source_id", related_name="tables", ondelete="CASCADE"
)
class DataSourceTableColumn(ormar.Model):
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)
data_type: str = ormar.String(max_length=200)
table: Optional[DataSourceTable] = ormar.ForeignKey(
DataSourceTable, name="table_id", related_name="columns", ondelete="CASCADE"
)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_double_nested_reverse_relation():
async with base_ormar_config.database:
data_source = await DataSource(name="local").save()
test_tables = [
{
"name": "test1",
"columns": [
{"name": "col1", "data_type": "test"},
{"name": "col2", "data_type": "test2"},
{"name": "col3", "data_type": "test3"},
],
},
{
"name": "test2",
"columns": [
{"name": "col4", "data_type": "test"},
{"name": "col5", "data_type": "test2"},
{"name": "col6", "data_type": "test3"},
],
},
]
data_source.tables = test_tables
await data_source.save_related(save_all=True, follow=True)
tables = await DataSourceTable.objects.all()
assert len(tables) == 2
columns = await DataSourceTableColumn.objects.all()
assert len(columns) == 6
data_source = (
await DataSource.objects.select_related("tables__columns")
.filter(tables__name__in=["test1", "test2"], name="local")
.get()
)
assert len(data_source.tables) == 2
assert len(data_source.tables[0].columns) == 3
assert data_source.tables[0].columns[0].name == "col1"
assert data_source.tables[0].columns[2].name == "col3"
assert len(data_source.tables[1].columns) == 3
assert data_source.tables[1].columns[0].name == "col4"
assert data_source.tables[1].columns[2].name == "col6"
ormar-orm-ormar-a7e7fde/tests/test_queries/test_non_relation_fields_not_merged.py 0000664 0000000 0000000 00000002024 15211567312 0031037 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Chart(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
datasets = ormar.JSON()
class Config(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
chart: Optional[Chart] = ormar.ForeignKey(Chart)
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 base_ormar_config.database:
chart = await Chart.objects.create(datasets=[{"test": "ok"}])
await Config.objects.create(chart=chart)
await Config.objects.create(chart=chart)
chart2 = await Chart.objects.select_related("configs").get()
assert len(chart2.datasets) == 1
assert chart2.datasets == [{"test": "ok"}]
ormar-orm-ormar-a7e7fde/tests/test_queries/test_or_filters.py 0000664 0000000 0000000 00000020135 15211567312 0024772 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from ormar.exceptions import QueryDefinitionError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
title: str = ormar.String(max_length=100)
year: Optional[int] = ormar.Integer(nullable=True)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_or_filters():
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()
await Book(author=tolkien, title="The Silmarillion", year=1977).save()
sapkowski = await Author(name="Andrzej Sapkowski").save()
await Book(author=sapkowski, title="The Witcher", year=1990).save()
await Book(author=sapkowski, title="The Tower of Fools", year=2002).save()
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(author__name="J.R.R. Tolkien", year__gt=1970))
.all()
)
assert len(books) == 5
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(author__name="J.R.R. Tolkien", year__lt=1995))
.all()
)
assert len(books) == 4
assert not any([x.title == "The Tower of Fools" for x in books])
books = (
await Book.objects.select_related("author")
.filter((Book.author.name == "J.R.R. Tolkien") | (Book.year < 1995))
.all()
)
assert len(books) == 4
assert not any([x.title == "The Tower of Fools" for x in books])
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(year__gt=1960, year__lt=1940))
.filter(author__name="J.R.R. Tolkien")
.all()
)
assert len(books) == 2
assert books[0].title == "The Hobbit"
assert books[1].title == "The Silmarillion"
books = (
await Book.objects.select_related("author")
.filter(
ormar.and_(
ormar.or_(year__gt=1960, year__lt=1940),
author__name="J.R.R. Tolkien",
)
)
.all()
)
assert len(books) == 2
assert books[0].title == "The Hobbit"
assert books[1].title == "The Silmarillion"
books = (
await Book.objects.select_related("author")
.filter(
ormar.or_(
ormar.and_(year__gt=1960, author__name="J.R.R. Tolkien"),
ormar.and_(year__lt=2000, author__name="Andrzej Sapkowski"),
)
)
.filter(title__startswith="The")
.all()
)
assert len(books) == 2
assert books[0].title == "The Silmarillion"
assert books[1].title == "The Witcher"
books = (
await Book.objects.select_related("author")
.filter(
(
(
(Book.year > 1960) & (Book.author.name == "J.R.R. Tolkien")
| (
(Book.year < 2000)
& (Book.author.name == "Andrzej Sapkowski")
)
)
& (Book.title.startswith("The"))
)
)
.all()
)
assert len(books) == 2
assert books[0].title == "The Silmarillion"
assert books[1].title == "The Witcher"
books = (
await Book.objects.select_related("author")
.filter(
ormar.or_(
ormar.and_(
ormar.or_(year__gt=1960, year__lt=1940),
author__name="J.R.R. Tolkien",
),
ormar.and_(year__lt=2000, author__name="Andrzej Sapkowski"),
)
)
.all()
)
assert len(books) == 3
assert books[0].title == "The Hobbit"
assert books[1].title == "The Silmarillion"
assert books[2].title == "The Witcher"
books = (
await Book.objects.select_related("author")
.exclude(
ormar.or_(
ormar.and_(year__gt=1960, author__name="J.R.R. Tolkien"),
ormar.and_(year__lt=2000, author__name="Andrzej Sapkowski"),
)
)
.filter(title__startswith="The")
.all()
)
assert len(books) == 3
assert not any([x.title in ["The Silmarillion", "The Witcher"] for x in books])
books = (
await Book.objects.select_related("author")
.filter(
ormar.or_(
ormar.and_(year__gt=1960, author__name="J.R.R. Tolkien"),
ormar.and_(year__lt=2000, author__name="Andrzej Sapkowski"),
title__icontains="hobbit",
)
)
.filter(title__startswith="The")
.all()
)
assert len(books) == 3
assert not any(
[x.title in ["The Tower of Fools", "The Lord of the Rings"] for x in books]
)
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(year__gt=1980, year__lt=1910))
.filter(title__startswith="The")
.limit(1)
.all()
)
assert len(books) == 1
assert books[0].title == "The Witcher"
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(year__gt=1980, author__name="Andrzej Sapkowski"))
.filter(title__startswith="The")
.limit(1)
.all()
)
assert len(books) == 1
assert books[0].title == "The Witcher"
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(year__gt=1980, author__name="Andrzej Sapkowski"))
.filter(title__startswith="The")
.limit(1)
.offset(1)
.all()
)
assert len(books) == 1
assert books[0].title == "The Tower of Fools"
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(year__gt=1980, author__name="Andrzej Sapkowski"))
.filter(title__startswith="The")
.limit(1)
.offset(1)
.order_by("-id")
.all()
)
assert len(books) == 1
assert books[0].title == "The Witcher"
with pytest.raises(QueryDefinitionError):
await Book.objects.select_related("author").filter("wrong").all()
books = await tolkien.books.filter(
ormar.or_(year__lt=1940, year__gt=1960)
).all()
assert len(books) == 2
books = await tolkien.books.filter(
ormar.and_(
ormar.or_(year__lt=1940, year__gt=1960), title__icontains="hobbit"
)
).all()
assert len(books) == 1
assert tolkien.books[0].title == "The Hobbit"
books = (
await Book.objects.select_related("author")
.filter(ormar.or_(author__name="J.R.R. Tolkien"))
.all()
)
assert len(books) == 3
books = (
await Book.objects.select_related("author")
.filter(
ormar.or_(
ormar.and_(author__name__icontains="tolkien"),
ormar.and_(author__name__icontains="sapkowski"),
)
)
.all()
)
assert len(books) == 5
ormar-orm-ormar-a7e7fde/tests/test_queries/test_order_by.py 0000664 0000000 0000000 00000040416 15211567312 0024433 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Song(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="songs")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
sort_order: Optional[int] = ormar.Integer(nullable=True)
class Owner(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="owners")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
age: Optional[int] = ormar.Integer(nullable=True)
class AliasNested(ormar.Model):
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):
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)
nested = ormar.ForeignKey(AliasNested, name="nested_alias")
class Toy(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="toys")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
owner: Owner = ormar.ForeignKey(Owner)
class Factory(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="cars")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
factory: Optional[Factory] = ormar.ForeignKey(Factory)
class User(ormar.Model):
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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_sort_order_on_main_model():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
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()
assert songs[0].name == "Song 3"
assert songs[1].name == "Song 1"
assert songs[2].name == "Song 2"
songs = await Song.objects.order_by("-sort_order").all()
assert songs[0].name == "Song 3"
assert songs[1].name == "Song 2"
assert songs[2].name == "Song 1"
songs = await Song.objects.order_by(Song.sort_order.desc()).all()
assert songs[0].name == "Song 3"
assert songs[1].name == "Song 2"
assert songs[2].name == "Song 1"
songs = await Song.objects.order_by("sort_order").all()
assert songs[0].name == "Song 1"
assert songs[1].name == "Song 2"
assert songs[2].name == "Song 3"
songs = await Song.objects.order_by(Song.sort_order.asc()).all()
assert songs[0].name == "Song 1"
assert songs[1].name == "Song 2"
assert songs[2].name == "Song 3"
songs = await Song.objects.order_by("name").all()
assert songs[0].name == "Song 1"
assert songs[1].name == "Song 2"
assert songs[2].name == "Song 3"
songs = await Song.objects.order_by("name").limit(2).all()
assert len(songs) == 2
assert songs[0].name == "Song 1"
assert songs[1].name == "Song 2"
await Song.objects.create(name="Song 4", sort_order=1)
songs = await Song.objects.order_by(["sort_order", "name"]).all()
assert songs[0].name == "Song 1"
assert songs[1].name == "Song 4"
assert songs[2].name == "Song 2"
assert songs[3].name == "Song 3"
songs = await Song.objects.order_by(
[Song.sort_order.asc(), Song.name.asc()]
).all()
assert songs[0].name == "Song 1"
assert songs[1].name == "Song 4"
assert songs[2].name == "Song 2"
assert songs[3].name == "Song 3"
@pytest.mark.asyncio
async def test_sort_order_nulls_first_last():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await Song.objects.create(name="Song A", sort_order=10)
await Song.objects.create(name="Song B", sort_order=20)
await Song.objects.create(name="Song C", sort_order=30)
await Song.objects.create(name="Song N1")
await Song.objects.create(name="Song N2")
songs = await Song.objects.order_by(
[
Song.sort_order.asc(nulls_ordering=ormar.NullsOrdering.LAST),
Song.name.asc(),
]
).all()
assert [s.name for s in songs] == [
"Song A",
"Song B",
"Song C",
"Song N1",
"Song N2",
]
songs = await Song.objects.order_by(
[
Song.sort_order.asc(nulls_ordering=ormar.NullsOrdering.FIRST),
Song.name.asc(),
]
).all()
assert [s.name for s in songs] == [
"Song N1",
"Song N2",
"Song A",
"Song B",
"Song C",
]
songs = await Song.objects.order_by(
[
Song.sort_order.desc(nulls_ordering=ormar.NullsOrdering.LAST),
Song.name.asc(),
]
).all()
assert [s.name for s in songs] == [
"Song C",
"Song B",
"Song A",
"Song N1",
"Song N2",
]
songs = await Song.objects.order_by(
[
Song.sort_order.desc(nulls_ordering=ormar.NullsOrdering.FIRST),
Song.name.asc(),
]
).all()
assert [s.name for s in songs] == [
"Song N1",
"Song N2",
"Song C",
"Song B",
"Song A",
]
with pytest.raises(ValueError):
Song.sort_order.asc(nulls_ordering=False)
with pytest.raises(ValueError):
Song.sort_order.desc(nulls_ordering=True)
@pytest.mark.asyncio
async def test_sort_order_on_related_model():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
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)
toys = await Toy.objects.select_related("owner").order_by("name").all()
assert [x.name.replace("Toy ", "") for x in toys] == [
str(x + 1) for x in range(6)
]
assert toys[0].owner == zeus
assert toys[1].owner == aphrodite
toys = (
await Toy.objects.select_related("owner").order_by("owner__name").all()
)
assert toys[0].owner.name == toys[1].owner.name == "Aphrodite"
assert toys[2].owner.name == toys[3].owner.name == "Hermes"
assert toys[4].owner.name == toys[5].owner.name == "Zeus"
owner = (
await Owner.objects.select_related("toys")
.order_by("toys__name")
.filter(name="Zeus")
.get()
)
assert owner.toys[0].name == "Toy 1"
assert owner.toys[1].name == "Toy 4"
owner = (
await Owner.objects.select_related("toys")
.order_by("-toys__name")
.filter(name="Zeus")
.get()
)
assert owner.toys[0].name == "Toy 4"
assert owner.toys[1].name == "Toy 1"
owners = (
await Owner.objects.select_related("toys")
.order_by("-toys__name")
.filter(name__in=["Zeus", "Hermes"])
.all()
)
assert owners[0].toys[0].name == "Toy 6"
assert owners[0].toys[1].name == "Toy 5"
assert owners[0].name == "Hermes"
assert owners[1].toys[0].name == "Toy 4"
assert owners[1].toys[1].name == "Toy 1"
assert owners[1].name == "Zeus"
await Toy.objects.create(name="Toy 7", owner=zeus)
owners = (
await Owner.objects.select_related("toys")
.order_by("-toys__name")
.filter(name__in=["Zeus", "Hermes"])
.all()
)
assert owners[0].toys[0].name == "Toy 7"
assert owners[0].toys[1].name == "Toy 4"
assert owners[0].toys[2].name == "Toy 1"
assert owners[0].name == "Zeus"
assert owners[1].toys[0].name == "Toy 6"
assert owners[1].toys[1].name == "Toy 5"
assert owners[1].name == "Hermes"
toys = (
await Toy.objects.select_related("owner")
.order_by(["owner__name", "name"])
.limit(2)
.all()
)
assert len(toys) == 2
assert toys[0].name == "Toy 2"
assert toys[1].name == "Toy 3"
athena = await Owner.objects.create(name="Athena", age=35)
apollo = await Owner.objects.create(name="Apollo", age=28)
await Toy.objects.create(name="Toy 8", owner=athena)
await Toy.objects.create(name="Toy 9", owner=apollo)
toys = (
await Toy.objects.select_related("owner")
.order_by(Toy.owner.age.asc(nulls_ordering=ormar.NullsOrdering.LAST))
.all()
)
assert toys[0].name == "Toy 9"
assert toys[1].name == "Toy 8"
assert all(t.owner.age is None for t in toys[2:])
toys = (
await Toy.objects.select_related("owner")
.order_by(Toy.owner.age.asc(nulls_ordering=ormar.NullsOrdering.FIRST))
.all()
)
assert all(t.owner.age is None for t in toys[:-2])
assert toys[-2].name == "Toy 9"
assert toys[-1].name == "Toy 8"
toys = (
await Toy.objects.select_related("owner")
.order_by(Toy.owner.age.desc(nulls_ordering=ormar.NullsOrdering.LAST))
.all()
)
assert toys[0].name == "Toy 8"
assert toys[1].name == "Toy 9"
assert all(t.owner.age is None for t in toys[2:])
toys = (
await Toy.objects.select_related("owner")
.order_by(Toy.owner.age.desc(nulls_ordering=ormar.NullsOrdering.FIRST))
.all()
)
assert all(t.owner.age is None for t in toys[:-2])
assert toys[-2].name == "Toy 8"
assert toys[-1].name == "Toy 9"
@pytest.mark.asyncio
async def test_sort_order_on_many_to_many():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
factory1 = await Factory.objects.create(name="Factory 1")
factory2 = await Factory.objects.create(name="Factory 2")
car1 = await Car.objects.create(name="Buggy", factory=factory1)
car2 = await Car.objects.create(name="Volkswagen", factory=factory2)
car3 = await Car.objects.create(name="Ferrari", factory=factory1)
car4 = await Car.objects.create(name="Volvo", factory=factory2)
car5 = await Car.objects.create(name="Skoda", factory=factory1)
car6 = await Car.objects.create(name="Seat", factory=factory2)
user1 = await User.objects.create(name="Mark")
user2 = await User.objects.create(name="Julie")
await user1.cars.add(car1)
await user1.cars.add(car3)
await user1.cars.add(car4)
await user1.cars.add(car5)
await user2.cars.add(car1)
await user2.cars.add(car2)
await user2.cars.add(car5)
await user2.cars.add(car6)
user = (
await User.objects.select_related("cars")
.filter(name="Mark")
.order_by("cars__name")
.get()
)
assert user.cars[0].name == "Buggy"
assert user.cars[1].name == "Ferrari"
assert user.cars[2].name == "Skoda"
assert user.cars[3].name == "Volvo"
user = (
await User.objects.select_related("cars")
.filter(name="Mark")
.order_by("-cars__name")
.get()
)
assert user.cars[3].name == "Buggy"
assert user.cars[2].name == "Ferrari"
assert user.cars[1].name == "Skoda"
assert user.cars[0].name == "Volvo"
users = (
await User.objects.select_related("cars").order_by("-cars__name").all()
)
assert users[0].name == "Mark"
assert users[1].cars[0].name == "Volkswagen"
assert users[1].cars[1].name == "Skoda"
assert users[1].cars[2].name == "Seat"
assert users[1].cars[3].name == "Buggy"
users = (
await User.objects.select_related(["cars__factory"])
.order_by(["-cars__factory__name", "cars__name"])
.all()
)
assert users[0].name == "Julie"
assert users[0].cars[0].name == "Seat"
assert users[0].cars[1].name == "Volkswagen"
assert users[0].cars[2].name == "Buggy"
assert users[0].cars[3].name == "Skoda"
assert users[1].name == "Mark"
assert users[1].cars[0].name == "Volvo"
assert users[1].cars[1].name == "Buggy"
assert users[1].cars[2].name == "Ferrari"
assert users[1].cars[3].name == "Skoda"
@pytest.mark.asyncio
async def test_sort_order_with_aliases():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
al1 = await AliasTest.objects.create(name="Test4")
al2 = await AliasTest.objects.create(name="Test2")
al3 = await AliasTest.objects.create(name="Test1")
al4 = await AliasTest.objects.create(name="Test3")
aliases = await AliasTest.objects.order_by("-name").all()
assert [alias.name[-1] for alias in aliases] == ["4", "3", "2", "1"]
nest1 = await AliasNested.objects.create(name="Try1")
nest2 = await AliasNested.objects.create(name="Try2")
nest3 = await AliasNested.objects.create(name="Try3")
nest4 = await AliasNested.objects.create(name="Try4")
al1.nested = nest1
await al1.update()
al2.nested = nest2
await al2.update()
al3.nested = nest3
await al3.update()
al4.nested = nest4
await al4.update()
aliases = (
await AliasTest.objects.select_related("nested")
.order_by("-nested__name")
.all()
)
assert aliases[0].nested.name == "Try4"
assert aliases[1].nested.name == "Try3"
assert aliases[2].nested.name == "Try2"
assert aliases[3].nested.name == "Try1"
ormar-orm-ormar-a7e7fde/tests/test_queries/test_pagination.py 0000664 0000000 0000000 00000023533 15211567312 0024760 0 ustar 00root root 0000000 0000000 import pytest
import ormar
from ormar.exceptions import QueryDefinitionError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Car(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class UsersCar(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="cars_x_users")
class User(ormar.Model):
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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_limit_zero():
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()
cars = await Car.objects.limit(0).all()
assert cars == []
assert len(cars) == 0
@pytest.mark.asyncio
async def test_pagination_errors():
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()
with pytest.raises(QueryDefinitionError):
await Car.objects.paginate(1, page_size=0).all()
@pytest.mark.asyncio
async def test_pagination_on_single_model():
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()
cars_page1 = await Car.objects.paginate(1, page_size=5).all()
assert len(cars_page1) == 5
assert cars_page1[0].name == "0"
assert cars_page1[4].name == "4"
cars_page2 = await Car.objects.paginate(2, page_size=5).all()
assert len(cars_page2) == 5
assert cars_page2[0].name == "5"
assert cars_page2[4].name == "9"
all_cars = await Car.objects.paginate(1).all()
assert len(all_cars) == 20
half_cars = await Car.objects.paginate(2, page_size=10).all()
assert len(half_cars) == 10
assert half_cars[0].name == "10"
@pytest.mark.asyncio
async def test_proxy_pagination():
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):
c = await Car(name=f"{i}").save()
await user.cars.add(c)
await user.cars.paginate(1, page_size=5).all()
assert len(user.cars) == 5
assert user.cars[0].name == "0"
assert user.cars[4].name == "4"
await user.cars.paginate(2, page_size=5).all()
assert len(user.cars) == 5
assert user.cars[0].name == "5"
assert user.cars[4].name == "9"
await user.cars.paginate(1).all()
assert len(user.cars) == 20
await user.cars.paginate(2, page_size=10).all()
assert len(user.cars) == 10
assert user.cars[0].name == "10"
@pytest.mark.asyncio
async def test_slice_rejects_invalid_types_and_shapes():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
with pytest.raises(QueryDefinitionError):
Car.objects["foo"] # type: ignore[index]
with pytest.raises(QueryDefinitionError):
Car.objects[::2]
with pytest.raises(QueryDefinitionError):
Car.objects[:-2]
with pytest.raises(QueryDefinitionError):
Car.objects[2:-5]
with pytest.raises(QueryDefinitionError):
Car.objects[-5:2]
@pytest.mark.asyncio
async def test_slice_positive_bounds():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
for i in range(10):
await Car(name=f"{i}").save()
cars_mid = await Car.objects[2:8].all()
assert len(cars_mid) == 6
assert cars_mid[0].name == "2"
assert cars_mid[-1].name == "7"
cars_tail = await Car.objects[2:].all()
assert len(cars_tail) == 8
assert cars_tail[0].name == "2"
assert cars_tail[-1].name == "9"
cars_head = await Car.objects[:8].all()
assert len(cars_head) == 8
assert cars_head[0].name == "0"
assert cars_head[-1].name == "7"
single = await Car.objects[5].all()
assert len(single) == 1
assert single[0].name == "5"
empty = await Car.objects[8:2].all()
assert empty == []
all_cars = await Car.objects[:].all()
assert len(all_cars) == 10
@pytest.mark.asyncio
async def test_slice_negative_returns_tail_in_original_order():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
for i in range(10):
await Car(name=f"{i}").save()
last_three = await Car.objects[-3:].all()
assert [c.name for c in last_three] == ["7", "8", "9"]
last_one = await Car.objects[-1].all()
assert [c.name for c in last_one] == ["9"]
middle_from_tail = await Car.objects[-5:-2].all()
assert [c.name for c in middle_from_tail] == ["5", "6", "7"]
empty_reversed = await Car.objects[-2:-5].all()
assert empty_reversed == []
@pytest.mark.asyncio
async def test_slice_respects_user_order_by():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
for i in range(10):
await Car(name=f"{i}").save()
desc_head = await Car.objects.order_by("-name")[:3].all()
assert [c.name for c in desc_head] == ["9", "8", "7"]
desc_tail = await Car.objects.order_by("-name")[-3:].all()
assert [c.name for c in desc_tail] == ["2", "1", "0"]
@pytest.mark.asyncio
async def test_slice_with_filter_chaining():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
for i in range(10):
await Car(name=f"{i}").save()
filtered_tail = await Car.objects.filter(id__gte=3)[-2:].all()
assert [c.name for c in filtered_tail] == ["8", "9"]
@pytest.mark.asyncio
async def test_proxy_slice_positive_and_negative():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
user = await User(name="Sep").save()
for i in range(10):
c = await Car(name=f"{i}").save()
await user.cars.add(c)
await user.cars.filter(id__gte=0)[:5].all()
assert len(user.cars) == 5
assert user.cars[0].name == "0"
assert user.cars[4].name == "4"
await user.cars.filter(id__gte=0)[-3:].all()
assert [c.name for c in user.cars] == ["7", "8", "9"]
await user.cars.filter(id__gte=0)[-1].all()
assert len(user.cars) == 1
assert user.cars[0].name == "9"
@pytest.mark.asyncio
async def test_slice_chained_after_negative_recanonicalizes():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
for i in range(10):
await Car(name=f"{i}").save()
sliced_again = await Car.objects[-3:][:2].all()
assert [c.name for c in sliced_again] == ["0", "1"]
@pytest.mark.asyncio
async def test_proxy_first_or_none():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
user = await User(name="Jon").save()
assert await user.cars.first_or_none() is None
for i in range(3):
c = await Car(name=f"{i}").save()
await user.cars.add(c)
first_car = await user.cars.first_or_none()
assert first_car is not None
assert first_car.name == "0"
assert await user.cars.first_or_none(name="missing") is None
@pytest.mark.asyncio
async def test_proxy_last_and_last_or_none():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
user = await User(name="Jon").save()
assert await user.cars.last_or_none() is None
for i in range(5):
c = await Car(name=f"{i}").save()
await user.cars.add(c)
last_car = await user.cars.last()
assert last_car.name == "4"
filtered = await user.cars.last(name="2")
assert filtered.name == "2"
existing = await user.cars.last_or_none(name="3")
assert existing is not None
assert existing.name == "3"
assert await user.cars.last_or_none(name="missing") is None
@pytest.mark.asyncio
async def test_last_with_prefetch_related():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
user = await User(name="Tom").save()
for i in range(3):
c = await Car(name=f"{i}").save()
await user.cars.add(c)
last_user = await User.objects.prefetch_related("cars").last()
assert last_user.name == "Tom"
assert len(last_user.cars) == 3
ormar-orm-ormar-a7e7fde/tests/test_queries/test_queryproxy_on_m2m_models.py 0000664 0000000 0000000 00000016617 15211567312 0027715 0 ustar 00root root 0000000 0000000 from typing import Optional, Union
import pytest
import ormar
from ormar.exceptions import QueryDefinitionError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Subject(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=80)
last_name: str = ormar.String(max_length=80)
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=40)
sort_order: Optional[int] = ormar.Integer(nullable=True)
subject: Optional[Subject] = ormar.ForeignKey(Subject)
class PostCategory(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="posts_categories")
class Post(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="posts")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[Union[Category, list[Category]]] = ormar.ManyToMany(
Category, through=PostCategory
)
author: Optional[Author] = ormar.ForeignKey(Author)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_queryset_methods():
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"
)
subject = await Subject(name="Random").save()
post = await Post.objects.create(title="Hello, M2M", author=guido)
news = await Category.objects.create(
name="News", sort_order=1, subject=subject
)
breaking = await Category.objects.create(
name="Breaking", sort_order=3, subject=subject
)
# Add a category to a post.
await post.categories.add(news)
await post.categories.add(breaking)
category, created = await post.categories.get_or_create(name="News")
assert category == news
assert len(post.categories) == 1
assert created is False
category, created = await post.categories.get_or_create(
name="Breaking News"
)
assert category != breaking
assert category.pk is not None
assert len(post.categories) == 2
assert created is True
await post.categories.update_or_create(pk=category.pk, name="Urgent News")
assert len(post.categories) == 2
cat, created = await post.categories.get_or_create(name="Urgent News")
assert cat.pk == category.pk
assert len(post.categories) == 1
assert created is False
await post.categories.remove(cat)
await cat.delete()
assert len(post.categories) == 0
category = await post.categories.update_or_create(
name="Weather News", sort_order=2, subject=subject
)
assert category.pk is not None
assert category.posts[0] == post
assert len(post.categories) == 1
categories = await post.categories.all()
assert len(categories) == 3 == len(post.categories)
assert await post.categories.exists()
assert 3 == await post.categories.count()
categories = await post.categories.limit(2).all()
assert len(categories) == 2 == len(post.categories)
categories2 = await post.categories.limit(2).offset(1).all()
assert len(categories2) == 2 == len(post.categories)
assert categories != categories2
categories = await post.categories.order_by("-sort_order").all()
assert len(categories) == 3 == len(post.categories)
assert post.categories[2].name == "News"
assert post.categories[0].name == "Breaking"
categories = await post.categories.exclude(name__icontains="news").all()
assert len(categories) == 1 == len(post.categories)
assert post.categories[0].name == "Breaking"
categories = (
await post.categories.filter(name__icontains="news")
.order_by("-name")
.all()
)
assert len(categories) == 2 == len(post.categories)
assert post.categories[0].name == "Weather News"
assert post.categories[1].name == "News"
categories = await post.categories.fields("name").all()
assert len(categories) == 3 == len(post.categories)
for cat in post.categories:
assert cat.sort_order is None
categories = await post.categories.exclude_fields("sort_order").all()
assert len(categories) == 3 == len(post.categories)
for cat in post.categories:
assert cat.sort_order is None
assert cat.subject.name is None
categories = await post.categories.select_related("subject").all()
assert len(categories) == 3 == len(post.categories)
for cat in post.categories:
assert cat.subject.name is not None
categories = await post.categories.prefetch_related("subject").all()
assert len(categories) == 3 == len(post.categories)
for cat in post.categories:
assert cat.subject.name is not None
@pytest.mark.asyncio
async def test_queryset_update():
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"
)
subject = await Subject(name="Random").save()
post = await Post.objects.create(title="Hello, M2M", author=guido)
await post.categories.create(name="News", sort_order=1, subject=subject)
await post.categories.create(name="Breaking", sort_order=3, subject=subject)
await post.categories.order_by("sort_order").all()
assert len(post.categories) == 2
assert post.categories[0].sort_order == 1
assert post.categories[0].name == "News"
assert post.categories[1].sort_order == 3
assert post.categories[1].name == "Breaking"
updated = await post.categories.update(each=True, name="Test")
assert updated == 2
await post.categories.order_by("sort_order").all()
assert len(post.categories) == 2
assert post.categories[0].name == "Test"
assert post.categories[1].name == "Test"
updated = await post.categories.filter(sort_order=3).update(name="Test 2")
assert updated == 1
await post.categories.order_by("sort_order").all()
assert len(post.categories) == 2
assert post.categories[0].name == "Test"
assert post.categories[1].name == "Test 2"
with pytest.raises(QueryDefinitionError):
await post.categories.update(name="Test WRONG")
ormar-orm-ormar-a7e7fde/tests/test_queries/test_queryset_level_methods.py 0000664 0000000 0000000 00000046560 15211567312 0027427 0 ustar 00root root 0000000 0000000 from enum import Enum
from typing import Optional
import pydantic
import pytest
from pydantic import Json
import ormar
from ormar import QuerySet
from ormar.exceptions import (
ModelListEmptyError,
ModelPersistenceError,
QueryDefinitionError,
)
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config(force_rollback=True)
class MySize(Enum):
SMALL = 0
BIG = 1
class Book(ormar.Model):
ormar_config = base_ormar_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",
)
class ToDo(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="todos")
id: int = ormar.Integer(primary_key=True)
text: str = ormar.String(max_length=500)
completed: bool = ormar.Boolean(default=False)
pairs: pydantic.Json = ormar.JSON(default=[])
size = ormar.Enum(enum_class=MySize, default=MySize.SMALL)
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=500)
class Note(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="notes")
id: int = ormar.Integer(primary_key=True)
text: str = ormar.String(max_length=500)
category: Optional[Category] = ormar.ForeignKey(Category)
class ItemConfig(ormar.Model):
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)
pairs: pydantic.Json = ormar.JSON(default=["2", "3"])
size = ormar.Enum(enum_class=MySize, default=MySize.SMALL)
class QuerySetCls(QuerySet):
async def first_or_404(self, *args, **kwargs):
entity = await self.get_or_none(*args, **kwargs)
if not entity:
# maybe HTTPException in fastapi
raise ValueError("customer not found")
return entity
class Customer(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="test_model")
id: int = ormar.Integer(primary_key=True)
json_field: Json = ormar.JSON()
class Library(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="libraries")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class UniqueBook(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="unique_books")
id: int = ormar.Integer(primary_key=True)
isbn: str = ormar.String(max_length=20, unique=True)
title: str = ormar.String(max_length=200)
library: Optional[Library] = ormar.ForeignKey(
Library, nullable=True, related_name="books"
)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_delete_and_update():
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"
)
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.create(
title="Harry Potter", author="Rowling, J.K.", genre="Fantasy"
)
await Book.objects.create(
title="Lord of the Rings", author="Tolkien, J.R.", genre="Fantasy"
)
all_books = await Book.objects.all()
assert len(all_books) == 5
await Book.objects.filter(author="Tolstoy, Leo").update(
author="Lenin, Vladimir"
)
all_books = await Book.objects.filter(author="Lenin, Vladimir").all()
assert len(all_books) == 2
historic_books = await Book.objects.filter(genre="Historic").all()
assert len(historic_books) == 0
with pytest.raises(QueryDefinitionError):
await Book.objects.update(genre="Historic")
await Book.objects.filter(author="Lenin, Vladimir").update(genre="Historic")
historic_books = await Book.objects.filter(genre="Historic").all()
assert len(historic_books) == 2
await Book.objects.delete(genre="Fantasy")
all_books = await Book.objects.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
with pytest.raises(QueryDefinitionError):
await Book.objects.delete()
await Book.objects.delete(each=True)
all_books = await Book.objects.all()
assert len(all_books) == 0
@pytest.mark.asyncio
async def test_get_or_create():
async with base_ormar_config.database:
tom, created = await Book.objects.get_or_create(
title="Volume I", author="Anonymous", genre="Fiction"
)
assert await Book.objects.count() == 1
assert created is True
second_tom, created = await Book.objects.get_or_create(
title="Volume I", author="Anonymous", genre="Fiction"
)
assert second_tom.pk == tom.pk
assert created is False
assert await Book.objects.count() == 1
assert await Book.objects.create(
title="Volume I", author="Anonymous", genre="Fiction"
)
with pytest.raises(ormar.exceptions.MultipleMatches):
await Book.objects.get_or_create(
title="Volume I", author="Anonymous", genre="Fiction"
)
@pytest.mark.asyncio
async def test_get_or_create_with_defaults():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
book, created = await Book.objects.get_or_create(
title="Nice book", _defaults={"author": "Mojix", "genre": "Historic"}
)
assert created is True
assert book.author == "Mojix"
assert book.title == "Nice book"
assert book.genre == "Historic"
book2, created = await Book.objects.get_or_create(
author="Mojix", _defaults={"title": "Book2"}
)
assert created is False
assert book2 == book
assert book2.title == "Nice book"
assert book2.author == "Mojix"
assert book2.genre == "Historic"
assert await Book.objects.count() == 1
book, created = await Book.objects.get_or_create(
title="doesn't exist",
_defaults={
"title": "overwritten",
"author": "Mojix",
"genre": "Historic",
},
)
assert created is True
assert book.title == "overwritten"
book2, created = await Book.objects.get_or_create(
title="overwritten", _defaults={"title": "doesn't work"}
)
assert created is False
assert book2.title == "overwritten"
assert book2 == book
@pytest.mark.asyncio
async def test_update_or_create():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
tom = await Book.objects.update_or_create(
title="Volume I", author="Anonymous", genre="Fiction"
)
assert await Book.objects.count() == 1
assert await Book.objects.update_or_create(id=tom.id, genre="Historic")
assert await Book.objects.count() == 1
assert await Book.objects.update_or_create(pk=tom.id, genre="Fantasy")
assert await Book.objects.count() == 1
assert await Book.objects.create(
title="Volume I", author="Anonymous", genre="Fantasy"
)
with pytest.raises(ormar.exceptions.MultipleMatches):
await Book.objects.get(
title="Volume I", author="Anonymous", genre="Fantasy"
)
@pytest.mark.asyncio
async def test_update_or_create_creates_when_pk_does_not_exist():
"""``update_or_create`` with a pk that doesn't exist must create the
row at that pk instead of raising ``NoMatch``.
"""
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
assert await Book.objects.count() == 0
book = await Book.objects.update_or_create(
id=42, title="X", author="Y", genre="Fiction"
)
assert book.id == 42
assert await Book.objects.count() == 1
updated = await Book.objects.update_or_create(id=42, genre="Historic")
assert updated.id == 42
assert updated.genre == "Historic"
assert await Book.objects.count() == 1
@pytest.mark.asyncio
async def test_update_or_create_creates_when_pk_alias_does_not_exist():
"""Same as above but using the ``pk`` alias kwarg, which goes
through the ``kwargs.pop("pk")`` rename branch.
"""
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
book = await Book.objects.update_or_create(
pk=99, title="A", author="B", genre="Fiction"
)
assert book.id == 99
assert await Book.objects.count() == 1
@pytest.mark.asyncio
async def test_proxy_update_or_create_creates_when_pk_does_not_exist():
"""``QuerysetProxy.update_or_create`` (m2m / reverse-fk) duplicates
the same get/create logic and must also create when given a missing
pk instead of raising ``NoMatch``.
"""
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
library = await Library.objects.create(name="Main")
book = await library.books.update_or_create(
id=123, isbn="978-0-00-000000-9", title="Created"
)
assert book.id == 123
assert book.isbn == "978-0-00-000000-9"
assert await UniqueBook.objects.count() == 1
@pytest.mark.asyncio
async def test_bulk_create():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
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
for todo in todoes:
assert todo.pk is not None
completed = await ToDo.objects.filter(completed=True).all()
assert len(completed) == 2
with pytest.raises(ormar.exceptions.ModelListEmptyError):
await ToDo.objects.bulk_create([])
@pytest.mark.asyncio
async def test_bulk_create_json_field():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(
force_rollback=True
) as transaction:
json_value = {"a": 1}
test_model_1 = JsonTestModel(id=1, json_field=json_value)
test_model_2 = JsonTestModel(id=2, json_field=json_value)
# store one with .save() and the other with .bulk_create()
await test_model_1.save()
await JsonTestModel.objects.bulk_create([test_model_2])
# refresh from the database
await test_model_1.load()
await test_model_2.load()
assert test_model_1.json_field == test_model_2.json_field # True
# try to query the json field
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 list(
(await transaction._connection.execute(query)).mappings().all()
)
]
assert test_model_1 in res
assert test_model_2 in res
assert len(res) == 2
@pytest.mark.asyncio
async def test_bulk_create_with_relation():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
category = await Category.objects.create(name="Sample Category")
await Note.objects.bulk_create(
[
Note(text="Buy the groceries.", category=category),
Note(text="Call Mum.", category=category),
]
)
todoes = await Note.objects.all()
assert len(todoes) == 2
for todo in todoes:
assert todo.category.pk == category.pk
@pytest.mark.asyncio
async def test_bulk_update():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
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
for todo in todoes:
todo.text = todo.text + "_1"
todo.completed = False
todo.size = MySize.BIG
await ToDo.objects.bulk_update(todoes)
completed = await ToDo.objects.filter(completed=False).all()
assert len(completed) == 3
todoes = await ToDo.objects.all()
assert len(todoes) == 3
for todo in todoes:
assert todo.text[-2:] == "_1"
assert todo.size == MySize.BIG
@pytest.mark.asyncio
async def test_bulk_update_with_only_selected_columns():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await ToDo.objects.bulk_create(
[
ToDo(text="Reset the world simulation.", completed=False),
ToDo(text="Watch kittens.", completed=True),
]
)
todoes = await ToDo.objects.all()
assert len(todoes) == 2
for todo in todoes:
todo.text = todo.text + "_1"
todo.completed = False
await ToDo.objects.bulk_update(todoes, columns=["completed"])
completed = await ToDo.objects.filter(completed=False).all()
assert len(completed) == 2
todoes = await ToDo.objects.all()
assert len(todoes) == 2
for todo in todoes:
assert todo.text[-2:] != "_1"
@pytest.mark.asyncio
async def test_bulk_update_with_relation():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
category = await Category.objects.create(name="Sample Category")
category2 = await Category.objects.create(name="Sample II Category")
await Note.objects.bulk_create(
[
Note(text="Buy the groceries.", category=category),
Note(text="Call Mum.", category=category),
Note(text="Text skynet.", category=category),
]
)
notes = await Note.objects.all()
assert len(notes) == 3
for note in notes:
note.category = category2
await Note.objects.bulk_update(notes)
notes_upd = await Note.objects.all()
assert len(notes_upd) == 3
for note in notes_upd:
assert note.category.pk == category2.pk
@pytest.mark.asyncio
async def test_bulk_update_not_saved_objts():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
category = await Category.objects.create(name="Sample Category")
with pytest.raises(ModelPersistenceError):
await Note.objects.bulk_update(
[
Note(text="Buy the groceries.", category=category),
Note(text="Call Mum.", category=category),
]
)
with pytest.raises(ModelListEmptyError):
await Note.objects.bulk_update([])
@pytest.mark.asyncio
async def test_bulk_operations_with_json():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(
force_rollback=True
) as transaction:
items = [
ItemConfig(item_id="test1"),
ItemConfig(item_id="test2"),
ItemConfig(item_id="test3"),
]
await ItemConfig.objects.bulk_create(items)
items = await ItemConfig.objects.all()
assert all(x.pairs == ["2", "3"] for x in items)
for item in items:
item.pairs = ["1"]
await ItemConfig.objects.bulk_update(items)
items = await ItemConfig.objects.all()
assert all(x.pairs == ["1"] for x in items)
items = await ItemConfig.objects.filter(ItemConfig.id > 1).all()
for item in items:
item.pairs = {"b": 2}
await ItemConfig.objects.bulk_update(items)
items = await ItemConfig.objects.filter(ItemConfig.id > 1).all()
assert all(x.pairs == {"b": 2} for x in items)
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 list(
(await transaction._connection.execute(query)).mappings().all()
)
]
assert len(res) == 2
@pytest.mark.asyncio
async def test_custom_queryset_cls():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
with pytest.raises(ValueError):
await Customer.objects.first_or_404(id=1)
await Customer(name="test").save()
c = await Customer.objects.first_or_404(name="test")
assert c.name == "test"
@pytest.mark.asyncio
async def test_filter_enum():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
it = ItemConfig(item_id="test_1")
await it.save()
it = await ItemConfig.objects.filter(size=MySize.SMALL).first()
assert it
ormar-orm-ormar-a7e7fde/tests/test_queries/test_quoting_table_names_in_on_join_clause.py 0000664 0000000 0000000 00000002517 15211567312 0032403 0 ustar 00root root 0000000 0000000 import datetime
import uuid
from typing import Optional, Union
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Team(ormar.Model):
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)
client_id = ormar.Text(nullable=True)
client_secret = ormar.Text(nullable=True)
created_on = ormar.DateTime(timezone=True, default=datetime.datetime.utcnow())
class User(ormar.Model):
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()
token = ormar.Text(nullable=True)
team: Optional[Team] = ormar.ForeignKey(to=Team, name="team_id")
class Order(ormar.Model):
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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_quoting_on_clause_without_prefix():
async with base_ormar_config.database:
await User.objects.select_related("orders").all()
ormar-orm-ormar-a7e7fde/tests/test_queries/test_reserved_sql_keywords_escaped.py 0000664 0000000 0000000 00000005173 15211567312 0030740 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config(force_rollback=True)
class User(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="user")
id: int = ormar.Integer(primary_key=True, autoincrement=True, nullable=False)
user: str = ormar.String(
unique=True, index=True, nullable=False, max_length=255
) # ID of the user on auth0
first: str = ormar.String(nullable=False, max_length=255)
last: str = ormar.String(nullable=False, max_length=255)
email: str = ormar.String(unique=True, index=True, nullable=False, max_length=255)
display_name: str = ormar.String(
unique=True, index=True, nullable=False, max_length=255
)
pic_url: Optional[str] = ormar.Text(nullable=True)
class Task(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="task")
id: int = ormar.Integer(primary_key=True, autoincrement=True, nullable=False)
from_: Optional[str] = ormar.String(name="from", nullable=True, max_length=200)
user = ormar.ForeignKey(User)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_single_model_quotes():
async with base_ormar_config.database:
await User.objects.create(
user="test",
first="first",
last="last",
email="email@com.com",
display_name="first last",
)
user = await User.objects.order_by("user").get(first="first")
assert user.last == "last"
assert user.email == "email@com.com"
@pytest.mark.asyncio
async def test_two_model_quotes():
async with base_ormar_config.database:
user = await User.objects.create(
user="test",
first="first",
last="last",
email="email@com.com",
display_name="first last",
)
await Task(user=user, from_="aa").save()
await Task(user=user, from_="bb").save()
task = (
await Task.objects.select_related("user")
.order_by("user__user")
.get(from_="aa")
)
assert task.user.last == "last"
assert task.user.email == "email@com.com"
tasks = await Task.objects.select_related("user").order_by("-from").all()
assert len(tasks) == 2
assert tasks[0].user.last == "last"
assert tasks[0].user.email == "email@com.com"
assert tasks[0].from_ == "bb"
assert tasks[1].user.last == "last"
assert tasks[1].user.email == "email@com.com"
assert tasks[1].from_ == "aa"
ormar-orm-ormar-a7e7fde/tests/test_queries/test_reverse_fk_queryset.py 0000664 0000000 0000000 00000020362 15211567312 0026720 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from ormar import NoMatch
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="album_id")
name: str = ormar.String(max_length=100)
is_best_seller: bool = ormar.Boolean(default=False)
class Writer(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="tracks")
id: int = ormar.Integer(primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album, name="album_id")
title: str = ormar.String(max_length=100)
position: int = ormar.Integer()
play_count: Optional[int] = ormar.Integer(nullable=True)
written_by: Optional[Writer] = ormar.ForeignKey(Writer, name="writer_id")
async def get_sample_data():
album = await Album(name="Malibu").save()
writer1 = await Writer.objects.create(name="John")
writer2 = await Writer.objects.create(name="Sue")
track1 = await Track(
album=album, title="The Bird", position=1, play_count=30, written_by=writer1
).save()
track2 = await Track(
album=album,
title="Heart don't stand a chance",
position=2,
play_count=20,
written_by=writer2,
).save()
tracks3 = await Track(
album=album, title="The Waters", position=3, play_count=10, written_by=writer1
).save()
return album, [track1, track2, tracks3]
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_quering_by_reverse_fk():
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()
assert await album.tracks.exists()
assert await album.tracks.count() == 3
track, created = await album.tracks.get_or_create(
title="The Bird", position=1, play_count=30
)
assert track == track1
assert created is False
assert len(album.tracks) == 1
track, created = await album.tracks.get_or_create(
title="The Bird2", _defaults={"position": 4, "play_count": 5}
)
assert track != track1
assert created is True
assert track.pk is not None
assert track.position == 4 and track.play_count == 5
assert len(album.tracks) == 2
await album.tracks.update_or_create(pk=track.pk, play_count=50)
assert len(album.tracks) == 2
track, created = await album.tracks.get_or_create(title="The Bird2")
assert created is False
assert track.play_count == 50
assert len(album.tracks) == 1
await album.tracks.remove(track)
assert track.album is None
await track.delete()
assert len(album.tracks) == 0
track6 = await album.tracks.update_or_create(
title="The Bird3", position=4, play_count=5
)
assert track6.pk is not None
assert track6.play_count == 5
assert len(album.tracks) == 1
await album.tracks.remove(track6)
assert track6.album is None
await track6.delete()
assert len(album.tracks) == 0
@pytest.mark.asyncio
async def test_getting():
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(
title="The Bird"
)
track2 = await album.tracks.exclude_fields("play_count").get(
title="The Bird"
)
for track in [track1, track2]:
assert track.title == "The Bird"
assert track.album == album
assert track.play_count is None
assert len(album.tracks) == 1
tracks = await album.tracks.all()
assert len(tracks) == 3
assert len(album.tracks) == 3
tracks = await album.tracks.order_by("play_count").all()
assert len(tracks) == 3
assert tracks[0].title == "The Waters"
assert tracks[2].title == "The Bird"
assert len(album.tracks) == 3
track = await album.tracks.create(
title="The Bird Fly Away", position=4, play_count=10
)
assert track.title == "The Bird Fly Away"
assert track.position == 4
assert track.album == album
assert len(album.tracks) == 4
tracks = await album.tracks.all()
assert len(tracks) == 4
tracks = await album.tracks.limit(2).all()
assert len(tracks) == 2
tracks2 = await album.tracks.limit(2).offset(2).all()
assert len(tracks2) == 2
assert tracks != tracks2
tracks3 = await album.tracks.filter(play_count__lt=15).all()
assert len(tracks3) == 2
tracks4 = await album.tracks.exclude(play_count__lt=15).all()
assert len(tracks4) == 2
assert tracks3 != tracks4
assert len(album.tracks) == 2
await album.tracks.clear()
tracks = await album.tracks.all()
assert len(tracks) == 0
assert len(album.tracks) == 0
still_tracks = await Track.objects.all()
assert len(still_tracks) == 4
for track in still_tracks:
assert track.album is None
@pytest.mark.asyncio
async def test_cleaning_related():
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)
tracks = await album.tracks.all()
assert len(tracks) == 0
assert len(album.tracks) == 0
no_tracks = await Track.objects.all()
assert len(no_tracks) == 0
@pytest.mark.asyncio
async def test_loading_related():
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()
assert len(tracks) == 3
assert len(album.tracks) == 3
for track in tracks:
assert track.written_by is not None
tracks = await album.tracks.prefetch_related("written_by").all()
assert len(tracks) == 3
assert len(album.tracks) == 3
for track in tracks:
assert track.written_by is not None
@pytest.mark.asyncio
async def test_adding_removing():
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()
await album.tracks.add(track_new)
assert track_new.album == album
assert len(album.tracks) == 4
track_check = await Track.objects.get(title="Rainbow")
assert track_check.album == album
await album.tracks.remove(track_new)
assert track_new.album is None
assert len(album.tracks) == 3
track1 = album.tracks[0]
await album.tracks.remove(track1, keep_reversed=False)
with pytest.raises(NoMatch):
await track1.load()
track_test = await Track.objects.get(title="Rainbow")
assert track_test.album is None
ormar-orm-ormar-a7e7fde/tests/test_queries/test_selecting_subset_of_columns.py 0000664 0000000 0000000 00000021374 15211567312 0030416 0 ustar 00root root 0000000 0000000 import itertools
from typing import Optional
import pydantic
import pytest
import pytest_asyncio
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class NickNames(ormar.Model):
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")
is_lame: Optional[bool] = ormar.Boolean(nullable=True)
class NicksHq(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="nicks_x_hq")
class HQ(ormar.Model):
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")
nicks: list[NickNames] = ormar.ManyToMany(NickNames, through=NicksHq)
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, nullable=False, name="company_name")
founded: Optional[int] = ormar.Integer(nullable=True)
hq: HQ = ormar.ForeignKey(HQ)
class Car(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="cars")
id: int = ormar.Integer(primary_key=True)
manufacturer: Optional[Company] = ormar.ForeignKey(Company)
name: str = ormar.String(max_length=100)
year: Optional[int] = ormar.Integer(nullable=True)
gearbox_type: Optional[str] = ormar.String(max_length=20, nullable=True)
gears: Optional[int] = ormar.Integer(nullable=True)
aircon_type: Optional[str] = ormar.String(max_length=20, nullable=True)
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(autouse=True, scope="module")
async def sample_data():
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")
await hq.nicks.add(nick1)
await hq.nicks.add(nick2)
toyota = await Company.objects.create(name="Toyota", founded=1937, hq=hq)
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",
)
@pytest.mark.asyncio
async def test_selecting_subset():
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(
[
"id",
"name",
"manufacturer__name",
"manufacturer__hq__name",
"manufacturer__hq__nicks__name",
]
)
.all()
)
all_cars2 = (
await Car.objects.select_related(["manufacturer__hq__nicks"])
.fields(
{
"id": ...,
"name": ...,
"manufacturer": {
"name": ...,
"hq": {"name": ..., "nicks": {"name": ...}},
},
}
)
.all()
)
all_cars3 = (
await Car.objects.select_related(["manufacturer__hq__nicks"])
.fields(
{
"id": ...,
"name": ...,
"manufacturer": {
"name": ...,
"hq": {"name": ..., "nicks": {"name"}},
},
}
)
.all()
)
assert all_cars3 == all_cars
for car in itertools.chain(all_cars, all_cars2):
assert all(
getattr(car, x) is None
for x in ["year", "gearbox_type", "gears", "aircon_type"]
)
assert car.manufacturer.name == "Toyota"
assert car.manufacturer.founded is None
assert car.manufacturer.hq.name == "Japan"
assert len(car.manufacturer.hq.nicks) == 2
assert car.manufacturer.hq.nicks[0].is_lame is None
all_cars = (
await Car.objects.select_related("manufacturer")
.fields("id")
.fields(["name"])
.all()
)
for car in all_cars:
assert all(
getattr(car, x) is None
for x in ["year", "gearbox_type", "gears", "aircon_type"]
)
assert car.manufacturer.name == "Toyota"
assert car.manufacturer.founded == 1937
assert car.manufacturer.hq.name is None
all_cars_check = await Car.objects.select_related("manufacturer").all()
all_cars_with_whole_nested = (
await Car.objects.select_related("manufacturer")
.fields(["id", "name", "year", "gearbox_type", "gears", "aircon_type"])
.fields({"manufacturer": ...})
.all()
)
for car in itertools.chain(all_cars_check, all_cars_with_whole_nested):
assert all(
getattr(car, x) is not None
for x in ["year", "gearbox_type", "gears", "aircon_type"]
)
assert car.manufacturer.name == "Toyota"
assert car.manufacturer.founded == 1937
all_cars_dummy = (
await Car.objects.select_related("manufacturer")
.fields(["id", "name", "year", "gearbox_type", "gears", "aircon_type"])
# .fields({"manufacturer": ...})
# .exclude_fields({"manufacturer": ...})
.fields({"manufacturer": {"name"}})
.exclude_fields({"manufacturer__founded"})
.all()
)
assert all_cars_dummy[0].manufacturer.founded is None
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"])
.all()
)
@pytest.mark.asyncio
async def test_selecting_subset_of_through_model():
async with base_ormar_config.database:
car = (
await Car.objects.select_related(["manufacturer__hq__nicks"])
.fields(
{
"id": ...,
"name": ...,
"manufacturer": {
"name": ...,
"hq": {"name": ..., "nicks": {"name": ...}},
},
}
)
.exclude_fields("manufacturer__hq__nickshq")
.get()
)
assert car.manufacturer.hq.nicks[0].nickshq is None
car = (
await Car.objects.select_related(["manufacturer__hq__nicks"])
.fields(
{
"id": ...,
"name": ...,
"manufacturer": {
"name": ...,
"hq": {"name": ..., "nicks": {"name": ...}},
},
}
)
.exclude_fields({"manufacturer": {"hq": {"nickshq": ...}}})
.get()
)
assert car.manufacturer.hq.nicks[0].nickshq is None
car = (
await Car.objects.select_related(["manufacturer__hq__nicks"])
.fields(
{
"id": ...,
"name": ...,
"manufacturer": {
"name": ...,
"hq": {"name": ..., "nicks": {"name": ...}},
},
}
)
.exclude_fields("manufacturer__hq__nickshq__nick")
.get()
)
assert car.manufacturer.hq.nicks[0].nickshq is not None
ormar-orm-ormar-a7e7fde/tests/test_queries/test_values_and_values_list.py 0000664 0000000 0000000 00000050600 15211567312 0027355 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import pytest_asyncio
import ormar
from ormar.exceptions import QueryDefinitionError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class User(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Role(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
users: list[User] = ormar.ManyToMany(User)
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=40)
sort_order: Optional[int] = ormar.Integer(nullable=True)
created_by: Optional[User] = ormar.ForeignKey(User, related_name="categories")
class Post(ormar.Model):
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)
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(autouse=True, scope="module")
async def sample_data():
async with base_ormar_config.database:
creator = await User(name="Anonymous").save()
admin = await Role(name="admin").save()
editor = await Role(name="editor").save()
await creator.roles.add(admin)
await creator.roles.add(editor)
news = await Category(name="News", sort_order=0, created_by=creator).save()
await Post(name="Ormar strikes again!", category=news).save()
await Post(name="Why don't you use ormar yet?", category=news).save()
await Post(name="Check this out, ormar now for free", category=news).save()
@pytest.mark.asyncio
async def test_simple_queryset_values():
async with base_ormar_config.database:
posts = await Post.objects.values()
assert posts == [
{"id": 1, "name": "Ormar strikes again!", "category": 1},
{"id": 2, "name": "Why don't you use ormar yet?", "category": 1},
{"id": 3, "name": "Check this out, ormar now for free", "category": 1},
]
@pytest.mark.asyncio
async def test_queryset_values_nested_relation():
async with base_ormar_config.database:
posts = await Post.objects.select_related("category__created_by").values()
assert posts == [
{
"id": 1,
"name": "Ormar strikes again!",
"category": 1,
"category__id": 1,
"category__name": "News",
"category__sort_order": 0,
"category__created_by": 1,
"category__created_by__id": 1,
"category__created_by__name": "Anonymous",
},
{
"category": 1,
"id": 2,
"name": "Why don't you use ormar yet?",
"category__id": 1,
"category__name": "News",
"category__sort_order": 0,
"category__created_by": 1,
"category__created_by__id": 1,
"category__created_by__name": "Anonymous",
},
{
"id": 3,
"name": "Check this out, ormar now for free",
"category": 1,
"category__id": 1,
"category__name": "News",
"category__sort_order": 0,
"category__created_by": 1,
"category__created_by__id": 1,
"category__created_by__name": "Anonymous",
},
]
@pytest.mark.asyncio
async def test_queryset_values_nested_relation_subset_of_fields():
async with base_ormar_config.database:
posts = await Post.objects.select_related("category__created_by").values(
["name", "category__name", "category__created_by__name"]
)
assert posts == [
{
"name": "Ormar strikes again!",
"category__name": "News",
"category__created_by__name": "Anonymous",
},
{
"name": "Why don't you use ormar yet?",
"category__name": "News",
"category__created_by__name": "Anonymous",
},
{
"name": "Check this out, ormar now for free",
"category__name": "News",
"category__created_by__name": "Anonymous",
},
]
@pytest.mark.asyncio
async def test_queryset_simple_values_list():
async with base_ormar_config.database:
posts = await Post.objects.values_list()
assert posts == [
(1, "Ormar strikes again!", 1),
(2, "Why don't you use ormar yet?", 1),
(3, "Check this out, ormar now for free", 1),
]
@pytest.mark.asyncio
async def test_queryset_nested_relation_values_list():
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"),
(2, "Why don't you use ormar yet?", 1, 1, "News", 0, 1, 1, "Anonymous"),
(
3,
"Check this out, ormar now for free",
1,
1,
"News",
0,
1,
1,
"Anonymous",
),
]
@pytest.mark.asyncio
async def test_queryset_nested_relation_subset_of_fields_values_list():
async with base_ormar_config.database:
posts = await Post.objects.select_related("category__created_by").values_list(
["name", "category__name", "category__created_by__name"]
)
assert posts == [
("Ormar strikes again!", "News", "Anonymous"),
("Why don't you use ormar yet?", "News", "Anonymous"),
("Check this out, ormar now for free", "News", "Anonymous"),
]
@pytest.mark.asyncio
async def test_m2m_values():
async with base_ormar_config.database:
user = await User.objects.select_related("roles").values()
assert user == [
{
"id": 1,
"name": "Anonymous",
"roleuser__id": 1,
"roleuser__role": 1,
"roleuser__user": 1,
"roles__id": 1,
"roles__name": "admin",
},
{
"id": 1,
"name": "Anonymous",
"roleuser__id": 2,
"roleuser__role": 2,
"roleuser__user": 1,
"roles__id": 2,
"roles__name": "editor",
},
]
@pytest.mark.asyncio
async def test_nested_m2m_values():
async with base_ormar_config.database:
user = (
await Role.objects.select_related("users__categories")
.filter(name="admin")
.values()
)
assert user == [
{
"id": 1,
"name": "admin",
"roleuser__id": 1,
"roleuser__role": 1,
"roleuser__user": 1,
"users__id": 1,
"users__name": "Anonymous",
"users__categories__id": 1,
"users__categories__name": "News",
"users__categories__sort_order": 0,
"users__categories__created_by": 1,
}
]
@pytest.mark.asyncio
async def test_nested_m2m_values_without_through_explicit():
async with base_ormar_config.database:
user = (
await Role.objects.select_related("users__categories")
.filter(name="admin")
.fields({"name": ..., "users": {"name": ..., "categories": {"name"}}})
.exclude_fields("roleuser")
.values()
)
assert user == [
{
"name": "admin",
"users__name": "Anonymous",
"users__categories__name": "News",
}
]
@pytest.mark.asyncio
async def test_nested_m2m_values_without_through_param():
async with base_ormar_config.database:
user = (
await Role.objects.select_related("users__categories")
.filter(name="admin")
.fields({"name": ..., "users": {"name": ..., "categories": {"name"}}})
.values(exclude_through=True)
)
assert user == [
{
"name": "admin",
"users__name": "Anonymous",
"users__categories__name": "News",
}
]
@pytest.mark.asyncio
async def test_nested_m2m_values_no_through_and_m2m_models_but_keep_end_model():
async with base_ormar_config.database:
user = (
await Role.objects.select_related("users__categories")
.filter(name="admin")
.fields({"name": ..., "users": {"name": ..., "categories": {"name"}}})
.exclude_fields(["roleuser", "users"])
.values()
)
assert user == [{"name": "admin", "users__categories__name": "News"}]
@pytest.mark.asyncio
async def test_nested_flatten_and_exception():
async with base_ormar_config.database:
with pytest.raises(QueryDefinitionError):
(await Role.objects.fields({"name", "id"}).values_list(flatten=True))
roles = await Role.objects.fields("name").values_list(flatten=True)
assert roles == ["admin", "editor"]
@pytest.mark.asyncio
async def test_empty_result():
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 == []
@pytest.mark.asyncio
async def test_queryset_values_multiple_select_related():
async with base_ormar_config.database:
posts = (
await Category.objects.select_related(["created_by__roles", "posts"])
.filter(Category.created_by.roles.name == "editor")
.values(
["name", "posts__name", "created_by__name", "created_by__roles__name"],
exclude_through=True,
)
)
assert posts == [
{
"name": "News",
"created_by__name": "Anonymous",
"created_by__roles__name": "editor",
"posts__name": "Ormar strikes again!",
},
{
"name": "News",
"created_by__name": "Anonymous",
"created_by__roles__name": "editor",
"posts__name": "Why don't you use ormar yet?",
},
{
"name": "News",
"created_by__name": "Anonymous",
"created_by__roles__name": "editor",
"posts__name": "Check this out, ormar now for free",
},
]
@pytest.mark.asyncio
async def test_querysetproxy_values():
async with base_ormar_config.database:
role = (
await Role.objects.select_related("users__categories")
.filter(name="admin")
.get()
)
user = await role.users.values()
assert user == [
{
"id": 1,
"name": "Anonymous",
"roles__id": 1,
"roles__name": "admin",
"roleuser__id": 1,
"roleuser__role": 1,
"roleuser__user": 1,
}
]
user = (
await role.users.filter(name="Anonymous")
.select_related("categories")
.fields({"name": ..., "categories": {"name"}})
.values(exclude_through=True)
)
assert user == [{"name": "Anonymous", "categories__name": "News"}]
user = (
await role.users.filter(name="Anonymous")
.select_related("categories")
.fields({"name": ..., "categories": {"name"}})
.exclude_fields("roles")
.values(exclude_through=True)
)
assert user == [{"name": "Anonymous", "categories__name": "News"}]
@pytest.mark.asyncio
async def test_querysetproxy_values_list():
async with base_ormar_config.database:
role = (
await Role.objects.select_related("users__categories")
.filter(name="admin")
.get()
)
user = await role.users.values_list()
assert user == [(1, "Anonymous", 1, 1, 1, 1, "admin")]
user = (
await role.users.filter(name="Anonymous")
.select_related("categories")
.fields({"name": ..., "categories": {"name"}})
.values_list(exclude_through=True)
)
assert user == [("Anonymous", "News")]
user = (
await role.users.filter(name="Anonymous")
.select_related("categories")
.fields({"name": ..., "categories": {"name"}})
.exclude_fields("roles")
.values_list(exclude_through=True)
)
assert user == [("Anonymous", "News")]
user = (
await role.users.filter(name="Anonymous")
.select_related("categories")
.fields({"name"})
.exclude_fields("roles")
.values_list(exclude_through=True, flatten=True)
)
assert user == ["Anonymous"]
@pytest.mark.asyncio
async def test_filter_related_does_not_leak_columns_in_values_list():
async with base_ormar_config.database:
posts = await Post.objects.filter(category__id=1).fields(["name"]).values_list()
assert posts == [
("Ormar strikes again!",),
("Why don't you use ormar yet?",),
("Check this out, ormar now for free",),
]
@pytest.mark.asyncio
async def test_values_list_fields_kwarg_does_not_leak_related_columns():
async with base_ormar_config.database:
posts = await Post.objects.filter(category__id=1).values_list(fields=["name"])
assert posts == [
("Ormar strikes again!",),
("Why don't you use ormar yet?",),
("Check this out, ormar now for free",),
]
@pytest.mark.asyncio
async def test_filter_related_does_not_leak_columns_in_values():
async with base_ormar_config.database:
posts = await Post.objects.filter(category__id=1).fields(["name"]).values()
assert posts == [
{"name": "Ormar strikes again!"},
{"name": "Why don't you use ormar yet?"},
{"name": "Check this out, ormar now for free"},
]
@pytest.mark.asyncio
async def test_explicit_select_related_with_only_main_fields_excludes_related():
async with base_ormar_config.database:
posts = await Post.objects.select_related("category").fields(["name"]).values()
assert posts == [
{"name": "Ormar strikes again!"},
{"name": "Why don't you use ormar yet?"},
{"name": "Check this out, ormar now for free"},
]
@pytest.mark.asyncio
async def test_nested_fields_keep_referenced_relation():
async with base_ormar_config.database:
posts = await (
Post.objects.select_related("category")
.fields(["name", "category__name"])
.values()
)
assert posts == [
{"name": "Ormar strikes again!", "category__name": "News"},
{"name": "Why don't you use ormar yet?", "category__name": "News"},
{"name": "Check this out, ormar now for free", "category__name": "News"},
]
@pytest.mark.asyncio
async def test_m2m_relation_and_through_excluded_when_only_main_fields():
async with base_ormar_config.database:
roles = await (
Role.objects.select_related("users__categories").fields(["name"]).values()
)
assert roles == [{"name": "admin"}, {"name": "editor"}]
@pytest.mark.asyncio
async def test_relation_name_in_fields_keeps_relation_columns():
async with base_ormar_config.database:
posts = await (
Post.objects.select_related("category")
.fields({"name", "category"})
.values()
)
assert posts == [
{
"name": "Ormar strikes again!",
"category": 1,
"category__id": 1,
"category__name": "News",
"category__sort_order": 0,
"category__created_by": 1,
},
{
"name": "Why don't you use ormar yet?",
"category": 1,
"category__id": 1,
"category__name": "News",
"category__sort_order": 0,
"category__created_by": 1,
},
{
"name": "Check this out, ormar now for free",
"category": 1,
"category__id": 1,
"category__name": "News",
"category__sort_order": 0,
"category__created_by": 1,
},
]
@pytest.mark.asyncio
async def test_filter_related_without_fields_keeps_all_columns():
async with base_ormar_config.database:
posts = await Post.objects.filter(category__id=1).values()
assert posts == [
{
"id": 1,
"name": "Ormar strikes again!",
"category": 1,
"category__id": 1,
"category__name": "News",
"category__sort_order": 0,
"category__created_by": 1,
},
{
"id": 2,
"name": "Why don't you use ormar yet?",
"category": 1,
"category__id": 1,
"category__name": "News",
"category__sort_order": 0,
"category__created_by": 1,
},
{
"id": 3,
"name": "Check this out, ormar now for free",
"category": 1,
"category__id": 1,
"category__name": "News",
"category__sort_order": 0,
"category__created_by": 1,
},
]
@pytest.mark.asyncio
async def test_nested_only_fields_drops_main_columns_in_values():
async with base_ormar_config.database:
posts = await (
Post.objects.select_related("category").fields(["category__name"]).values()
)
assert posts == [
{"category__name": "News"},
{"category__name": "News"},
{"category__name": "News"},
]
@pytest.mark.asyncio
async def test_nested_only_fields_drops_main_columns_in_values_list():
async with base_ormar_config.database:
posts = await (
Post.objects.select_related("category")
.fields(["category__name"])
.values_list()
)
assert posts == [("News",), ("News",), ("News",)]
@pytest.mark.asyncio
async def test_nested_only_fields_flatten_returns_nested_value():
"""
Reproducer for issue #1060. Without the fix, flatten=True picks up
Post.id (the first leaked main-model column) instead of the nested
value the caller actually requested.
"""
async with base_ormar_config.database:
names = await (
Post.objects.select_related("category")
.fields(["category__name"])
.values_list(flatten=True)
)
assert names == ["News", "News", "News"]
@pytest.mark.asyncio
async def test_deeper_nested_only_fields_drops_main_columns():
async with base_ormar_config.database:
posts = await (
Post.objects.select_related("category__created_by")
.fields(["category__created_by__name"])
.values()
)
assert posts == [
{"category__created_by__name": "Anonymous"},
{"category__created_by__name": "Anonymous"},
{"category__created_by__name": "Anonymous"},
]
@pytest.mark.asyncio
async def test_main_referenced_keeps_main_columns():
async with base_ormar_config.database:
posts = await (
Post.objects.select_related("category")
.fields(["name", "category__name"])
.values()
)
assert posts == [
{"name": "Ormar strikes again!", "category__name": "News"},
{"name": "Why don't you use ormar yet?", "category__name": "News"},
{"name": "Check this out, ormar now for free", "category__name": "News"},
]
ormar-orm-ormar-a7e7fde/tests/test_relations/ 0000775 0000000 0000000 00000000000 15211567312 0021533 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_relations/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0023632 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_relations/test_cascades.py 0000664 0000000 0000000 00000007440 15211567312 0024717 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import pytest_asyncio
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Band(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="artists_x_bands")
id: int = ormar.Integer(primary_key=True)
class Artist(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="artists")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
bands = ormar.ManyToMany(Band, through=ArtistsBands)
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)
artist: Optional[Artist] = ormar.ForeignKey(Artist, ondelete="CASCADE")
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, ondelete="CASCADE")
title: str = ormar.String(max_length=100)
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(scope="function")
async def cleanup():
yield
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 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)
artists = await Artist.objects.all()
assert len(artists) == 0
albums = await Album.objects.all()
assert len(albums) == 0
@pytest.mark.asyncio
async def test_nested_cascade(cleanup):
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()
await Artist.objects.delete(id=artist.id)
artists = await Artist.objects.all()
assert len(artists) == 0
albums = await Album.objects.all()
assert len(albums) == 0
tracks = await Track.objects.all()
assert len(tracks) == 0
@pytest.mark.asyncio
async def test_many_to_many_cascade(cleanup):
async with base_ormar_config.database:
artist = await Artist(name="Dr Alban").save()
band = await Band(name="Scorpions").save()
await artist.bands.add(band)
check = await Artist.objects.select_related("bands").get()
assert check.bands[0].name == "Scorpions"
await Artist.objects.delete(id=artist.id)
artists = await Artist.objects.all()
assert len(artists) == 0
bands = await Band.objects.all()
assert len(bands) == 1
connections = await ArtistsBands.objects.all()
assert len(connections) == 0
@pytest.mark.asyncio
async def test_reverse_many_to_many_cascade(cleanup):
async with base_ormar_config.database:
artist = await Artist(name="Dr Alban").save()
band = await Band(name="Scorpions").save()
await artist.bands.add(band)
check = await Artist.objects.select_related("bands").get()
assert check.bands[0].name == "Scorpions"
await Band.objects.delete(id=band.id)
artists = await Artist.objects.all()
assert len(artists) == 1
connections = await ArtistsBands.objects.all()
assert len(connections) == 0
bands = await Band.objects.all()
assert len(bands) == 0
ormar-orm-ormar-a7e7fde/tests/test_relations/test_customizing_through_model_relation_names.py 0000664 0000000 0000000 00000004466 15211567312 0033531 0 ustar 00root root 0000000 0000000 import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Course(ormar.Model):
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):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
courses = ormar.ManyToMany(
Course,
through_relation_name="student_id",
through_reverse_relation_name="course_id",
)
create_test_database = init_tests(base_ormar_config)
def test_tables_columns():
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 base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
to_save = {
"course_name": "basic1",
"students": [{"name": "Jack"}, {"name": "Abi"}],
}
await Course(**to_save).save_related(follow=True, save_all=True)
course_check = await Course.objects.select_related("students").get()
assert course_check.course_name == "basic1"
assert course_check.students[0].name == "Jack"
assert course_check.students[1].name == "Abi"
students = await course_check.students.all()
assert len(students) == 2
student = await course_check.students.get(name="Jack")
assert student.name == "Jack"
students = await Student.objects.select_related("courses").all(
courses__course_name="basic1"
)
assert len(students) == 2
course_check = (
await Course.objects.select_related("students")
.order_by("students__name")
.get()
)
assert course_check.students[0].name == "Abi"
assert course_check.students[1].name == "Jack"
ormar-orm-ormar-a7e7fde/tests/test_relations/test_database_fk_creation.py 0000664 0000000 0000000 00000006105 15211567312 0027256 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import sqlalchemy
import ormar
from ormar.fields.foreign_key import validate_referential_action
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Artist(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="albums")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
artist: Optional[Artist] = ormar.ForeignKey(Artist, ondelete="CASCADE")
class A(ormar.Model):
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):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=64, nullalbe=False)
a: A = ormar.ForeignKey(to=A, ondelete=ormar.ReferentialAction.CASCADE)
class C(ormar.Model):
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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_simple_cascade():
async with base_ormar_config.engine.connect() as conn:
albums_columns = await conn.run_sync(
lambda sync_conn: sqlalchemy.inspect(sync_conn).get_columns("albums")
)
albums_fks = await conn.run_sync(
lambda sync_conn: sqlalchemy.inspect(sync_conn).get_foreign_keys("albums")
)
assert len(albums_columns) == 3
col_names = [col.get("name") for col in albums_columns]
assert sorted(["id", "name", "artist"]) == sorted(col_names)
assert len(albums_fks) == 1
assert albums_fks[0]["name"] == "fk_albums_artists_id_artist"
assert albums_fks[0]["constrained_columns"][0] == "artist"
assert albums_fks[0]["referred_columns"][0] == "id"
assert albums_fks[0]["options"].get("ondelete") == "CASCADE"
def test_validations_referential_action():
CASCADE = ormar.ReferentialAction.CASCADE.value
assert validate_referential_action(None) is None
assert validate_referential_action("cascade") == CASCADE
assert validate_referential_action(ormar.ReferentialAction.CASCADE) == CASCADE
with pytest.raises(ormar.ModelDefinitionError):
validate_referential_action("NOT VALID")
@pytest.mark.asyncio
async def test_cascade_clear():
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)
await C.objects.create(name="c", b=b)
await a.bs.clear(keep_reversed=False)
assert await B.objects.count() == 0
assert await C.objects.count() == 0
ormar-orm-ormar-a7e7fde/tests/test_relations/test_foreign_key_name_override.py 0000664 0000000 0000000 00000006773 15211567312 0030361 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import sqlalchemy
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="fkn_authors")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="fkn_books")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=100)
author: Optional[Author] = ormar.ForeignKey(
Author, foreign_key_name="my_custom_fk_books_author"
)
class Tag(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="fkn_tags")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class BookTag(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="fkn_book_tags")
id: int = ormar.Integer(primary_key=True)
class BookWithTags(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="fkn_books_with_tags")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=100)
tags = ormar.ManyToMany(
Tag,
through=BookTag,
through_foreign_key_name="my_fk_through_to_book",
through_reverse_foreign_key_name="my_fk_through_to_tag",
)
create_test_database = init_tests(base_ormar_config)
def _fk_names_for(table: sqlalchemy.Table) -> list:
names = []
for col in table.c:
for fk in col.foreign_keys:
names.append(fk.name)
return names
def test_foreign_key_name_overrides_generated_constraint_name():
names = _fk_names_for(Book.ormar_config.table)
assert "my_custom_fk_books_author" in names
assert not any(n.startswith("fk_fkn_books_") for n in names)
def test_foreign_key_name_default_is_generated():
fk_author = Author.ormar_config.model_fields.get("id")
assert (
fk_author.foreign_key_name is None
if hasattr(fk_author, "foreign_key_name")
else True
)
field = Book.ormar_config.model_fields["author"]
assert field.foreign_key_name == "my_custom_fk_books_author"
def test_many_to_many_through_foreign_key_name_overrides():
through_table = BookWithTags.ormar_config.model_fields[
"tags"
].through.ormar_config.table
names = _fk_names_for(through_table)
assert "my_fk_through_to_book" in names
assert "my_fk_through_to_tag" in names
@pytest.mark.asyncio
async def test_model_with_custom_fk_name_still_works_at_runtime():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
author = await Author.objects.create(name="J. Doe")
book = await Book.objects.create(title="Untitled", author=author)
fetched = await Book.objects.select_related("author").get(id=book.id)
assert fetched.author.name == "J. Doe"
@pytest.mark.asyncio
async def test_many_to_many_with_custom_through_fk_names_still_works():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
tag = await Tag.objects.create(name="scifi")
book = await BookWithTags.objects.create(title="Dune")
await book.tags.add(tag)
fetched = await BookWithTags.objects.select_related("tags").get(id=book.id)
assert len(fetched.tags) == 1
assert fetched.tags[0].name == "scifi"
ormar-orm-ormar-a7e7fde/tests/test_relations/test_foreign_keys.py 0000664 0000000 0000000 00000040114 15211567312 0025630 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from ormar.exceptions import MultipleMatches, NoMatch, RelationshipInstanceError
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)
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: Optional[int] = ormar.Integer(nullable=True, default=0)
is_disabled: bool = ormar.Boolean(default=False)
class Cover(ormar.Model):
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")
title: str = ormar.String(max_length=100)
class Organisation(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="org")
id: int = ormar.Integer(primary_key=True)
ident: str = ormar.String(max_length=100)
class Team(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="teams")
id: int = ormar.Integer(primary_key=True)
org: Optional[Organisation] = ormar.ForeignKey(Organisation)
name: str = ormar.String(max_length=100)
class Member(ormar.Model):
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)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_wrong_query_foreign_key_type():
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 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 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
@pytest.mark.asyncio
async def test_model_crud():
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)
track2 = Track(album=album, title="Heart don't stand a chance", position=2)
track3 = Track(album=album, title="The Waters", position=3)
await track1.save()
await track2.save()
await track3.save()
track = await Track.objects.get(title="The Bird")
assert track.album.pk == album.pk
assert isinstance(track.album, ormar.Model)
assert track.album.name is None
await track.album.load()
assert track.album.name == "Jamaica"
assert len(album.tracks) == 3
assert album.tracks[1].title == "Heart don't stand a chance"
album1 = await Album.objects.get(name="Jamaica")
assert album1.pk == album.pk
assert album1.tracks == []
await Track.objects.create(
album={"id": track.album.pk}, title="The Bird2", position=4
)
@pytest.mark.asyncio
async def test_select_related():
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)
track2 = Track(album=album, title="Heart don't stand a chance", position=2)
track3 = Track(album=album, title="The Waters", position=3)
await track1.save()
await track2.save()
await track3.save()
fantasies = Album(name="Fantasies")
await fantasies.save()
track4 = Track(album=fantasies, title="Help I'm Alive", position=1)
track5 = Track(album=fantasies, title="Sick Muse", position=2)
track6 = Track(album=fantasies, title="Satellite Mind", position=3)
await track4.save()
await track5.save()
await track6.save()
track = await Track.objects.select_related("album").get(title="The Bird")
assert track.album.name == "Malibu"
tracks = await Track.objects.select_related("album").all()
assert len(tracks) == 6
@pytest.mark.asyncio
async def test_model_removal_from_relations():
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)
track2 = Track(album=album, title="Superman", position=2)
track3 = Track(album=album, title="Wonder Woman", position=3)
await track1.save()
await track2.save()
await track3.save()
assert len(album.tracks) == 3
await album.tracks.remove(track1)
assert len(album.tracks) == 2
assert track1.album is None
await track1.update()
track1 = await Track.objects.get(title="The Birdman")
assert track1.album is None
await album.tracks.add(track1)
assert len(album.tracks) == 3
assert track1.album == album
await track1.update()
track1 = await Track.objects.select_related("album__tracks").get(
title="The Birdman"
)
album = await Album.objects.select_related("tracks").get(name="Chichi")
assert track1.album == album
track1.remove(album, name="album")
assert track1.album is None
assert len(album.tracks) == 2
track2.remove(album, name="album")
assert track2.album is None
assert len(album.tracks) == 1
@pytest.mark.asyncio
async def test_fk_filter():
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)
await Track.objects.create(
album=malibu, title="Heart don't stand a chance", position=2
)
await Track.objects.create(album=malibu, title="The Waters", position=3)
fantasies = await Album.objects.create(name="Fantasies")
await Track.objects.create(
album=fantasies, title="Help I'm Alive", position=1
)
await Track.objects.create(album=fantasies, title="Sick Muse", position=2)
await Track.objects.create(
album=fantasies, title="Satellite Mind", position=3
)
tracks = (
await Track.objects.select_related("album")
.filter(album__name="Fantasies")
.all()
)
assert len(tracks) == 3
for track in tracks:
assert track.album.name == "Fantasies"
tracks = (
await Track.objects.select_related("album")
.filter(album__name__icontains="fan")
.all()
)
assert len(tracks) == 3
for track in tracks:
assert track.album.name == "Fantasies"
tracks = await Track.objects.filter(album__name__contains="Fan").all()
assert len(tracks) == 3
for track in tracks:
assert track.album.name == "Fantasies"
tracks = await Track.objects.filter(album__name__contains="Malibu%").all()
assert len(tracks) == 3
tracks = (
await Track.objects.filter(album=malibu).select_related("album").all()
)
assert len(tracks) == 3
for track in tracks:
assert track.album.name == "Malibu%"
tracks = await Track.objects.select_related("album").all(album=malibu)
assert len(tracks) == 3
for track in tracks:
assert track.album.name == "Malibu%"
@pytest.mark.asyncio
async def test_multiple_fk():
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")
await Member.objects.create(team=red_team, email="a@example.org")
await Member.objects.create(team=red_team, email="b@example.org")
await Member.objects.create(team=blue_team, email="c@example.org")
await Member.objects.create(team=blue_team, email="d@example.org")
other = await Organisation.objects.create(ident="Other ltd")
team = await Team.objects.create(org=other, name="Green Team")
await Member.objects.create(team=team, email="e@example.org")
members = (
await Member.objects.select_related("team__org")
.filter(team__org__ident="ACME Ltd")
.all()
)
assert len(members) == 4
for member in members:
assert member.team.org.ident == "ACME Ltd"
@pytest.mark.asyncio
async def test_pk_filter():
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
)
await Track.objects.create(album=fantasies, title="Test2", position=2)
await Track.objects.create(album=fantasies, title="Test3", position=3)
tracks = (
await Track.objects.select_related("album").filter(pk=track.pk).all()
)
assert len(tracks) == 1
tracks = (
await Track.objects.select_related("album")
.filter(position=2, album__name="Test")
.all()
)
assert len(tracks) == 1
@pytest.mark.asyncio
async def test_limit_and_offset():
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
)
await Track.objects.create(album=fantasies, title="Sample2", position=2)
await Track.objects.create(album=fantasies, title="Sample3", position=3)
tracks = await Track.objects.limit(1).all()
assert len(tracks) == 1
assert tracks[0].title == "Sample"
tracks = await Track.objects.limit(1).offset(1).all()
assert len(tracks) == 1
assert tracks[0].title == "Sample2"
album = await Album.objects.select_related("tracks").limit(1).get()
assert len(album.tracks) == 3
assert album.tracks[0].title == "Sample"
album = (
await Album.objects.select_related("tracks")
.limit(1, limit_raw_sql=True)
.get()
)
assert len(album.tracks) == 1
assert album.tracks[0].title == "Sample"
@pytest.mark.asyncio
async def test_get_exceptions():
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):
await Album.objects.get(name="Test2")
await Track.objects.create(album=fantasies, title="Test1", position=1)
await Track.objects.create(album=fantasies, title="Test2", position=2)
await Track.objects.create(album=fantasies, title="Test3", position=3)
with pytest.raises(MultipleMatches):
await Track.objects.select_related("album").get(album=fantasies)
@pytest.mark.asyncio
async def test_wrong_model_passed_as_fk():
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)
@pytest.mark.asyncio
async def test_bulk_update_model_with_no_children():
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"])
updated_album = await Album.objects.get(id=album.id)
assert updated_album.name == "Test2"
@pytest.mark.asyncio
async def test_bulk_update_model_with_children():
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")
await Track.objects.create(
album=best_seller, title="t1", position=1, play_count=100
)
await Track.objects.create(
album=best_seller2, title="t2", position=1, play_count=100
)
await Track.objects.create(
album=not_best_seller, title="t3", position=1, play_count=3
)
await Track.objects.create(
album=best_seller, title="t4", position=1, play_count=500
)
tracks = (
await Track.objects.select_related("album")
.filter(play_count__gt=10)
.all()
)
best_seller_albums = {}
for track in tracks:
album = track.album
if album.id in best_seller_albums:
continue
album.is_best_seller = True
best_seller_albums[album.id] = album
await Album.objects.bulk_update(
best_seller_albums.values(), columns=["is_best_seller"]
)
best_seller_albums_db = await Album.objects.filter(
is_best_seller=True
).all()
assert len(best_seller_albums_db) == 2
@pytest.mark.asyncio
async def test_fk_update_to_none():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
album = await Album.objects.create(name="Limitless")
track = await Track.objects.create(album=album, title="Test1", position=1)
assert track.album.name == "Limitless"
track.album = None
assert track.album is None
updated_track = await track.update(_columns=["album"])
assert updated_track.album is None
reloaded = await Track.objects.get(id=track.id)
assert reloaded.album is None
ormar-orm-ormar-a7e7fde/tests/test_relations/test_m2m_through_fields.py 0000664 0000000 0000000 00000030526 15211567312 0026733 0 ustar 00root root 0000000 0000000 from typing import Any, ForwardRef, Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config(force_rollback=True)
class Category(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="categories")
id = ormar.Integer(primary_key=True)
name = ormar.String(max_length=40)
class PostCategory(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="posts_x_categories")
id: int = ormar.Integer(primary_key=True)
sort_order: Optional[int] = ormar.Integer(nullable=True)
param_name: str = ormar.String(default="Name", max_length=200)
class Blog(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
class Post(ormar.Model):
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)
blog = ormar.ForeignKey(Blog)
create_test_database = init_tests(base_ormar_config)
class PostCategory2(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="posts_x_categories2")
id: int = ormar.Integer(primary_key=True)
sort_order: Optional[int] = ormar.Integer(nullable=True)
class Post2(ormar.Model):
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=ForwardRef("PostCategory2"))
@pytest.mark.asyncio
async def test_forward_ref_is_updated():
async with base_ormar_config.database:
assert Post2.ormar_config.requires_ref_update
Post2.update_forward_refs()
assert Post2.ormar_config.model_fields["postcategory2"].to == PostCategory2
@pytest.mark.asyncio
async def test_setting_fields_on_through_model():
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)
assert hasattr(post.categories[0], "postcategory")
assert post.categories[0].postcategory is None
@pytest.mark.asyncio
async def test_setting_additional_fields_on_through_model_in_add():
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)
postcat = await PostCategory.objects.get()
assert postcat.sort_order == 1
@pytest.mark.asyncio
async def test_setting_additional_fields_on_through_model_in_create():
async with base_ormar_config.database:
post = await Post(title="Test post").save()
await post.categories.create(
name="Test category2", postcategory={"sort_order": 2}
)
postcat = await PostCategory.objects.get()
assert postcat.sort_order == 2
@pytest.mark.asyncio
async def test_getting_additional_fields_from_queryset() -> Any:
async with base_ormar_config.database:
post = await Post(title="Test post").save()
await post.categories.create(
name="Test category1", postcategory={"sort_order": 1}
)
await post.categories.create(
name="Test category2", postcategory={"sort_order": 2}
)
await post.categories.all()
assert post.postcategory is None
assert post.categories[0].postcategory.sort_order == 1
assert post.categories[1].postcategory.sort_order == 2
post2 = await Post.objects.select_related("categories").get(
categories__name="Test category2"
)
assert post2.categories[0].postcategory.sort_order == 2
@pytest.mark.asyncio
async def test_only_one_side_has_through() -> Any:
async with base_ormar_config.database:
post = await Post(title="Test post").save()
await post.categories.create(
name="Test category1", postcategory={"sort_order": 1}
)
await post.categories.create(
name="Test category2", postcategory={"sort_order": 2}
)
post2 = await Post.objects.select_related("categories").get()
assert post2.postcategory is None
assert post2.categories[0].postcategory is not None
await post2.categories.all()
assert post2.postcategory is None
assert post2.categories[0].postcategory is not None
categories = await Category.objects.select_related("posts").all()
assert isinstance(categories[0], Category)
assert categories[0].postcategory is None
assert categories[0].posts[0].postcategory is not None
@pytest.mark.asyncio
async def test_filtering_by_through_model() -> Any:
async with base_ormar_config.database:
post = await Post(title="Test post").save()
await post.categories.create(
name="Test category1",
postcategory={"sort_order": 1, "param_name": "volume"},
)
await post.categories.create(
name="Test category2", postcategory={"sort_order": 2, "param_name": "area"}
)
post2 = (
await Post.objects.select_related("categories")
.filter(postcategory__sort_order__gt=1)
.get()
)
assert len(post2.categories) == 1
assert post2.categories[0].postcategory.sort_order == 2
post3 = await Post.objects.filter(
categories__postcategory__param_name="volume"
).get()
assert len(post3.categories) == 1
assert post3.categories[0].postcategory.param_name == "volume"
@pytest.mark.asyncio
async def test_deep_filtering_by_through_model() -> Any:
async with base_ormar_config.database:
blog = await Blog(title="My Blog").save()
post = await Post(title="Test post", blog=blog).save()
await post.categories.create(
name="Test category1",
postcategory={"sort_order": 1, "param_name": "volume"},
)
await post.categories.create(
name="Test category2", postcategory={"sort_order": 2, "param_name": "area"}
)
blog2 = (
await Blog.objects.select_related("posts__categories")
.filter(posts__postcategory__sort_order__gt=1)
.get()
)
assert len(blog2.posts) == 1
assert len(blog2.posts[0].categories) == 1
assert blog2.posts[0].categories[0].postcategory.sort_order == 2
blog3 = await Blog.objects.filter(
posts__categories__postcategory__param_name="volume"
).get()
assert len(blog3.posts) == 1
assert len(blog3.posts[0].categories) == 1
assert blog3.posts[0].categories[0].postcategory.param_name == "volume"
@pytest.mark.asyncio
async def test_ordering_by_through_model() -> Any:
async with base_ormar_config.database:
post = await Post(title="Test post").save()
await post.categories.create(
name="Test category1",
postcategory={"sort_order": 2, "param_name": "volume"},
)
await post.categories.create(
name="Test category2", postcategory={"sort_order": 1, "param_name": "area"}
)
await post.categories.create(
name="Test category3",
postcategory={"sort_order": 3, "param_name": "velocity"},
)
post2 = (
await Post.objects.select_related("categories")
.order_by("-postcategory__sort_order")
.get()
)
assert len(post2.categories) == 3
assert post2.categories[0].name == "Test category3"
assert post2.categories[2].name == "Test category2"
post3 = (
await Post.objects.select_related("categories")
.order_by("categories__postcategory__param_name")
.get()
)
assert len(post3.categories) == 3
assert post3.categories[0].postcategory.param_name == "area"
assert post3.categories[2].postcategory.param_name == "volume"
@pytest.mark.asyncio
async def test_update_through_models_from_queryset_on_through() -> Any:
async with base_ormar_config.database:
post = await Post(title="Test post").save()
await post.categories.create(
name="Test category1",
postcategory={"sort_order": 2, "param_name": "volume"},
)
await post.categories.create(
name="Test category2", postcategory={"sort_order": 1, "param_name": "area"}
)
await post.categories.create(
name="Test category3",
postcategory={"sort_order": 3, "param_name": "velocity"},
)
await PostCategory.objects.filter(param_name="volume", post=post.id).update(
sort_order=4
)
post2 = (
await Post.objects.select_related("categories")
.order_by("-postcategory__sort_order")
.get()
)
assert len(post2.categories) == 3
assert post2.categories[0].postcategory.param_name == "volume"
assert post2.categories[2].postcategory.param_name == "area"
@pytest.mark.asyncio
async def test_update_through_model_after_load() -> Any:
async with base_ormar_config.database:
post = await Post(title="Test post").save()
await post.categories.create(
name="Test category1",
postcategory={"sort_order": 2, "param_name": "volume"},
)
post2 = await Post.objects.select_related("categories").get()
assert len(post2.categories) == 1
await post2.categories[0].postcategory.load()
await post2.categories[0].postcategory.update(sort_order=3)
post3 = await Post.objects.select_related("categories").get()
assert len(post3.categories) == 1
assert post3.categories[0].postcategory.sort_order == 3
@pytest.mark.asyncio
async def test_update_through_from_related() -> Any:
async with base_ormar_config.database:
post = await Post(title="Test post").save()
await post.categories.create(
name="Test category1",
postcategory={"sort_order": 2, "param_name": "volume"},
)
await post.categories.create(
name="Test category2", postcategory={"sort_order": 1, "param_name": "area"}
)
await post.categories.create(
name="Test category3",
postcategory={"sort_order": 3, "param_name": "velocity"},
)
await post.categories.filter(name="Test category3").update(
postcategory={"sort_order": 4}
)
post2 = (
await Post.objects.select_related("categories")
.order_by("postcategory__sort_order")
.get()
)
assert len(post2.categories) == 3
assert post2.categories[2].postcategory.sort_order == 4
@pytest.mark.asyncio
async def test_excluding_fields_on_through_model() -> Any:
async with base_ormar_config.database:
post = await Post(title="Test post").save()
await post.categories.create(
name="Test category1",
postcategory={"sort_order": 2, "param_name": "volume"},
)
await post.categories.create(
name="Test category2", postcategory={"sort_order": 1, "param_name": "area"}
)
await post.categories.create(
name="Test category3",
postcategory={"sort_order": 3, "param_name": "velocity"},
)
post2 = (
await Post.objects.select_related("categories")
.exclude_fields("postcategory__param_name")
.order_by("postcategory__sort_order")
.get()
)
assert len(post2.categories) == 3
assert post2.categories[0].postcategory.param_name is None
assert post2.categories[0].postcategory.sort_order == 1
assert post2.categories[2].postcategory.param_name is None
assert post2.categories[2].postcategory.sort_order == 3
post3 = (
await Post.objects.select_related("categories")
.fields({"postcategory": ..., "title": ...})
.exclude_fields({"postcategory": {"param_name", "sort_order"}})
.get()
)
assert len(post3.categories) == 3
for category in post3.categories:
assert category.postcategory.param_name is None
assert category.postcategory.sort_order is None
ormar-orm-ormar-a7e7fde/tests/test_relations/test_many_to_many.py 0000664 0000000 0000000 00000017564 15211567312 0025653 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import pytest_asyncio
import ormar
from ormar.exceptions import ModelPersistenceError, NoMatch, RelationshipInstanceError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config(force_rollback=True)
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=80)
last_name: str = ormar.String(max_length=80)
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=40)
class Post(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="posts")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[list[Category]] = ormar.ManyToMany(Category)
author: Optional[Author] = ormar.ForeignKey(Author)
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(scope="function")
async def cleanup():
yield
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)
await Author.objects.delete(each=True)
@pytest.mark.asyncio
async def test_not_saved_raises_error(cleanup):
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")
with pytest.raises(ModelPersistenceError):
await post.categories.add(news)
@pytest.mark.asyncio
async def test_not_existing_raises_error(cleanup):
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)
with pytest.raises(NoMatch):
await post.categories.get()
assert await post.categories.get_or_none() is None
@pytest.mark.asyncio
async def test_assigning_related_objects(cleanup):
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")
# Add a category to a post.
await post.categories.add(news)
# or from the other end:
await news.posts.add(post)
assert await post.categories.get_or_none(name="no exist") is None
assert await post.categories.get_or_none(name="News") == news
# Creating columns object from instance:
await post.categories.create(name="Tips")
assert len(post.categories) == 2
post_categories = await post.categories.all()
assert len(post_categories) == 2
@pytest.mark.asyncio
async def test_quering_of_the_m2m_models(cleanup):
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)
news = await Category.objects.create(name="News")
# tl;dr: `post.categories` exposes the QuerySet API.
await post.categories.add(news)
post_categories = await post.categories.all()
assert len(post_categories) == 1
assert news == await post.categories.get(name="News")
num_posts = await news.posts.count()
assert num_posts == 1
posts_about_m2m = await news.posts.filter(title__contains="M2M").all()
assert len(posts_about_m2m) == 1
assert posts_about_m2m[0] == post
posts_about_python = await Post.objects.filter(categories__name="python").all()
assert len(posts_about_python) == 0
# Traversal of relationships: which categories has Guido contributed to?
category = await Category.objects.filter(posts__author=guido).get()
assert category == news
# or:
category2 = await Category.objects.filter(
posts__author__first_name="Guido"
).get()
assert category2 == news
@pytest.mark.asyncio
async def test_removal_of_the_relations(cleanup):
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")
await post.categories.add(news)
assert len(await post.categories.all()) == 1
await post.categories.remove(news)
assert len(await post.categories.all()) == 0
# or:
await news.posts.add(post)
assert len(await news.posts.all()) == 1
await news.posts.remove(post)
assert len(await news.posts.all()) == 0
# Remove all columns objects:
await post.categories.add(news)
await post.categories.clear()
assert len(await post.categories.all()) == 0
# post would also lose 'news' category when running:
await post.categories.add(news)
await news.delete()
assert len(await post.categories.all()) == 0
@pytest.mark.asyncio
async def test_selecting_related(cleanup):
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")
recent = await Category.objects.create(name="Recent")
await post.categories.add(news)
await post.categories.add(recent)
assert len(await post.categories.all()) == 2
# Loads categories and posts (2 queries) and perform the join in Python.
categories = await Category.objects.select_related("posts").all()
# No extra queries needed => no more `await`s required.
for category in categories:
assert category.posts[0] == post
news_posts = await news.posts.select_related("author").all()
assert news_posts[0].author == guido
assert (await post.categories.limit(1).all())[0] == news
assert (await post.categories.offset(1).limit(1).all())[0] == recent
assert await post.categories.first() == news
assert await post.categories.exists()
@pytest.mark.asyncio
async def test_selecting_related_fail_without_saving(cleanup):
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):
await post.categories.all()
@pytest.mark.asyncio
async def test_adding_unsaved_related(cleanup):
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")
with pytest.raises(ModelPersistenceError):
await post.categories.add(news)
await news.save()
await post.categories.add(news)
assert len(await post.categories.all()) == 1
@pytest.mark.asyncio
async def test_removing_unsaved_related(cleanup):
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")
with pytest.raises(NoMatch):
await post.categories.remove(news)
ormar-orm-ormar-a7e7fde/tests/test_relations/test_postgress_select_related_with_limit.py 0000664 0000000 0000000 00000005752 15211567312 0032476 0 ustar 00root root 0000000 0000000 # Models
import uuid
from datetime import date
from enum import Enum
from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config(force_rollback=True)
class PrimaryKeyMixin:
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4)
class Level(Enum):
ADMIN = "0"
STAFF = "1"
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: 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)
ormar_config = base_ormar_config.copy(order_by=["-is_active", "-level"])
class Task(PrimaryKeyMixin, ormar.Model):
"""Task Model Class to Implement Method for Operations of Task Entity"""
name: str = ormar.String(max_length=64, nullalbe=False)
description: Optional[str] = ormar.Text(nullable=True, default=None)
start_date: Optional[date] = ormar.Date(nullable=True, default=None)
end_date: Optional[date] = ormar.Date(nullable=True, default=None)
is_halted: bool = ormar.Boolean(index=True, nullable=False, default=True)
user: User = ormar.ForeignKey(to=User)
ormar_config = base_ormar_config.copy(
order_by=["-end_date", "-start_date"],
constraints=[
ormar.UniqueColumns("user", "name"),
],
)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_selecting_related_with_limit():
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()
await Task(name="two", user=user1).save()
await Task(name="three", user=user2).save()
await Task(name="four", user=user2).save()
users = (
await User.objects.limit(2, limit_raw_sql=True)
.select_related(User.tasks)
.all()
)
users2 = (
await User.objects.select_related(User.tasks)
.limit(2, limit_raw_sql=True)
.all()
)
assert users == users2
assert len(users) == 1
assert len(users[0].tasks) == 2
users3 = await User.objects.limit(2).select_related(User.tasks).all()
users4 = await User.objects.select_related(User.tasks).limit(2).all()
assert users3 == users4
assert len(users3) == 2
assert len(users3[0].tasks) == 2
assert len(users3[1].tasks) == 2
ormar-orm-ormar-a7e7fde/tests/test_relations/test_prefetch_related.py 0000664 0000000 0000000 00000034411 15211567312 0026447 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config(force_rollback=True)
class RandomSet(ormar.Model):
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):
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)
rand_set: Optional[RandomSet] = ormar.ForeignKey(RandomSet)
class Division(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="divisions")
id: int = ormar.Integer(name="division_id", primary_key=True)
name: Optional[str] = ormar.String(max_length=100, nullable=True)
class Shop(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="shops")
id: int = ormar.Integer(primary_key=True)
name: Optional[str] = ormar.String(max_length=100, nullable=True)
division: Optional[Division] = ormar.ForeignKey(Division)
class AlbumShops(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="albums_x_shops")
class Album(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="albums")
id: int = ormar.Integer(primary_key=True)
name: Optional[str] = ormar.String(max_length=100, nullable=True)
shops: list[Shop] = ormar.ManyToMany(to=Shop, through=AlbumShops)
sides: list = ormar.JSON(default=list)
class Track(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="tracks")
id: int = ormar.Integer(name="track_id", primary_key=True)
album: Optional[Album] = ormar.ForeignKey(Album)
title: str = ormar.String(max_length=100)
position: int = ormar.Integer()
tonation: Optional[Tonation] = ormar.ForeignKey(Tonation, name="tonation_id")
class Cover(ormar.Model):
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", name="album_id"
)
title: str = ormar.String(max_length=100)
artist: Optional[str] = ormar.String(max_length=200, nullable=True)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_prefetch_related():
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")
await Track.objects.create(
album=album, title="The Bird", position=1, tonation=ton1
)
await Track.objects.create(
album=album,
title="Heart don't stand a chance",
position=2,
tonation=ton1,
)
await Track.objects.create(
album=album, title="The Waters", position=3, tonation=ton1
)
await Cover.objects.create(title="Cover1", album=album, artist="Artist 1")
await Cover.objects.create(title="Cover2", album=album, artist="Artist 2")
fantasies = Album(name="Fantasies")
await fantasies.save()
await Track.objects.create(
album=fantasies, title="Help I'm Alive", position=1
)
await Track.objects.create(album=fantasies, title="Sick Muse", position=2)
await Track.objects.create(
album=fantasies, title="Satellite Mind", position=3
)
await Cover.objects.create(
title="Cover3", album=fantasies, artist="Artist 3"
)
await Cover.objects.create(
title="Cover4", album=fantasies, artist="Artist 4"
)
album = (
await Album.objects.filter(name="Malibu")
.prefetch_related(["tracks__tonation", "cover_pictures"])
.get()
)
assert len(album.tracks) == 3
assert album.tracks[0].title == "The Bird"
assert len(album.cover_pictures) == 2
assert album.cover_pictures[0].title == "Cover1"
assert (
album.tracks[0].tonation.name
== album.tracks[2].tonation.name
== "B-mol"
)
albums = await Album.objects.prefetch_related("tracks").all()
assert len(albums[0].tracks) == 3
assert len(albums[1].tracks) == 3
assert albums[0].tracks[0].title == "The Bird"
assert albums[1].tracks[0].title == "Help I'm Alive"
track = await Track.objects.prefetch_related(["album__cover_pictures"]).get(
title="The Bird"
)
assert track.album.name == "Malibu"
assert len(track.album.cover_pictures) == 2
assert track.album.cover_pictures[0].artist == "Artist 1"
track = (
await Track.objects.prefetch_related(["album__cover_pictures"])
.exclude_fields("album__cover_pictures__artist")
.get(title="The Bird")
)
assert track.album.name == "Malibu"
assert len(track.album.cover_pictures) == 2
assert track.album.cover_pictures[0].artist is None
tracks = await Track.objects.prefetch_related("album").all()
assert len(tracks) == 6
@pytest.mark.asyncio
async def test_prefetch_related_with_many_to_many():
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)
album = Album(name="Malibu")
await album.save()
await album.shops.add(shop1)
await album.shops.add(shop2)
await Track.objects.create(album=album, title="The Bird", position=1)
await Track.objects.create(
album=album, title="Heart don't stand a chance", position=2
)
await Track.objects.create(album=album, title="The Waters", position=3)
await Cover.objects.create(title="Cover1", album=album, artist="Artist 1")
await Cover.objects.create(title="Cover2", album=album, artist="Artist 2")
track = await Track.objects.prefetch_related(
["album__cover_pictures", "album__shops__division"]
).get(title="The Bird")
assert track.album.name == "Malibu"
assert len(track.album.cover_pictures) == 2
assert track.album.cover_pictures[0].artist == "Artist 1"
assert len(track.album.shops) == 2
assert track.album.shops[0].name == "Shop 1"
assert track.album.shops[0].division.name == "Div 1"
album2 = Album(name="Malibu 2")
await album2.save()
await album2.shops.add(shop1)
await album2.shops.add(shop2)
await Track.objects.create(album=album2, title="The Bird 2", position=1)
tracks = await Track.objects.prefetch_related(["album__shops"]).all()
assert tracks[0].album.name == "Malibu"
assert tracks[0].album.shops[0].name == "Shop 1"
assert tracks[3].album.name == "Malibu 2"
assert tracks[3].album.shops[0].name == "Shop 1"
assert tracks[0].album.shops[0] == tracks[3].album.shops[0]
assert id(tracks[0].album.shops[0]) == id(tracks[3].album.shops[0])
tracks[0].album.shops[0].name = "Dummy"
assert tracks[0].album.shops[0].name == tracks[3].album.shops[0].name
@pytest.mark.asyncio
async def test_prefetch_related_empty():
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"
)
assert track.title == "The Bird"
assert track.album is None
@pytest.mark.asyncio
async def test_prefetch_related_with_select_related():
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)
album = Album(name="Malibu")
await album.save()
await album.shops.add(shop1)
await album.shops.add(shop2)
await Cover.objects.create(title="Cover1", album=album, artist="Artist 1")
await Cover.objects.create(title="Cover2", album=album, artist="Artist 2")
album = (
await Album.objects.select_related(["tracks", "shops"])
.filter(name="Malibu")
.prefetch_related(["cover_pictures", "shops__division"])
.first()
)
assert len(album.tracks) == 0
assert len(album.cover_pictures) == 2
assert album.shops[0].division.name == "Div 1"
rand_set = await RandomSet.objects.create(name="Rand 1")
ton1 = await Tonation.objects.create(name="B-mol", rand_set=rand_set)
await Track.objects.create(
album=album, title="The Bird", position=1, tonation=ton1
)
await Track.objects.create(
album=album,
title="Heart don't stand a chance",
position=2,
tonation=ton1,
)
await Track.objects.create(
album=album, title="The Waters", position=3, tonation=ton1
)
album = (
await Album.objects.select_related("tracks__tonation__rand_set")
.filter(name="Malibu")
.prefetch_related(["cover_pictures", "shops__division"])
.order_by(
["-shops__name", "-cover_pictures__artist", "shops__division__name"]
)
.get()
)
assert len(album.tracks) == 3
assert album.tracks[0].tonation == album.tracks[2].tonation == ton1
assert len(album.cover_pictures) == 2
assert album.cover_pictures[0].artist == "Artist 2"
assert len(album.shops) == 2
assert album.shops[0].name == "Shop 2"
assert album.shops[0].division.name == "Div 1"
track = (
await Track.objects.select_related("album")
.prefetch_related(["album__cover_pictures", "album__shops__division"])
.get(title="The Bird")
)
assert track.album.name == "Malibu"
assert len(track.album.cover_pictures) == 2
assert track.album.cover_pictures[0].artist == "Artist 1"
assert len(track.album.shops) == 2
assert track.album.shops[0].name == "Shop 1"
assert track.album.shops[0].division.name == "Div 1"
@pytest.mark.asyncio
async def test_prefetch_related_with_select_related_and_fields():
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)
album = Album(name="Malibu")
await album.save()
await album.shops.add(shop1)
await album.shops.add(shop2)
await Cover.objects.create(title="Cover1", album=album, artist="Artist 1")
await Cover.objects.create(title="Cover2", album=album, artist="Artist 2")
rand_set = await RandomSet.objects.create(name="Rand 1")
ton1 = await Tonation.objects.create(name="B-mol", rand_set=rand_set)
await Track.objects.create(
album=album, title="The Bird", position=1, tonation=ton1
)
await Track.objects.create(
album=album,
title="Heart don't stand a chance",
position=2,
tonation=ton1,
)
await Track.objects.create(
album=album, title="The Waters", position=3, tonation=ton1
)
album = (
await Album.objects.select_related("tracks__tonation__rand_set")
.filter(name="Malibu")
.prefetch_related(["cover_pictures", "shops__division"])
.exclude_fields({"shops": {"division": {"name"}}})
.get()
)
assert len(album.tracks) == 3
assert album.tracks[0].tonation == album.tracks[2].tonation == ton1
assert len(album.cover_pictures) == 2
assert album.cover_pictures[0].artist == "Artist 1"
assert len(album.shops) == 2
assert album.shops[0].name == "Shop 1"
assert album.shops[0].division.name is None
album = (
await Album.objects.select_related("tracks")
.filter(name="Malibu")
.prefetch_related(["cover_pictures", "shops__division"])
.fields(
{
"name": ...,
"shops": {"division"},
"cover_pictures": {"id": ..., "title": ...},
}
)
.exclude_fields({"shops": {"division": {"name"}}})
.get()
)
assert len(album.tracks) == 3
assert len(album.cover_pictures) == 2
assert album.cover_pictures[0].artist is None
assert album.cover_pictures[0].title is not None
assert len(album.shops) == 2
assert album.shops[0].name is None
assert album.shops[0].division is not None
assert album.shops[0].division.name is None
ormar-orm-ormar-a7e7fde/tests/test_relations/test_prefetch_related_multiple_models_relation.py 0000664 0000000 0000000 00000004724 15211567312 0033626 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class User(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="test_signup")
id: int = ormar.Integer(primary_key=True)
class Session(ormar.Model):
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)
some_text: str = ormar.Text()
some_other_text: Optional[str] = ormar.Text(nullable=True)
teacher: Optional[User] = ormar.ForeignKey(
User, nullable=True, related_name="teaching"
)
students: Optional[list[User]] = ormar.ManyToMany(
User, through=Signup, related_name="attending"
)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_add_students():
async with base_ormar_config.database:
for user_id in [1, 2, 3, 4, 5]:
await User.objects.create(name=f"User {user_id}")
for name, some_text, some_other_text in [
("Session 1", "Some text 1", "Some other text 1"),
("Session 2", "Some text 2", "Some other text 2"),
("Session 3", "Some text 3", "Some other text 3"),
("Session 4", "Some text 4", "Some other text 4"),
("Session 5", "Some text 5", "Some other text 5"),
]:
await Session(
name=name, some_text=some_text, some_other_text=some_other_text
).save()
s1 = await Session.objects.get(pk=1)
s2 = await Session.objects.get(pk=2)
users = {}
for i in range(1, 6):
user = await User.objects.get(pk=i)
users[f"user_{i}"] = user
if i % 2 == 0:
await s1.students.add(user)
else:
await s2.students.add(user)
assert len(s1.students) > 0
assert len(s2.students) > 0
user = await User.objects.select_related("attending").get(pk=1)
assert user.attending is not None
assert len(user.attending) > 0
query = Session.objects.prefetch_related(["students", "teacher"])
sessions = await query.all()
assert len(sessions) == 5
ormar-orm-ormar-a7e7fde/tests/test_relations/test_prefetch_related_with_same_models.py 0000664 0000000 0000000 00000007575 15211567312 0032065 0 ustar 00root root 0000000 0000000 from random import randint
from typing import ForwardRef, Optional
import pytest
from faker import Faker
import ormar
from ormar.relations.relation_proxy import RelationProxy
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
fake = Faker()
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=256)
class BookAuthor(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="book_authors")
id: int = ormar.Integer(primary_key=True)
class BookCoAuthor(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="book_co_authors")
id: int = ormar.Integer(primary_key=True)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=256)
description: Optional[str] = ormar.String(max_length=256, nullable=True)
authors: RelationProxy[Author] = ormar.ManyToMany(
Author, related_name="author_books", through=BookAuthor
)
co_authors: RelationProxy[Author] = ormar.ManyToMany(
Author, related_name="co_author_books", through=BookCoAuthor
)
class SelfRef(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="selfrefs")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
main_child = ormar.ForeignKey(to=ForwardRef("SelfRef"), related_name="parent")
children: RelationProxy["SelfRef"] = ormar.ManyToMany(ForwardRef("SelfRef"))
SelfRef.update_forward_refs()
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_prefetch_related_with_same_model_relations() -> None:
async with base_ormar_config.database:
for _ in range(6):
await Author.objects.create(name=fake.name())
book = await Book.objects.create(name=fake.sentence(nb_words=randint(1, 4)))
for i in range(1, 3):
await book.authors.add(await Author.objects.get(id=i))
for i in range(3, 6):
await book.co_authors.add(await Author.objects.get(id=i))
prefetch_result = await Book.objects.prefetch_related(
["authors", "co_authors"]
).all()
prefetch_dict_result = [x.model_dump() for x in prefetch_result if x.id == 1][0]
select_result = await Book.objects.select_related(
["authors", "co_authors"]
).all()
select_dict_result = [
x.model_dump(
exclude={
"authors": {"bookauthor": ...},
"co_authors": {"bookcoauthor": ...},
}
)
for x in select_result
if x.id == 1
][0]
assert prefetch_dict_result == select_dict_result
@pytest.mark.asyncio
async def test_prefetch_related_with_self_referencing() -> None:
async with base_ormar_config.database:
main_child = await SelfRef.objects.create(name="MainChild")
main = await SelfRef.objects.create(name="Main", main_child=main_child)
child1 = await SelfRef.objects.create(name="Child1")
child2 = await SelfRef.objects.create(name="Child2")
await main.children.add(child1)
await main.children.add(child2)
select_result = await SelfRef.objects.select_related(
["main_child", "children"]
).get(name="Main")
print(select_result.model_dump_json(indent=4))
prefetch_result = await SelfRef.objects.prefetch_related(
["main_child", "children"]
).get(name="Main")
assert prefetch_result.main_child.name == main_child.name
assert len(prefetch_result.children) == 2
assert prefetch_result.children[0].name == child1.name
assert prefetch_result.children[1].name == child2.name
ormar-orm-ormar-a7e7fde/tests/test_relations/test_python_style_relations.py 0000664 0000000 0000000 00000006133 15211567312 0027770 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import pytest_asyncio
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=80)
last_name: str = ormar.String(max_length=80)
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=40)
class Post(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="posts")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[list[Category]] = ormar.ManyToMany(Category)
author: Optional[Author] = ormar.ForeignKey(Author, related_name="author_posts")
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(scope="function")
async def cleanup():
yield
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)
await Author.objects.delete(each=True)
@pytest.mark.asyncio
async def test_selecting_related(cleanup):
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")
recent = await Category.objects.create(name="Recent")
await post.categories.add(news)
await post.categories.add(recent)
assert len(await post.categories.all()) == 2
# Loads categories and posts (2 queries) and perform the join in Python.
categories = await Category.objects.select_related(Category.posts).all()
assert len(categories) == 2
assert categories[0].name == "News"
news_posts = await news.posts.select_related(Post.author).all()
assert news_posts[0].author == guido
assert (await post.categories.limit(1).all())[0] == news
assert (await post.categories.offset(1).limit(1).all())[0] == recent
assert await post.categories.first() == news
assert await post.categories.exists()
author = await Author.objects.prefetch_related(
Author.author_posts.categories
).get()
assert len(author.author_posts) == 1
assert author.author_posts[0].title == "Hello, M2M"
assert author.author_posts[0].categories[0].name == "News"
assert author.author_posts[0].categories[1].name == "Recent"
post = await Post.objects.select_related([Post.author, Post.categories]).get()
assert len(post.categories) == 2
assert post.categories[0].name == "News"
assert post.categories[1].name == "Recent"
assert post.author.first_name == "Guido"
ormar-orm-ormar-a7e7fde/tests/test_relations/test_relations_default_exception.py 0000664 0000000 0000000 00000003030 15211567312 0030722 0 ustar 00root root 0000000 0000000 # type: ignore
from typing import Optional
import pytest
import ormar
from ormar.exceptions import ModelDefinitionError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=80)
last_name: str = ormar.String(max_length=80)
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=40)
create_test_database = init_tests(base_ormar_config)
def test_fk_error():
with pytest.raises(ModelDefinitionError):
class Post(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="posts")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[list[Category]] = ormar.ManyToMany(Category)
author: Optional[Author] = ormar.ForeignKey(Author, default="aa")
def test_m2m_error():
with pytest.raises(ModelDefinitionError):
class Post(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="posts")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[list[Category]] = ormar.ManyToMany(
Category, default="aa"
)
ormar-orm-ormar-a7e7fde/tests/test_relations/test_replacing_models_with_copy.py 0000664 0000000 0000000 00000002333 15211567312 0030541 0 ustar 00root root 0000000 0000000 from typing import Any, Optional, Union
import pytest
import ormar
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: Optional[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()
)
ormar-orm-ormar-a7e7fde/tests/test_relations/test_reverse_relation_preserves_validator.py 0000664 0000000 0000000 00000002255 15211567312 0032663 0 ustar 00root root 0000000 0000000 from typing import Optional, Union
import pytest_asyncio
from pydantic import field_validator
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=80)
@field_validator("name", mode="before")
@classmethod
def validate_name(cls, v: Union[str, list[str]]) -> str:
if isinstance(v, list):
v = " ".join(v)
return v
class Post(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
author: Optional[Author] = ormar.ForeignKey(Author)
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(scope="function", autouse=True)
async def cleanup():
yield
async with base_ormar_config.database:
await Post.objects.delete(each=True)
await Author.objects.delete(each=True)
def test_validator():
author = Author(name=["Test", "Author"])
assert author.name == "Test Author"
ormar-orm-ormar-a7e7fde/tests/test_relations/test_saving_related.py 0000664 0000000 0000000 00000003657 15211567312 0026146 0 ustar 00root root 0000000 0000000 from typing import Union
import pytest
import ormar
from ormar.exceptions import ModelPersistenceError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
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=50, unique=True, index=True)
code: int = ormar.Integer()
class Workshop(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="workshops")
id: int = ormar.Integer(primary_key=True)
topic: str = ormar.String(max_length=255, index=True)
category: Union[ormar.Model, Category] = ormar.ForeignKey(
Category, related_name="workshops", nullable=False
)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_model_relationship():
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()
assert ws.id == 1
assert ws.topic == "Topic 1"
assert ws.category.name == "Foo"
ws.topic = "Topic 2"
await ws.update()
assert ws.id == 1
assert ws.topic == "Topic 2"
assert ws.category.name == "Foo"
@pytest.mark.asyncio
async def test_model_relationship_with_not_saved():
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()
with pytest.raises(ModelPersistenceError):
await Workshop.objects.create(topic="Topic 1", category=cat)
ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/ 0000775 0000000 0000000 00000000000 15211567312 0024215 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0026314 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/dialect.py 0000664 0000000 0000000 00000000441 15211567312 0026173 0 ustar 00root root 0000000 0000000 """Helpers for selecting which schema tests run on which dialect."""
import os
import pytest
IS_SQLITE = os.getenv("DATABASE_URL", "sqlite:///test.db").startswith("sqlite")
requires_schemas = pytest.mark.skipif(
IS_SQLITE,
reason="SQLite does not support per-table schemas",
)
ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/test_aggregations_cross_schema.py 0000664 0000000 0000000 00000011312 15211567312 0033027 0 ustar 00root root 0000000 0000000 """Aggregate functions over cross-schema relations.
Exercises ``count`` / ``min`` / ``max`` / ``sum`` / ``avg`` against a related
model that lives in a different schema, including the variant that filters
through the relation. These hit the LIMIT / GROUP BY / ORDER BY paths that
previously broke when a join's source table carried a schema name.
"""
from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
from .dialect import requires_schemas
pytestmark = requires_schemas
base_ormar_config = create_config()
class AggAuthor(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors", schema="catalog_people")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class AggBook(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="books", schema="catalog_books")
id: int = ormar.Integer(primary_key=True)
author: Optional[AggAuthor] = ormar.ForeignKey(AggAuthor)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer()
ranking: int = ormar.Integer()
class AggReviewer(ormar.Model):
ormar_config = base_ormar_config.copy(
tablename="reviewers", schema="catalog_reviewers"
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
books: Optional[list[AggBook]] = ormar.ManyToMany(AggBook)
create_test_database = init_tests(base_ormar_config)
async def seed(): # pragma: no cover
a1 = await AggAuthor.objects.create(name="Author 1")
await AggBook.objects.create(title="B1", year=1920, ranking=3, author=a1)
await AggBook.objects.create(title="B2", year=1930, ranking=1, author=a1)
await AggBook.objects.create(title="B3", year=1923, ranking=5, author=a1)
return a1
@pytest.mark.asyncio
async def test_count_across_schema_via_select_related(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
await seed()
# Three rows on the joined side, distinct authors collapses to 1.
assert await AggAuthor.objects.select_related("aggbooks").count() == 1
assert (
await AggAuthor.objects.select_related("aggbooks").count(distinct=False)
== 3
)
@pytest.mark.asyncio
async def test_min_max_across_schema_via_related(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
await seed()
assert (
await AggAuthor.objects.select_related("aggbooks").min("aggbooks__year")
== 1920
)
assert (
await AggAuthor.objects.select_related("aggbooks").max("aggbooks__year")
== 1930
)
@pytest.mark.asyncio
async def test_sum_avg_across_schema_via_related(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
await seed()
assert (
await AggAuthor.objects.select_related("aggbooks").sum("aggbooks__ranking")
== 9
)
avg = await AggAuthor.objects.select_related("aggbooks").avg(
"aggbooks__ranking"
)
assert float(avg) == 3.0
@pytest.mark.asyncio
async def test_aggregate_with_filter_through_relation(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
await seed()
result = (
await AggAuthor.objects.select_related("aggbooks")
.filter(aggbooks__year__lt=1925)
.min("aggbooks__year")
)
assert result == 1920
result = (
await AggAuthor.objects.select_related("aggbooks")
.filter(aggbooks__year__lt=1925)
.sum("aggbooks__ranking")
)
assert result == 8
@pytest.mark.asyncio
async def test_aggregate_across_m2m_in_third_schema(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
author = await seed()
reviewer = await AggReviewer.objects.create(name="Reviewer 1")
# Span three schemas in a single query: reviewer -> through -> book.
for book in await AggBook.objects.filter(author=author).all():
await reviewer.books.add(book)
assert (
await AggReviewer.objects.select_related("books").max("books__year") == 1930
)
assert (
await AggReviewer.objects.select_related("books").count(distinct=False) == 3
)
ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/test_basic_schema.py 0000664 0000000 0000000 00000003455 15211567312 0030236 0 ustar 00root root 0000000 0000000 """End-to-end CRUD on two models that live in different schemas."""
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
from .dialect import requires_schemas
pytestmark = requires_schemas
base_ormar_config = create_config()
class HrEmployee(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="employees", schema="hr")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class ItEmployee(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="employees", schema="it")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
create_test_database = init_tests(base_ormar_config)
def test_table_objects_carry_their_schema(): # pragma: no cover
assert HrEmployee.ormar_config.table.schema == "hr"
assert ItEmployee.ormar_config.table.schema == "it"
@pytest.mark.asyncio
async def test_round_trip_in_each_schema(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
alice = await HrEmployee.objects.create(name="Alice")
bob = await ItEmployee.objects.create(name="Bob")
fetched_alice = await HrEmployee.objects.get(id=alice.id)
fetched_bob = await ItEmployee.objects.get(id=bob.id)
assert fetched_alice.name == "Alice"
assert fetched_bob.name == "Bob"
# Updating one schema's row does not touch the other's.
await fetched_alice.update(name="Alice in HR")
bob_again = await ItEmployee.objects.get(id=bob.id)
assert bob_again.name == "Bob"
await fetched_alice.delete()
assert await HrEmployee.objects.count() == 0
assert await ItEmployee.objects.count() == 1
ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/test_cross_schema_fk.py 0000664 0000000 0000000 00000010270 15211567312 0030757 0 ustar 00root root 0000000 0000000 """Foreign key from a model in schema B to a model in schema A."""
from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
from .dialect import requires_schemas
pytestmark = requires_schemas
base_ormar_config = create_config()
class XSUser(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="users", schema="public_app")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class XSAuditEvent(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="events", schema="audit_log")
id: int = ormar.Integer(primary_key=True)
actor: Optional[XSUser] = ormar.ForeignKey(XSUser)
message: str = ormar.String(max_length=200)
create_test_database = init_tests(base_ormar_config)
def test_fk_constraint_uses_schema_qualified_target(): # pragma: no cover
fks = list(XSAuditEvent.ormar_config.table.c["actor"].foreign_keys)
assert len(fks) == 1
target = fks[0].target_fullname
assert target.startswith("public_app.users.")
@pytest.mark.asyncio
async def test_cross_schema_fk_save_and_select_related(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
actor = await XSUser.objects.create(name="root")
await XSAuditEvent.objects.create(actor=actor, message="login")
events = await XSAuditEvent.objects.select_related("actor").all()
assert len(events) == 1
assert events[0].actor.name == "root"
@pytest.mark.asyncio
async def test_cross_schema_filter_through_relation(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
alice = await XSUser.objects.create(name="alice")
bob = await XSUser.objects.create(name="bob")
await XSAuditEvent.objects.create(actor=alice, message="ok")
await XSAuditEvent.objects.create(actor=bob, message="ok")
alice_events = await XSAuditEvent.objects.filter(actor__name="alice").all()
assert len(alice_events) == 1
@pytest.mark.asyncio
async def test_cross_schema_prefetch_forward_fk(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
alice = await XSUser.objects.create(name="alice")
await XSAuditEvent.objects.create(actor=alice, message="login")
await XSAuditEvent.objects.create(actor=alice, message="logout")
events = await XSAuditEvent.objects.prefetch_related("actor").all()
assert len(events) == 2
assert {e.actor.name for e in events} == {"alice"}
@pytest.mark.asyncio
async def test_cross_schema_prefetch_reverse_fk(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
alice = await XSUser.objects.create(name="alice")
bob = await XSUser.objects.create(name="bob")
await XSAuditEvent.objects.create(actor=alice, message="m1")
await XSAuditEvent.objects.create(actor=alice, message="m2")
await XSAuditEvent.objects.create(actor=bob, message="m3")
users = (
await XSUser.objects.prefetch_related("xsauditevents")
.order_by("name")
.all()
)
assert len(users) == 2
assert {e.message for e in users[0].xsauditevents} == {"m1", "m2"}
assert {e.message for e in users[1].xsauditevents} == {"m3"}
@pytest.mark.asyncio
async def test_cross_schema_filter_via_reverse_relation(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
alice = await XSUser.objects.create(name="alice")
bob = await XSUser.objects.create(name="bob")
await XSAuditEvent.objects.create(actor=alice, message="login")
await XSAuditEvent.objects.create(actor=bob, message="other")
users = await XSUser.objects.filter(xsauditevents__message="login").all()
assert [u.name for u in users] == ["alice"]
ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/test_cross_schema_m2m.py 0000664 0000000 0000000 00000011374 15211567312 0031060 0 ustar 00root root 0000000 0000000 """Many-to-many across schemas, including auto-generated through models."""
from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
from .dialect import requires_schemas
pytestmark = requires_schemas
base_ormar_config = create_config()
class M2MAuthor(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors", schema="people")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class M2MBook(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="books", schema="library")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
authors: Optional[list[M2MAuthor]] = ormar.ManyToMany(M2MAuthor)
create_test_database = init_tests(base_ormar_config)
def test_auto_through_model_inherits_owner_schema(): # pragma: no cover
book_field = M2MBook.ormar_config.model_fields["authors"]
through = book_field.through
assert through.ormar_config.schema == "library"
assert through.ormar_config.table.schema == "library"
@pytest.mark.asyncio
async def test_m2m_save_and_prefetch_across_schemas(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
alice = await M2MAuthor.objects.create(name="Alice")
bob = await M2MAuthor.objects.create(name="Bob")
book = await M2MBook.objects.create(title="Coauthored")
await book.authors.add(alice)
await book.authors.add(bob)
prefetched = await M2MBook.objects.prefetch_related("authors").get(id=book.id)
names = sorted(a.name for a in prefetched.authors)
assert names == ["Alice", "Bob"]
@pytest.mark.asyncio
async def test_m2m_select_related_across_schemas(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
alice = await M2MAuthor.objects.create(name="Alice")
bob = await M2MAuthor.objects.create(name="Bob")
book = await M2MBook.objects.create(title="Coauthored")
await book.authors.add(alice)
await book.authors.add(bob)
loaded = await M2MBook.objects.select_related("authors").get(id=book.id)
names = sorted(a.name for a in loaded.authors)
assert names == ["Alice", "Bob"]
@pytest.mark.asyncio
async def test_m2m_filter_through_implicit_join(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
alice = await M2MAuthor.objects.create(name="Alice")
bob = await M2MAuthor.objects.create(name="Bob")
b1 = await M2MBook.objects.create(title="Solo Alice")
b2 = await M2MBook.objects.create(title="Solo Bob")
await b1.authors.add(alice)
await b2.authors.add(bob)
# Implicit join from M2MBook through the m2m relation to M2MAuthor —
# exercises the cross-schema through table.
alice_books = await M2MBook.objects.filter(authors__name="Alice").all()
assert [b.title for b in alice_books] == ["Solo Alice"]
@pytest.mark.asyncio
async def test_m2m_filter_via_reverse_relation(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
alice = await M2MAuthor.objects.create(name="Alice")
bob = await M2MAuthor.objects.create(name="Bob")
b1 = await M2MBook.objects.create(title="Hit")
b2 = await M2MBook.objects.create(title="Miss")
await b1.authors.add(alice)
await b2.authors.add(bob)
# Reverse side: filter authors by the books they wrote.
hit_authors = await M2MAuthor.objects.filter(m2mbooks__title="Hit").all()
assert [a.name for a in hit_authors] == ["Alice"]
@pytest.mark.asyncio
async def test_m2m_reverse_prefetch_across_schemas(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
alice = await M2MAuthor.objects.create(name="Alice")
bob = await M2MAuthor.objects.create(name="Bob")
b1 = await M2MBook.objects.create(title="A1")
b2 = await M2MBook.objects.create(title="A2")
b3 = await M2MBook.objects.create(title="B1")
await b1.authors.add(alice)
await b2.authors.add(alice)
await b3.authors.add(bob)
authors = (
await M2MAuthor.objects.prefetch_related("m2mbooks").order_by("name").all()
)
assert {b.title for b in authors[0].m2mbooks} == {"A1", "A2"}
assert {b.title for b in authors[1].m2mbooks} == {"B1"}
ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/test_filter_across_schema.py 0000664 0000000 0000000 00000002674 15211567312 0032016 0 ustar 00root root 0000000 0000000 """Filtering through a relation that crosses schema boundaries."""
from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
from .dialect import requires_schemas
pytestmark = requires_schemas
base_ormar_config = create_config()
class FAUser(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="users", schema="accounts")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class FAOrder(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="orders", schema="commerce")
id: int = ormar.Integer(primary_key=True)
user: Optional[FAUser] = ormar.ForeignKey(FAUser)
total: int = ormar.Integer()
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_filter_by_related_field_across_schema(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
alice = await FAUser.objects.create(name="alice")
bob = await FAUser.objects.create(name="bob")
await FAOrder.objects.create(user=alice, total=100)
await FAOrder.objects.create(user=bob, total=200)
await FAOrder.objects.create(user=alice, total=50)
alice_orders = await FAOrder.objects.filter(user__name="alice").all()
assert {o.total for o in alice_orders} == {100, 50}
ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/test_helpers.py 0000664 0000000 0000000 00000006026 15211567312 0027274 0 ustar 00root root 0000000 0000000 """
Unit tests for the schema-aware helpers introduced for per-model schemas.
These run on every dialect because they exercise the helper functions directly,
not via DDL — so SQLite test runs still cover both branches of the helpers.
"""
import pytest
import sqlalchemy
import ormar
from ormar.exceptions import ModelDefinitionError
from ormar.models.helpers.sqlalchemy import (
qualified_fk_reference,
validate_cross_schema_constraints,
)
from tests.settings import create_config
base_ormar_config = create_config()
class HelperUserNoSchema(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="helper_users_plain")
id: int = ormar.Integer(primary_key=True)
class HelperUserAudit(ormar.Model):
ormar_config = base_ormar_config.copy(
tablename="helper_users_audit", schema="audit"
)
id: int = ormar.Integer(primary_key=True)
def test_qualified_fk_reference_without_schema():
assert qualified_fk_reference(HelperUserNoSchema, "id") == "helper_users_plain.id"
def test_qualified_fk_reference_with_schema():
assert (
qualified_fk_reference(HelperUserAudit, "id") == "audit.helper_users_audit.id"
)
def test_validate_cross_schema_returns_early_on_postgres():
metadata = sqlalchemy.MetaData()
sqlalchemy.Table(
"parent",
metadata,
sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
schema="a",
)
sqlalchemy.Table(
"child",
metadata,
sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
sqlalchemy.Column(
"parent_id", sqlalchemy.Integer, sqlalchemy.ForeignKey("a.parent.id")
),
schema="b",
)
# Non-sqlite dialects skip the validation entirely.
validate_cross_schema_constraints(metadata, "postgresql")
validate_cross_schema_constraints(metadata, "mysql")
def test_validate_cross_schema_passes_when_schemas_match():
metadata = sqlalchemy.MetaData()
sqlalchemy.Table(
"parent",
metadata,
sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
)
sqlalchemy.Table(
"child",
metadata,
sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
sqlalchemy.Column(
"parent_id", sqlalchemy.Integer, sqlalchemy.ForeignKey("parent.id")
),
)
validate_cross_schema_constraints(metadata, "sqlite")
def test_validate_cross_schema_raises_on_sqlite_when_schemas_differ():
metadata = sqlalchemy.MetaData()
sqlalchemy.Table(
"parent",
metadata,
sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
schema="a",
)
sqlalchemy.Table(
"child",
metadata,
sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
sqlalchemy.Column(
"parent_id", sqlalchemy.Integer, sqlalchemy.ForeignKey("a.parent.id")
),
schema="b",
)
with pytest.raises(ModelDefinitionError, match="across schemas"):
validate_cross_schema_constraints(metadata, "sqlite")
ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/test_multi_fk_cross_schema.py 0000664 0000000 0000000 00000003063 15211567312 0032173 0 ustar 00root root 0000000 0000000 """Two foreign keys from one model into the same schema-qualified target."""
from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
from .dialect import requires_schemas
pytestmark = requires_schemas
base_ormar_config = create_config()
class MFUser(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="users", schema="core")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class MFTicket(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="tickets", schema="support")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=100)
created_by: Optional[MFUser] = ormar.ForeignKey(MFUser, related_name="created")
updated_by: Optional[MFUser] = ormar.ForeignKey(MFUser, related_name="updated")
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_two_fks_to_same_target_resolve_independently(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
alice = await MFUser.objects.create(name="alice")
bob = await MFUser.objects.create(name="bob")
await MFTicket.objects.create(title="t1", created_by=alice, updated_by=bob)
ticket = await MFTicket.objects.select_related(
["created_by", "updated_by"]
).get()
assert ticket.created_by.name == "alice"
assert ticket.updated_by.name == "bob"
ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/test_same_table_diff_schema.py 0000664 0000000 0000000 00000002573 15211567312 0032241 0 ustar 00root root 0000000 0000000 """Two models share a tablename but live in distinct schemas."""
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
from .dialect import requires_schemas
pytestmark = requires_schemas
base_ormar_config = create_config()
class HrPerson(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="people", schema="dept_hr")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class FinancePerson(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="people", schema="dept_fin")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
create_test_database = init_tests(base_ormar_config)
def test_metadata_keys_are_schema_qualified(): # pragma: no cover
keys = set(base_ormar_config.metadata.tables.keys())
assert "dept_hr.people" in keys
assert "dept_fin.people" in keys
@pytest.mark.asyncio
async def test_independent_rows_in_each_schema(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
await HrPerson.objects.create(name="Alice")
await FinancePerson.objects.create(name="Bob")
assert (await HrPerson.objects.get()).name == "Alice"
assert (await FinancePerson.objects.get()).name == "Bob"
ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/test_schema_inheritance.py 0000664 0000000 0000000 00000001767 15211567312 0031452 0 ustar 00root root 0000000 0000000 """
Verifies how OrmarConfig.copy() handles the schema parameter.
Runs on every dialect — this only exercises Python attribute behaviour.
"""
from tests.settings import create_config
def test_copy_inherits_schema_by_default():
base = create_config()
child = base.copy(tablename="t1", schema="hr")
grandchild = child.copy(tablename="t2")
assert grandchild.schema == "hr"
def test_copy_with_explicit_none_clears_schema():
base = create_config()
child = base.copy(tablename="t1", schema="hr")
grandchild = child.copy(tablename="t2", schema=None)
assert grandchild.schema is None
def test_copy_with_explicit_value_overrides_schema():
base = create_config()
child = base.copy(tablename="t1", schema="hr")
grandchild = child.copy(tablename="t2", schema="it")
assert grandchild.schema == "it"
def test_default_config_has_no_schema():
base = create_config()
assert base.schema is None
child = base.copy(tablename="t1")
assert child.schema is None
ormar-orm-ormar-a7e7fde/tests/test_relations/test_schemas/test_self_join_cross_schema.py 0000664 0000000 0000000 00000002511 15211567312 0032326 0 ustar 00root root 0000000 0000000 """Self-referential foreign key on a model that lives in a non-default schema."""
from typing import ForwardRef
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
from .dialect import requires_schemas
pytestmark = requires_schemas
base_ormar_config = create_config()
class OrgNode(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="org_nodes", schema="org_tree")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
parent = ormar.ForeignKey(ForwardRef("OrgNode"), related_name="children")
OrgNode.update_forward_refs()
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_self_reference_across_same_schema(): # pragma: no cover
async with (
base_ormar_config.database,
base_ormar_config.database.transaction(force_rollback=True),
):
ceo = await OrgNode.objects.create(name="CEO")
cto = await OrgNode.objects.create(name="CTO", parent=ceo)
await OrgNode.objects.create(name="Eng Lead", parent=cto)
chain = (
await OrgNode.objects.select_related("parent__parent")
.filter(name="Eng Lead")
.get()
)
assert chain.parent.name == "CTO"
assert chain.parent.parent.name == "CEO"
ormar-orm-ormar-a7e7fde/tests/test_relations/test_select_related_with_limit.py 0000664 0000000 0000000 00000010642 15211567312 0030357 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Keyword(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="primary_models_keywords")
id: int = ormar.Integer(primary_key=True)
class PrimaryModel(ormar.Model):
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)
some_text: str = ormar.Text()
some_other_text: Optional[str] = ormar.Text(nullable=True)
keywords: Optional[list[Keyword]] = ormar.ManyToMany(
Keyword, through=KeywordPrimaryModel
)
class SecondaryModel(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="secondary_models")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
primary_model: PrimaryModel = ormar.ForeignKey(
PrimaryModel, related_name="secondary_models"
)
@pytest.mark.asyncio
async def test_create_primary_models():
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"),
("Primary 3", "Some text 3", "Some other text 3"),
("Primary 4", "Some text 4", "Some other text 4"),
("Primary 5", "Some text 5", "Some other text 5"),
("Primary 6", "Some text 6", "Some other text 6"),
("Primary 7", "Some text 7", "Some other text 7"),
("Primary 8", "Some text 8", "Some other text 8"),
("Primary 9", "Some text 9", "Some other text 9"),
("Primary 10", "Some text 10", "Some other text 10"),
]:
await PrimaryModel(
name=name, some_text=some_text, some_other_text=some_other_text
).save()
for tag_id in [1, 2, 3, 4, 5]:
await Keyword.objects.create(name=f"Tag {tag_id}")
p1 = await PrimaryModel.objects.get(pk=1)
p2 = await PrimaryModel.objects.get(pk=2)
for i in range(1, 6):
keyword = await Keyword.objects.get(pk=i)
if i % 2 == 0:
await p1.keywords.add(keyword)
else:
await p2.keywords.add(keyword)
models = await PrimaryModel.objects.select_related("keywords").limit(5).all()
assert len(models) == 5
assert len(models[0].keywords) == 2
assert len(models[1].keywords) == 3
assert len(models[2].keywords) == 0
models2 = (
await PrimaryModel.objects.select_related("keywords")
.limit(5)
.offset(3)
.all()
)
assert len(models2) == 5
assert [x.name for x in models2] != [x.name for x in models]
assert [x.name for x in models2] == [
"Primary 4",
"Primary 5",
"Primary 6",
"Primary 7",
"Primary 8",
]
models3 = (
await PrimaryModel.objects.select_related("keywords")
.limit(5, limit_raw_sql=True)
.all()
)
assert len(models3) == 2
assert len(models3[0].keywords) == 2
assert len(models3[1].keywords) == 3
models4 = (
await PrimaryModel.objects.offset(1)
.select_related("keywords")
.limit(5, limit_raw_sql=True)
.all()
)
assert len(models4) == 3
assert [x.name for x in models4] == ["Primary 1", "Primary 2", "Primary 3"]
assert len(models4[0].keywords) == 1
assert len(models4[1].keywords) == 3
assert len(models4[2].keywords) == 0
models5 = (
await PrimaryModel.objects.select_related("keywords")
.offset(2, limit_raw_sql=True)
.limit(5)
.all()
)
assert len(models5) == 3
assert [x.name for x in models5] == ["Primary 2", "Primary 3", "Primary 4"]
assert len(models5[0].keywords) == 3
assert len(models5[1].keywords) == 0
assert len(models5[2].keywords) == 0
create_test_database = init_tests(base_ormar_config)
ormar-orm-ormar-a7e7fde/tests/test_relations/test_select_related_with_m2m_and_pk_name_set.py 0000664 0000000 0000000 00000007363 15211567312 0033131 0 ustar 00root root 0000000 0000000 # type: ignore
from datetime import date
from typing import Optional, Union
import pytest
import sqlalchemy
import ormar
from ormar import ModelDefinitionError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Role(ormar.Model):
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")
description: str = ormar.Text()
class Company(ormar.Model):
ormar_config = base_ormar_config.copy()
name: str = ormar.String(primary_key=True, max_length=1000)
class UserRoleCompany(ormar.Model):
ormar_config = base_ormar_config.copy()
class User(ormar.Model):
ormar_config = base_ormar_config.copy()
registrationnumber: str = ormar.String(primary_key=True, max_length=1000)
company: Company = ormar.ForeignKey(Company)
company2: Company = ormar.ForeignKey(Company, related_name="secondary_users")
name: str = ormar.Text()
role: Optional[Role] = ormar.ForeignKey(Role)
roleforcompanies: Optional[Union[Company, list[Company]]] = ormar.ManyToMany(
Company, through=UserRoleCompany, related_name="role_users"
)
lastupdate: date = ormar.DateTime(server_default=sqlalchemy.func.now())
create_test_database = init_tests(base_ormar_config)
def test_wrong_model():
with pytest.raises(ModelDefinitionError):
class User(ormar.Model):
ormar_config = base_ormar_config.copy()
registrationnumber: str = ormar.Text(primary_key=True)
company: Company = ormar.ForeignKey(Company)
company2: Company = ormar.ForeignKey(Company)
@pytest.mark.asyncio
async def test_create_primary_models():
async with base_ormar_config.database:
await Role.objects.create(
name="user", order=0, description="no administration right"
)
role_1 = await Role.objects.create(
name="admin", order=1, description="standard administration right"
)
await Role.objects.create(
name="super_admin", order=2, description="super administration right"
)
assert await Role.objects.count() == 3
company_0 = await Company.objects.create(name="Company")
company_1 = await Company.objects.create(name="Subsidiary Company 1")
company_2 = await Company.objects.create(name="Subsidiary Company 2")
company_3 = await Company.objects.create(name="Subsidiary Company 3")
assert await Company.objects.count() == 4
user = await User.objects.create(
registrationnumber="00-00000", company=company_0, name="admin", role=role_1
)
assert await User.objects.count() == 1
await user.delete()
assert await User.objects.count() == 0
user = await User.objects.create(
registrationnumber="00-00000",
company=company_0,
company2=company_3,
name="admin",
role=role_1,
)
await user.roleforcompanies.add(company_1)
await user.roleforcompanies.add(company_2)
users = await User.objects.select_related(
["company", "company2", "roleforcompanies"]
).all()
assert len(users) == 1
assert len(users[0].roleforcompanies) == 2
assert len(users[0].roleforcompanies[0].role_users) == 1
assert users[0].company.name == "Company"
assert len(users[0].company.users) == 1
assert users[0].company2.name == "Subsidiary Company 3"
assert len(users[0].company2.secondary_users) == 1
users = await User.objects.select_related("roleforcompanies").all()
assert len(users) == 1
assert len(users[0].roleforcompanies) == 2
ormar-orm-ormar-a7e7fde/tests/test_relations/test_selecting_proper_table_prefix.py 0000664 0000000 0000000 00000004446 15211567312 0031244 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class User(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="test_signup")
id: int = ormar.Integer(primary_key=True)
class Session(ormar.Model):
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)
some_text: str = ormar.Text()
some_other_text: Optional[str] = ormar.Text(nullable=True)
students: Optional[list[User]] = ormar.ManyToMany(User, through=Signup)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_list_sessions_for_user():
async with base_ormar_config.database:
for user_id in [1, 2, 3, 4, 5]:
await User.objects.create(name=f"User {user_id}")
for name, some_text, some_other_text in [
("Session 1", "Some text 1", "Some other text 1"),
("Session 2", "Some text 2", "Some other text 2"),
("Session 3", "Some text 3", "Some other text 3"),
("Session 4", "Some text 4", "Some other text 4"),
("Session 5", "Some text 5", "Some other text 5"),
]:
await Session(
name=name, some_text=some_text, some_other_text=some_other_text
).save()
s1 = await Session.objects.get(pk=1)
s2 = await Session.objects.get(pk=2)
users = {}
for i in range(1, 6):
user = await User.objects.get(pk=i)
users[f"user_{i}"] = user
if i % 2 == 0:
await s1.students.add(user)
else:
await s2.students.add(user)
assert len(s1.students) == 2
assert len(s2.students) == 3
assert [x.pk for x in s1.students] == [2, 4]
assert [x.pk for x in s2.students] == [1, 3, 5]
user = await User.objects.select_related("sessions").get(pk=1)
assert user.sessions is not None
assert len(user.sessions) > 0
ormar-orm-ormar-a7e7fde/tests/test_relations/test_skipping_reverse.py 0000664 0000000 0000000 00000015724 15211567312 0026534 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import pytest_asyncio
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Author(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
first_name: str = ormar.String(max_length=80)
last_name: str = ormar.String(max_length=80)
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=40)
class Post(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
categories: Optional[list[Category]] = ormar.ManyToMany(Category, skip_reverse=True)
author: Optional[Author] = ormar.ForeignKey(Author, skip_reverse=True)
create_test_database = init_tests(base_ormar_config)
@pytest_asyncio.fixture(scope="function")
async def cleanup():
yield
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)
await Author.objects.delete(each=True)
def test_model_definition():
category = Category(name="Test")
author = Author(first_name="Test", last_name="Author")
post = Post(title="Test Post", author=author)
post.categories = category
assert post.categories[0] == category
assert post.author == author
with pytest.raises(AttributeError):
assert author.posts
with pytest.raises(AttributeError):
assert category.posts
assert "posts" not in category._orm
@pytest.mark.asyncio
async def test_assigning_related_objects(cleanup):
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")
# Add a category to a post.
await post.categories.add(news)
# other way is disabled
with pytest.raises(AttributeError):
await news.posts.add(post)
assert await post.categories.get_or_none(name="no exist") is None
assert await post.categories.get_or_none(name="News") == news
# Creating columns object from instance:
await post.categories.create(name="Tips")
assert len(post.categories) == 2
post_categories = await post.categories.all()
assert len(post_categories) == 2
category = await Category.objects.select_related("posts").get(name="News")
with pytest.raises(AttributeError):
assert category.posts
@pytest.mark.asyncio
async def test_quering_of_related_model_works_but_no_result(cleanup):
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")
await post.categories.add(news)
post_categories = await post.categories.all()
assert len(post_categories) == 1
assert "posts" not in post.model_dump().get("categories", [])[0]
assert news == await post.categories.get(name="News")
posts_about_python = await Post.objects.filter(categories__name="python").all()
assert len(posts_about_python) == 0
# relation not in dict
category = (
await Category.objects.select_related("posts")
.filter(posts__author=guido)
.get()
)
assert category == news
assert "posts" not in category.model_dump()
# relation not in json
category2 = (
await Category.objects.select_related("posts")
.filter(posts__author__first_name="Guido")
.get()
)
assert category2 == news
assert "posts" not in category2.model_dump_json()
assert "posts" not in Category.model_json_schema().get("properties")
@pytest.mark.asyncio
async def test_removal_of_the_relations(cleanup):
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")
await post.categories.add(news)
assert len(await post.categories.all()) == 1
await post.categories.remove(news)
assert len(await post.categories.all()) == 0
with pytest.raises(AttributeError):
await news.posts.add(post)
with pytest.raises(AttributeError):
await news.posts.remove(post)
await post.categories.add(news)
await post.categories.clear()
assert len(await post.categories.all()) == 0
await post.categories.add(news)
await news.delete()
assert len(await post.categories.all()) == 0
@pytest.mark.asyncio
async def test_selecting_related(cleanup):
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"
)
post = await Post.objects.create(title="Hello, M2M", author=guido)
post2 = await Post.objects.create(title="Bye, M2M", author=guido2)
news = await Category.objects.create(name="News")
recent = await Category.objects.create(name="Recent")
await post.categories.add(news)
await post.categories.add(recent)
await post2.categories.add(recent)
assert len(await post.categories.all()) == 2
assert (await post.categories.limit(1).all())[0] == news
assert (await post.categories.offset(1).limit(1).all())[0] == recent
assert await post.categories.first() == news
assert await post.categories.exists()
# still can order
categories = (
await Category.objects.select_related("posts")
.order_by("posts__title")
.all()
)
assert categories[0].name == "Recent"
assert categories[1].name == "News"
# still can filter
categories = await Category.objects.filter(posts__title="Bye, M2M").all()
assert categories[0].name == "Recent"
assert len(categories) == 1
# same for reverse fk
authors = (
await Author.objects.select_related("posts").order_by("posts__title").all()
)
assert authors[0].first_name == "Guido2"
assert authors[1].first_name == "Guido"
authors = await Author.objects.filter(posts__title="Bye, M2M").all()
assert authors[0].first_name == "Guido2"
assert len(authors) == 1
ormar-orm-ormar-a7e7fde/tests/test_relations/test_through_relation_nullable.py 0000664 0000000 0000000 00000010442 15211567312 0030400 0 ustar 00root root 0000000 0000000 import pytest
import sqlalchemy
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Subject(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="trnn_subjects")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Student(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="trnn_students")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
subjects = ormar.ManyToMany(
Subject,
through_foreign_key_name="fk_trnn_stu_subj",
through_reverse_foreign_key_name="fk_trnn_stu_subj_rev",
)
class StudentStrict(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="trnn_students_strict")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
subjects = ormar.ManyToMany(
Subject,
through_relation_nullable=False,
through_reverse_relation_nullable=False,
through_foreign_key_name="fk_trnn_stu_strict_subj",
through_reverse_foreign_key_name="fk_trnn_stu_strict_subj_rev",
)
class StudentOwnerNotNull(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="trnn_students_owner_not_null")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
subjects = ormar.ManyToMany(
Subject,
through_relation_nullable=False,
through_foreign_key_name="fk_trnn_stu_owner_subj",
through_reverse_foreign_key_name="fk_trnn_stu_owner_subj_rev",
)
class StudentReverseNotNull(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="trnn_students_reverse_not_null")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
subjects = ormar.ManyToMany(
Subject,
through_reverse_relation_nullable=False,
through_foreign_key_name="fk_trnn_stu_rev_subj",
through_reverse_foreign_key_name="fk_trnn_stu_rev_subj_rev",
)
create_test_database = init_tests(base_ormar_config)
def _through_table(model, field_name):
return model.ormar_config.model_fields[field_name].through.ormar_config.table
def test_default_through_columns_are_nullable():
table = _through_table(Student, "subjects")
assert table.c["student"].nullable is True
assert table.c["subject"].nullable is True
def test_through_relation_nullable_false_sets_owner_column_not_null():
table = _through_table(StudentOwnerNotNull, "subjects")
assert table.c["studentownernotnull"].nullable is False
assert table.c["subject"].nullable is True
def test_through_reverse_relation_nullable_false_sets_target_column_not_null():
table = _through_table(StudentReverseNotNull, "subjects")
assert table.c["studentreversenotnull"].nullable is True
assert table.c["subject"].nullable is False
def test_both_through_columns_can_be_not_null():
table = _through_table(StudentStrict, "subjects")
assert table.c["studentstrict"].nullable is False
assert table.c["subject"].nullable is False
@pytest.mark.asyncio
async def test_m2m_with_non_nullable_through_columns_works_at_runtime():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
subject = await Subject.objects.create(name="math")
student = await StudentStrict.objects.create(name="Alice")
await student.subjects.add(subject)
fetched = await StudentStrict.objects.select_related("subjects").get(
id=student.id
)
assert len(fetched.subjects) == 1
assert fetched.subjects[0].name == "math"
@pytest.mark.asyncio
async def test_insert_null_into_non_nullable_through_column_fails():
async with base_ormar_config.database:
subject = await Subject.objects.create(name="chemistry")
through_table = _through_table(StudentStrict, "subjects")
async with base_ormar_config.database.connection() as conn:
with pytest.raises(sqlalchemy.exc.IntegrityError):
await conn.execute(
through_table.insert().values(
studentstrict=None, subject=subject.id
)
)
ormar-orm-ormar-a7e7fde/tests/test_relations/test_through_relations_fail.py 0000664 0000000 0000000 00000002520 15211567312 0027676 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
# type: ignore
import ormar
from ormar import ModelDefinitionError
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
def test_through_with_relation_fails() -> None:
class Category(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="categories")
id = ormar.Integer(primary_key=True)
name = ormar.String(max_length=40)
class Blog(ormar.Model):
ormar_config = base_ormar_config.copy()
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=200)
class PostCategory(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="posts_x_categories")
id: int = ormar.Integer(primary_key=True)
sort_order: Optional[int] = ormar.Integer(nullable=True)
param_name: str = ormar.String(default="Name", max_length=200)
blog = ormar.ForeignKey(Blog)
with pytest.raises(ModelDefinitionError):
class Post(ormar.Model):
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)
ormar-orm-ormar-a7e7fde/tests/test_relations/test_weakref_checking.py 0000664 0000000 0000000 00000002033 15211567312 0026421 0 ustar 00root root 0000000 0000000 import ormar
from tests.settings import create_config
base_ormar_config = create_config()
from tests.lifespan import init_tests
class Band(ormar.Model):
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):
ormar_config = base_ormar_config.copy(tablename="artists")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
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)
artist2 = Artist(name="Artist 2", band=band)
artist3 = Artist(name="Artist 3", band=band)
del artist1
Artist(
name="Artist 2", band=band
) # Force it to check for weakly-referenced objects
del artist3
band.artists # Force it to clean
assert len(band.artists) == 1
assert band.artists[0].name == artist2.name
ormar-orm-ormar-a7e7fde/tests/test_signals/ 0000775 0000000 0000000 00000000000 15211567312 0021173 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_signals/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0023272 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_signals/test_signals.py 0000664 0000000 0000000 00000032164 15211567312 0024252 0 ustar 00root root 0000000 0000000 from typing import Optional
import pydantic
import pytest
import ormar
from ormar import (
post_bulk_update,
post_delete,
post_save,
post_update,
pre_delete,
pre_save,
pre_update,
)
from ormar.exceptions import SignalDefinitionError
from ormar.signals import SignalEmitter
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class AuditLog(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="audits")
id: int = ormar.Integer(primary_key=True)
event_type: str = ormar.String(max_length=100)
event_log: pydantic.Json = ormar.JSON()
class Cover(ormar.Model):
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):
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)
play_count: int = ormar.Integer(default=0)
cover: Optional[Cover] = ormar.ForeignKey(Cover)
create_test_database = init_tests(base_ormar_config)
def test_passing_not_callable():
with pytest.raises(SignalDefinitionError):
pre_save(Album)("wrong")
def test_passing_callable_without_kwargs():
with pytest.raises(SignalDefinitionError):
@pre_save(Album)
def trigger(sender, instance): # pragma: no cover
pass
def test_invalid_signal():
emitter = SignalEmitter()
with pytest.raises(SignalDefinitionError):
emitter.save = 1
@pytest.mark.asyncio
async def test_signal_functions():
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.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.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.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.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.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.model_dump_json(),
).save()
@post_bulk_update(Album)
async def after_bulk_update(sender, instances, **kwargs):
for it in instances:
await AuditLog(
event_type=f"BULK_POST_UPDATE_{sender.get_name()}",
event_log=it.model_dump_json(),
).save()
album = await Album.objects.create(name="Venice")
audits = await AuditLog.objects.all()
assert len(audits) == 2
assert audits[0].event_type == "PRE_SAVE_album"
assert audits[0].event_log.get("name") == album.name
assert audits[1].event_type == "POST_SAVE_album"
assert audits[1].event_log.get("id") == album.pk
album = await Album(name="Rome").save()
audits = await AuditLog.objects.all()
assert len(audits) == 4
assert audits[2].event_type == "PRE_SAVE_album"
assert audits[2].event_log.get("name") == album.name
assert audits[3].event_type == "POST_SAVE_album"
assert audits[3].event_log.get("id") == album.pk
album.is_best_seller = True
await album.update()
audits = await AuditLog.objects.filter(event_type__contains="UPDATE").all()
assert len(audits) == 2
assert audits[0].event_type == "PRE_UPDATE_album"
assert audits[0].event_log.get("name") == album.name
assert audits[1].event_type == "POST_UPDATE_album"
assert audits[1].event_log.get("is_best_seller") == album.is_best_seller
album.signals.pre_update.disconnect(before_update)
album.signals.post_update.disconnect(after_update)
album.is_best_seller = False
await album.update()
audits = await AuditLog.objects.filter(event_type__contains="UPDATE").all()
assert len(audits) == 2
await album.delete()
audits = await AuditLog.objects.filter(event_type__contains="DELETE").all()
assert len(audits) == 2
assert audits[0].event_type == "PRE_DELETE_album"
assert (
audits[0].event_log.get("id")
== audits[1].event_log.get("id")
== album.id
)
assert audits[1].event_type == "POST_DELETE_album"
album.signals.pre_delete.disconnect(before_delete)
album.signals.post_delete.disconnect(after_delete)
album.signals.pre_save.disconnect(before_save)
album.signals.post_save.disconnect(after_save)
albums = await Album.objects.all()
assert len(albums)
for album in albums:
album.play_count = 1
await Album.objects.bulk_update(albums)
cnt = await AuditLog.objects.filter(
event_type__contains="BULK_POST"
).count()
assert cnt == len(albums)
album.signals.bulk_post_update.disconnect(after_bulk_update)
@pytest.mark.asyncio
async def test_multiple_signals():
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.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.model_dump_json(),
).save()
album = await Album.objects.create(name="Miami")
audits = await AuditLog.objects.all()
assert len(audits) == 2
assert audits[0].event_type == "PRE_SAVE_album"
assert audits[0].event_log.get("name") == album.name
assert audits[1].event_type == "PRE_SAVE_album"
assert audits[1].event_log.get("name") == album.name
album.signals.pre_save.disconnect(before_save)
album.signals.pre_save.disconnect(before_save2)
@pytest.mark.asyncio
async def test_static_methods_as_signals():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
class AlbumAuditor:
event_type = "ALBUM_INSTANCE"
@staticmethod
@pre_save(Album)
async def before_save(sender, instance, **kwargs):
await AuditLog(
event_type=f"{AlbumAuditor.event_type}_SAVE",
event_log=instance.model_dump_json(),
).save()
album = await Album.objects.create(name="Colorado")
audits = await AuditLog.objects.all()
assert len(audits) == 1
assert audits[0].event_type == "ALBUM_INSTANCE_SAVE"
assert audits[0].event_log.get("name") == album.name
album.signals.pre_save.disconnect(AlbumAuditor.before_save)
@pytest.mark.asyncio
async def test_methods_as_signals():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
class AlbumAuditor:
def __init__(self):
self.event_type = "ALBUM_INSTANCE"
async def before_save(self, sender, instance, **kwargs):
await AuditLog(
event_type=f"{self.event_type}_SAVE",
event_log=instance.model_dump_json(),
).save()
auditor = AlbumAuditor()
pre_save(Album)(auditor.before_save)
album = await Album.objects.create(name="San Francisco")
audits = await AuditLog.objects.all()
assert len(audits) == 1
assert audits[0].event_type == "ALBUM_INSTANCE_SAVE"
assert audits[0].event_log.get("name") == album.name
album.signals.pre_save.disconnect(auditor.before_save)
@pytest.mark.asyncio
async def test_multiple_senders_signal():
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.model_dump_json(),
).save()
cover = await Cover(title="Blue").save()
album = await Album.objects.create(name="San Francisco", cover=cover)
audits = await AuditLog.objects.all()
assert len(audits) == 2
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.model_dump(
exclude={"albums"}
)
album.signals.pre_save.disconnect(before_save)
cover.signals.pre_save.disconnect(before_save)
@pytest.mark.asyncio
async def test_modifing_the_instance():
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):
if instance.play_count > 50 and not instance.is_best_seller:
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
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.signals.pre_update.disconnect(before_update)
@pytest.mark.asyncio
async def test_custom_signal():
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:
instance.is_best_seller = True
elif instance.play_count < 50 and instance.is_best_seller:
instance.is_best_seller = False
await instance.update()
Album.ormar_config.signals.custom.connect(after_update)
# here album.play_count and 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 = 60
await album.update()
assert not album.is_best_seller
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.ormar_config.signals.custom.send(sender=Album, instance=album)
assert not album.is_best_seller
Album.ormar_config.signals.custom.disconnect(after_update)
ormar-orm-ormar-a7e7fde/tests/test_signals/test_signals_for_relations.py 0000664 0000000 0000000 00000016330 15211567312 0027175 0 ustar 00root root 0000000 0000000 from typing import Optional
import pydantic
import pytest
import ormar
from ormar import (
post_relation_add,
post_relation_remove,
pre_relation_add,
pre_relation_remove,
)
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class AuditLog(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="audits")
id: int = ormar.Integer(primary_key=True)
event_type: str = ormar.String(max_length=100)
event_log: pydantic.Json = ormar.JSON()
class Cover(ormar.Model):
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):
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):
ormar_config = base_ormar_config.copy(tablename="albums")
id: int = ormar.Integer(primary_key=True)
title: str = ormar.String(max_length=100)
cover: Optional[Cover] = ormar.ForeignKey(Cover)
artists = ormar.ManyToMany(Artist)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_relation_signal_functions():
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(
sender, instance, child, relation_name, passed_kwargs, **kwargs
):
await AuditLog.objects.create(
event_type="RELATION_PRE_ADD",
event_log=dict(
class_affected=sender.get_name(),
parent_id=instance.pk,
child_id=child.pk,
relation_name=relation_name,
kwargs=passed_kwargs,
),
)
passed_kwargs.pop("dummy", None)
@post_relation_add([Album, Cover, Artist])
async def after_relation_add(
sender, instance, child, relation_name, passed_kwargs, **kwargs
):
await AuditLog.objects.create(
event_type="RELATION_POST_ADD",
event_log=dict(
class_affected=sender.get_name(),
parent_id=instance.pk,
child_id=child.pk,
relation_name=relation_name,
kwargs=passed_kwargs,
),
)
@pre_relation_remove([Album, Cover, Artist])
async def before_relation_remove(
sender, instance, child, relation_name, **kwargs
):
await AuditLog.objects.create(
event_type="RELATION_PRE_REMOVE",
event_log=dict(
class_affected=sender.get_name(),
parent_id=instance.pk,
child_id=child.pk,
relation_name=relation_name,
kwargs=kwargs,
),
)
@post_relation_remove([Album, Cover, Artist])
async def after_relation_remove(
sender, instance, child, relation_name, **kwargs
):
await AuditLog.objects.create(
event_type="RELATION_POST_REMOVE",
event_log=dict(
class_affected=sender.get_name(),
parent_id=instance.pk,
child_id=child.pk,
relation_name=relation_name,
kwargs=kwargs,
),
)
cover = await Cover(title="New").save()
artist = await Artist(name="Artist").save()
album = await Album(title="New Album").save()
await cover.albums.add(album, index=0)
log = await AuditLog.objects.get(event_type="RELATION_PRE_ADD")
assert log.event_log.get("parent_id") == cover.pk
assert log.event_log.get("child_id") == album.pk
assert log.event_log.get("relation_name") == "albums"
assert log.event_log.get("kwargs") == dict(index=0)
log2 = await AuditLog.objects.get(event_type="RELATION_POST_ADD")
assert log2.event_log.get("parent_id") == cover.pk
assert log2.event_log.get("child_id") == album.pk
assert log2.event_log.get("relation_name") == "albums"
assert log2.event_log.get("kwargs") == dict(index=0)
await album.artists.add(artist, dummy="test")
log3 = await AuditLog.objects.filter(
event_type="RELATION_PRE_ADD", id__gt=log2.pk
).get()
assert log3.event_log.get("parent_id") == album.pk
assert log3.event_log.get("child_id") == artist.pk
assert log3.event_log.get("relation_name") == "artists"
assert log3.event_log.get("kwargs") == dict(dummy="test")
log4 = await AuditLog.objects.get(
event_type="RELATION_POST_ADD", id__gt=log3.pk
)
assert log4.event_log.get("parent_id") == album.pk
assert log4.event_log.get("child_id") == artist.pk
assert log4.event_log.get("relation_name") == "artists"
assert log4.event_log.get("kwargs") == dict()
assert album.cover == cover
assert len(album.artists) == 1
await cover.albums.remove(album)
log = await AuditLog.objects.get(event_type="RELATION_PRE_REMOVE")
assert log.event_log.get("parent_id") == cover.pk
assert log.event_log.get("child_id") == album.pk
assert log.event_log.get("relation_name") == "albums"
assert log.event_log.get("kwargs") == dict()
log2 = await AuditLog.objects.get(event_type="RELATION_POST_REMOVE")
assert log2.event_log.get("parent_id") == cover.pk
assert log2.event_log.get("child_id") == album.pk
assert log2.event_log.get("relation_name") == "albums"
assert log2.event_log.get("kwargs") == dict()
await album.artists.remove(artist)
log3 = await AuditLog.objects.filter(
event_type="RELATION_PRE_REMOVE", id__gt=log2.pk
).get()
assert log3.event_log.get("parent_id") == album.pk
assert log3.event_log.get("child_id") == artist.pk
assert log3.event_log.get("relation_name") == "artists"
assert log3.event_log.get("kwargs") == dict()
log4 = await AuditLog.objects.get(
event_type="RELATION_POST_REMOVE", id__gt=log3.pk
)
assert log4.event_log.get("parent_id") == album.pk
assert log4.event_log.get("child_id") == artist.pk
assert log4.event_log.get("relation_name") == "artists"
assert log4.event_log.get("kwargs") == dict()
await album.load_all()
assert len(album.artists) == 0
assert album.cover is None
ormar-orm-ormar-a7e7fde/tests/test_signals/test_signals_proxy_models.py 0000664 0000000 0000000 00000016370 15211567312 0027057 0 ustar 00root root 0000000 0000000 """Tests for the emit_parent_signals opt-in on proxy ormar models."""
from typing import Callable
import pytest
import ormar
from ormar import post_delete, post_save, post_update, pre_delete, pre_save, pre_update
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Person(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="signal_persons")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50)
class QuietProxy(Person):
ormar_config = base_ormar_config.copy(proxy=True)
class LoudProxy(Person):
ormar_config = base_ormar_config.copy(proxy=True, emit_parent_signals=True)
class MidProxy(LoudProxy):
ormar_config = base_ormar_config.copy(proxy=True, emit_parent_signals=True)
class Solo(ormar.Model):
"""Non-proxy concrete model with the opt-in flag set — should be a no-op."""
ormar_config = base_ormar_config.copy(
tablename="signal_solo", emit_parent_signals=True
)
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50)
loud_base_config = base_ormar_config.copy(emit_parent_signals=True)
class SilentProxy(Person):
"""Proxy that explicitly opts out even though its source config opts in."""
ormar_config = loud_base_config.copy(proxy=True, emit_parent_signals=False)
create_test_database = init_tests(base_ormar_config)
def collect(target: list) -> Callable:
async def receiver(sender, instance, **kwargs):
target.append((sender, type(instance), kwargs))
return receiver
def disconnect_all(*signal_attrs) -> None:
for signal_attr in signal_attrs:
for receiver_id in list(signal_attr._receivers):
del signal_attr._receivers[receiver_id]
@pytest.mark.asyncio
async def test_default_proxy_does_not_emit_parent_signals() -> None:
parent_calls: list = []
proxy_calls: list = []
pre_save(Person)(collect(parent_calls))
pre_save(QuietProxy)(collect(proxy_calls))
try:
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await QuietProxy.objects.create(name="quiet")
assert proxy_calls and proxy_calls[0][0] is QuietProxy
assert parent_calls == []
finally:
disconnect_all(Person.ormar_config.signals.pre_save)
disconnect_all(QuietProxy.ormar_config.signals.pre_save)
@pytest.mark.asyncio
async def test_opt_in_emits_parent_pre_save_with_parent_sender() -> None:
parent_calls: list = []
proxy_calls: list = []
pre_save(Person)(collect(parent_calls))
pre_save(LoudProxy)(collect(proxy_calls))
try:
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await LoudProxy.objects.create(name="loud")
assert len(proxy_calls) == 1
assert proxy_calls[0][0] is LoudProxy
assert len(parent_calls) == 1
assert parent_calls[0][0] is Person
assert parent_calls[0][1] is LoudProxy
finally:
disconnect_all(Person.ormar_config.signals.pre_save)
disconnect_all(LoudProxy.ormar_config.signals.pre_save)
@pytest.mark.asyncio
async def test_opt_in_forwards_full_lifecycle() -> None:
captured: dict[str, list] = {
"pre_save": [],
"post_save": [],
"pre_update": [],
"post_update": [],
"pre_delete": [],
"post_delete": [],
}
pre_save(Person)(collect(captured["pre_save"]))
post_save(Person)(collect(captured["post_save"]))
pre_update(Person)(collect(captured["pre_update"]))
post_update(Person)(collect(captured["post_update"]))
pre_delete(Person)(collect(captured["pre_delete"]))
post_delete(Person)(collect(captured["post_delete"]))
try:
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
instance = await LoudProxy.objects.create(name="full")
await instance.update(name="renamed")
await instance.delete()
for name, calls in captured.items():
assert len(calls) == 1, f"{name} not received exactly once"
assert calls[0][0] is Person, f"{name} sender mismatch"
finally:
cfg = Person.ormar_config.signals
disconnect_all(
cfg.pre_save,
cfg.post_save,
cfg.pre_update,
cfg.post_update,
cfg.pre_delete,
cfg.post_delete,
)
@pytest.mark.asyncio
async def test_two_level_proxy_chain_forwards_to_every_ancestor() -> None:
person_calls: list = []
loud_calls: list = []
mid_calls: list = []
pre_save(Person)(collect(person_calls))
pre_save(LoudProxy)(collect(loud_calls))
pre_save(MidProxy)(collect(mid_calls))
try:
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await MidProxy.objects.create(name="chain")
assert mid_calls and mid_calls[0][0] is MidProxy
assert loud_calls and loud_calls[0][0] is LoudProxy
assert person_calls and person_calls[0][0] is Person
finally:
disconnect_all(
Person.ormar_config.signals.pre_save,
LoudProxy.ormar_config.signals.pre_save,
MidProxy.ormar_config.signals.pre_save,
)
@pytest.mark.asyncio
async def test_emit_parent_signals_on_non_proxy_is_noop() -> None:
calls: list = []
pre_save(Solo)(collect(calls))
try:
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await Solo.objects.create(name="one")
assert len(calls) == 1
assert calls[0][0] is Solo
finally:
disconnect_all(Solo.ormar_config.signals.pre_save)
def test_copy_explicit_false_overrides_inherited_true() -> None:
"""Pin the ``is not None`` pattern: explicit False wins over True parent."""
parent = base_ormar_config.copy(emit_parent_signals=True)
inherited = parent.copy()
overridden = parent.copy(emit_parent_signals=False)
assert parent.emit_parent_signals is True
assert inherited.emit_parent_signals is True
assert overridden.emit_parent_signals is False
@pytest.mark.asyncio
async def test_proxy_with_explicit_false_does_not_forward_even_with_loud_base() -> None:
"""Runtime regression: SilentProxy was built from a True-flagged config but
overrides to False — saving it must not forward to the parent class."""
parent_calls: list = []
proxy_calls: list = []
pre_save(Person)(collect(parent_calls))
pre_save(SilentProxy)(collect(proxy_calls))
try:
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await SilentProxy.objects.create(name="silent")
assert SilentProxy.ormar_config.emit_parent_signals is False
assert len(proxy_calls) == 1
assert proxy_calls[0][0] is SilentProxy
assert parent_calls == []
finally:
disconnect_all(
Person.ormar_config.signals.pre_save,
SilentProxy.ormar_config.signals.pre_save,
)
ormar-orm-ormar-a7e7fde/tests/test_types.py 0000664 0000000 0000000 00000007114 15211567312 0021254 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import sqlalchemy
import ormar
from ormar.databases.connection import DatabaseConnection
from ormar.models.ormar_config import OrmarConfig
from tests.settings import ASYNC_DATABASE_URL, DATABASE_URL
database = DatabaseConnection(ASYNC_DATABASE_URL)
metadata = sqlalchemy.MetaData()
class Publisher(ormar.Model):
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):
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)
publishers = ormar.ManyToMany(Publisher)
class Book(ormar.Model):
ormar_config = OrmarConfig(
metadata=metadata,
database=database,
tablename="books",
order_by=["year", "-ranking"],
)
id: int = ormar.Integer(primary_key=True)
author = ormar.ForeignKey(Author)
title: str = ormar.String(max_length=100)
year: Optional[int] = ormar.Integer(nullable=True)
ranking: Optional[int] = ormar.Integer(nullable=True)
@pytest.fixture(autouse=True, scope="module")
def create_test_database_for_this_module():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.drop_all(engine)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
def assert_type(book: Book):
_ = str(book)
@pytest.mark.asyncio
async def test_types() -> None:
async with database:
publisher = await Publisher(name="Test publisher").save()
author = await Author.objects.create(name="Test Author")
await author.publishers.add(publisher)
await Author.objects.select_related("publishers").get()
publisher2 = await Publisher.objects.select_related("authors").get()
authors = publisher2.authors
assert authors[0] == author
for author in authors:
pass
# if TYPE_CHECKING: # pragma: no cover
# reveal_type(author) # iter of relation proxy
book = await Book.objects.create(title="Test", author=author)
book2 = await Book.objects.select_related("author").get()
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
# reveal_type(publisher) # model method
# reveal_type(publishers) # many to many
# reveal_type(publishers[0]) # item in m2m list
# reveal_type(next(p for p in publishers)) # item in m2m iterator
# # getting relation without __getattribute__
# reveal_type(authors) # reverse many to many # TODO: wrong
# reveal_type(book2) # queryset get
# reveal_type(books) # queryset all
# reveal_type(book) # queryset - create
# reveal_type(query) # queryset itself
# reveal_type(book.author) # fk
# reveal_type(author.books) # reverse fk relation proxy # TODO: wrong
# reveal_type(author) # another test for queryset get different model
# reveal_type(book.author.name) # field on related model
# reveal_type(author_books) # querysetproxy result for fk # TODO: wrong
# reveal_type(author_books[0]) # item in qs proxy for fk # TODO: wrong
assert_type(book)
ormar-orm-ormar-a7e7fde/tests/test_utils/ 0000775 0000000 0000000 00000000000 15211567312 0020673 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_utils/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0022772 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_utils/test_models_helpers.py 0000664 0000000 0000000 00000001200 15211567312 0025302 0 ustar 00root root 0000000 0000000 from ormar.models.helpers.models import group_related_list
from ormar.models.helpers.relations import replace_models_with_copy
def test_group_related_list():
given = [
"friends__least_favourite_game",
"least_favourite_game",
"friends",
"favourite_game",
"friends__favourite_game",
]
expected = {
"least_favourite_game": [],
"favourite_game": [],
"friends": ["favourite_game", "least_favourite_game"],
}
assert group_related_list(given) == expected
def test_replace_models_with_copy_returns_plain_type():
assert replace_models_with_copy(int) is int
ormar-orm-ormar-a7e7fde/tests/test_utils/test_queryset_utils.py 0000664 0000000 0000000 00000020054 15211567312 0025406 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from ormar.exceptions import QueryDefinitionError
from ormar.queryset.actions.order_action import OrderAction
from ormar.queryset.utils import (
SliceBounds,
normalize_slice,
subtract_dict,
translate_list_to_dict,
update,
update_dict_from_list,
)
from tests.settings import create_config
base_ormar_config = create_config()
class _SliceModel(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="slice_utils_model")
id: int = ormar.Integer(primary_key=True)
name: Optional[str] = ormar.String(max_length=50, nullable=True)
def test_normalize_slice_positive_int_and_slice():
assert normalize_slice(5) == SliceBounds(limit=1, offset=5, reverse=False)
assert normalize_slice(slice(2, 8)) == SliceBounds(limit=6, offset=2, reverse=False)
assert normalize_slice(slice(None, 8)) == SliceBounds(
limit=8, offset=0, reverse=False
)
assert normalize_slice(slice(2, None)) == SliceBounds(
limit=None, offset=2, reverse=False
)
assert normalize_slice(slice(None, None)) == SliceBounds(
limit=None, offset=0, reverse=False
)
assert normalize_slice(slice(8, 2)) == SliceBounds(limit=0, offset=8, reverse=False)
def test_normalize_slice_negative_int_and_slice():
assert normalize_slice(-1) == SliceBounds(limit=1, offset=0, reverse=True)
assert normalize_slice(-3) == SliceBounds(limit=1, offset=2, reverse=True)
assert normalize_slice(slice(-3, None)) == SliceBounds(
limit=3, offset=0, reverse=True
)
assert normalize_slice(slice(-5, -2)) == SliceBounds(
limit=3, offset=2, reverse=True
)
assert normalize_slice(slice(-2, -5)) == SliceBounds(
limit=0, offset=0, reverse=True
)
def test_normalize_slice_rejects_invalid_input():
with pytest.raises(QueryDefinitionError):
normalize_slice("five") # type: ignore[arg-type]
with pytest.raises(QueryDefinitionError):
normalize_slice(slice(None, None, 2))
with pytest.raises(QueryDefinitionError):
normalize_slice(slice(None, -5))
with pytest.raises(QueryDefinitionError):
normalize_slice(slice(3, -5))
with pytest.raises(QueryDefinitionError):
normalize_slice(slice(-3, 5))
def test_order_action_flipped_direction_and_nulls():
asc_first = OrderAction(
order_str="name",
model_cls=_SliceModel,
nulls_ordering="first",
)
desc_last = asc_first.flipped()
assert desc_last.direction == "desc"
assert desc_last.nulls_ordering == "last"
desc_first = OrderAction(
order_str="-name",
model_cls=_SliceModel,
nulls_ordering="last",
)
asc_last = desc_first.flipped()
assert asc_last.direction == ""
assert asc_last.nulls_ordering == "first"
plain = OrderAction(order_str="name", model_cls=_SliceModel)
assert plain.flipped().nulls_ordering is None
def test_list_to_dict_translation():
tet_list = ["aa", "bb", "cc__aa", "cc__bb", "cc__aa__xx", "cc__aa__yy"]
test = translate_list_to_dict(tet_list)
assert test == {
"aa": Ellipsis,
"bb": Ellipsis,
"cc": {"aa": {"xx": Ellipsis, "yy": Ellipsis}, "bb": Ellipsis},
}
def test_list_to_dict_translation_with_default():
tet_list = ["aa", "aa__inner", "bb"]
testshallow = translate_list_to_dict(tet_list, default={})
assert testshallow == {"aa": {"inner": {}}, "bb": {}}
tet_list = ["aa", "aa__inner", "bb"]
testdeep = translate_list_to_dict(tet_list, default={"foo": {}})
assert testdeep == {"aa": {"foo": {}, "inner": {"foo": {}}}, "bb": {"foo": {}}}
testdeep["aa"]["foo"]["bar"] = 1234
assert testdeep == {
"aa": {"foo": {"bar": 1234}, "inner": {"foo": {}}},
"bb": {"foo": {}},
}
def test_updating_dict_with_list():
curr_dict = {
"aa": Ellipsis,
"bb": Ellipsis,
"cc": {"aa": {"xx": Ellipsis, "yy": Ellipsis}, "bb": Ellipsis},
}
list_to_update = ["ee", "bb__cc", "cc__aa__xx__oo", "cc__aa__oo"]
test = update_dict_from_list(curr_dict, list_to_update)
assert test == {
"aa": Ellipsis,
"bb": {"cc": Ellipsis},
"cc": {
"aa": {"xx": {"oo": Ellipsis}, "yy": Ellipsis, "oo": Ellipsis},
"bb": Ellipsis,
},
"ee": Ellipsis,
}
def test_updating_dict_inc_set_with_list():
curr_dict = {
"aa": Ellipsis,
"bb": Ellipsis,
"cc": {"aa": {"xx", "yy"}, "bb": Ellipsis},
}
list_to_update = ["uu", "bb__cc", "cc__aa__xx__oo", "cc__aa__oo"]
test = update_dict_from_list(curr_dict, list_to_update)
assert test == {
"aa": Ellipsis,
"bb": {"cc": Ellipsis},
"cc": {
"aa": {"xx": {"oo": Ellipsis}, "yy": Ellipsis, "oo": Ellipsis},
"bb": Ellipsis,
},
"uu": Ellipsis,
}
def test_updating_dict_inc_set_with_dict():
curr_dict = {
"aa": Ellipsis,
"bb": Ellipsis,
"cc": {"aa": {"xx", "yy"}, "bb": Ellipsis},
}
dict_to_update = {
"uu": Ellipsis,
"bb": {"cc", "dd"},
"cc": {"aa": {"xx": {"oo": Ellipsis}, "oo": Ellipsis}},
}
test = update(curr_dict, dict_to_update)
assert test == {
"aa": Ellipsis,
"bb": {"cc", "dd"},
"cc": {
"aa": {"xx": {"oo": Ellipsis}, "yy": Ellipsis, "oo": Ellipsis},
"bb": Ellipsis,
},
"uu": Ellipsis,
}
def test_subtracting_dict_inc_set_with_dict():
curr_dict = {
"aa": Ellipsis,
"bb": Ellipsis,
"cc": {"aa": {"xx", "yy"}, "bb": Ellipsis},
}
dict_to_update = {
"uu": Ellipsis,
"bb": {"cc", "dd"},
"cc": {"aa": {"xx": {"oo": Ellipsis}}, "bb": Ellipsis},
}
test = subtract_dict(curr_dict, dict_to_update)
assert test == {"aa": Ellipsis, "cc": {"aa": {"yy": Ellipsis}}}
def test_updating_dict_inc_set_with_dict_inc_set():
curr_dict = {
"aa": Ellipsis,
"bb": Ellipsis,
"cc": {"aa": {"xx", "yy"}, "bb": Ellipsis},
}
dict_to_update = {
"uu": Ellipsis,
"bb": {"cc", "dd"},
"cc": {"aa": {"xx", "oo", "zz", "ii"}},
}
test = update(curr_dict, dict_to_update)
assert test == {
"aa": Ellipsis,
"bb": {"cc", "dd"},
"cc": {"aa": {"xx", "yy", "oo", "zz", "ii"}, "bb": Ellipsis},
"uu": Ellipsis,
}
def test_subtracting_dict_inc_set_with_dict_inc_set():
curr_dict = {
"aa": Ellipsis,
"bb": Ellipsis,
"cc": {"aa": {"xx", "yy"}, "bb": Ellipsis},
"dd": {"aa", "bb"},
}
dict_to_update = {
"aa": Ellipsis,
"bb": {"cc", "dd"},
"cc": {"aa": {"xx", "oo", "zz", "ii"}},
"dd": {"aa", "bb"},
}
test = subtract_dict(curr_dict, dict_to_update)
assert test == {"cc": {"aa": {"yy"}, "bb": Ellipsis}}
def test_subtracting_with_set_and_dict():
curr_dict = {
"translation": {
"filters": {
"values": Ellipsis,
"reports": {"report": {"charts": {"chart": Ellipsis}}},
},
"translations": {"language": Ellipsis},
"filtervalues": {
"filter": {"reports": {"report": {"charts": {"chart": Ellipsis}}}}
},
},
"chart": {
"reports": {
"report": {
"filters": {
"filter": {
"translation": {
"translations": {"language": Ellipsis},
"filtervalues": Ellipsis,
},
"values": {
"translation": {"translations": {"language": Ellipsis}}
},
}
}
}
}
},
}
dict_to_update = {
"chart": Ellipsis,
"translation": {"filters", "filtervalues", "chartcolumns"},
}
test = subtract_dict(curr_dict, dict_to_update)
assert test == {"translation": {"translations": {"language": Ellipsis}}}
ormar-orm-ormar-a7e7fde/tests/test_vulnerabilities/ 0000775 0000000 0000000 00000000000 15211567312 0022734 5 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_vulnerabilities/__init__.py 0000664 0000000 0000000 00000000000 15211567312 0025033 0 ustar 00root root 0000000 0000000 ormar-orm-ormar-a7e7fde/tests/test_vulnerabilities/test_aggregated_functions.py 0000664 0000000 0000000 00000005035 15211567312 0030532 0 ustar 00root root 0000000 0000000 from typing import Optional
import pytest
import ormar
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
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(tablename="items")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
price: float = ormar.Float(default=0)
category: Optional[Category] = ormar.ForeignKey(Category, nullable=True)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_arbitrary_sql_execution():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
if not await Item.objects.count():
cat = await Category.objects.create(name="Electronics")
await Item.objects.create(name="Tablet", price=449.99, category=cat)
await Item.objects.create(name="Monitor", price=329.99, category=cat)
column = "1 + 1"
with pytest.raises(ormar.exceptions.QueryDefinitionError):
await Item.objects.min(column)
@pytest.mark.asyncio
async def test_arbitrary_sql_execution_on_related_model():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
if not await Item.objects.count():
cat = await Category.objects.create(name="Electronics")
await Item.objects.create(name="Tablet", price=449.99, category=cat)
await Item.objects.create(name="Monitor", price=329.99, category=cat)
column = "category__1 + 1"
with pytest.raises(ormar.exceptions.QueryDefinitionError):
await Item.objects.min(column)
@pytest.mark.asyncio
async def test_schema_extraction():
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
if not await Item.objects.count():
cat = await Category.objects.create(name="Electronics")
await Item.objects.create(name="Laptop", price=999.99, category=cat)
column = "(SELECT GROUP_CONCAT(name) FROM sqlite_master WHERE type='table')"
with pytest.raises(ormar.exceptions.QueryDefinitionError):
await Item.objects.min(column)
ormar-orm-ormar-a7e7fde/tests/test_vulnerabilities/test_kwargs_injection.py 0000664 0000000 0000000 00000010454 15211567312 0027711 0 ustar 00root root 0000000 0000000 # type: ignore
"""
Tests that __pk_only__ and __excluded__ cannot be injected via kwargs.
"""
import pytest
import ormar
from ormar.exceptions import ModelError
from ormar.models import Model
from tests.lifespan import init_tests
from tests.settings import create_config
base_ormar_config = create_config()
class Item(Model):
ormar_config = base_ormar_config.copy(tablename="security_items")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
secret: str = ormar.String(max_length=100, default="default_secret")
class Category(Model):
ormar_config = base_ormar_config.copy(tablename="security_categories")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Product(Model):
ormar_config = base_ormar_config.copy(tablename="security_products")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
category = ormar.ForeignKey(Category)
create_test_database = init_tests(base_ormar_config)
@pytest.mark.asyncio
async def test_pk_only_injection_rejected():
"""__pk_only__ in kwargs must not bypass validation."""
with pytest.raises(ModelError, match="Unknown field '__pk_only__'"):
Item(**{"__pk_only__": True, "id": 1, "name": "test"})
@pytest.mark.asyncio
async def test_pk_only_invalid_data_still_validated():
"""Even with __pk_only__=True in kwargs, validation still runs."""
with pytest.raises(ModelError, match="Unknown field '__pk_only__'"):
Item(**{"__pk_only__": True, "id": 1, "name": 123456})
@pytest.mark.asyncio
async def test_excluded_injection_ignored():
"""__excluded__ in kwargs must not nullify fields."""
with pytest.raises(ModelError, match="Unknown field '__excluded__'"):
Item(**{"__excluded__": {"secret"}, "id": 1, "name": "test"})
@pytest.mark.asyncio
async def test_create_with_pk_only_kwarg():
"""objects.create with __pk_only__ must not bypass validation."""
with pytest.raises(ModelError, match="Unknown field '__pk_only__'"):
await Item.objects.create(**{"__pk_only__": True, "id": 1, "name": "injected"})
@pytest.mark.asyncio
async def test_fk_relations_still_work():
"""Internal pk_only path via _internal_construct still works for FK."""
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
cat = await Category.objects.create(name="Electronics")
prod = await Product.objects.create(name="Phone", category=cat)
loaded = await Product.objects.select_related("category").get(id=prod.id)
assert loaded.category.name == "Electronics"
@pytest.mark.asyncio
async def test_query_exclusions_still_work():
"""Internal excluded path via _internal_construct still works for queries."""
async with base_ormar_config.database:
async with base_ormar_config.database.transaction(force_rollback=True):
await Item.objects.create(name="widget", secret="hidden")
loaded = await Item.objects.fields(["id", "name"]).get(name="widget")
assert loaded.name == "widget"
assert loaded.secret is None
@pytest.mark.asyncio
async def test_single_underscore_pk_only_not_a_threat():
"""_pk_only in kwargs is rejected as unknown field, not treated specially."""
with pytest.raises(ModelError, match="Unknown field '_pk_only'"):
Item(**{"_pk_only": True, "id": 1, "name": "test"})
@pytest.mark.asyncio
async def test_single_underscore_excluded_not_a_threat():
"""_excluded in kwargs is rejected as unknown field, not treated specially."""
with pytest.raises(ModelError, match="Unknown field '_excluded'"):
Item(**{"_excluded": {"secret"}, "id": 1, "name": "test"})
@pytest.mark.asyncio
async def test_internal_construct_pk_only():
"""_internal_construct with _pk_only=True skips validation."""
instance = Item._internal_construct(_pk_only=True, id=42)
assert instance.pk == 42
assert instance.__pk_only__ is True
@pytest.mark.asyncio
async def test_internal_construct_excluded():
"""_internal_construct with _excluded nullifies fields."""
instance = Item._internal_construct(
_excluded={"secret"}, id=1, name="test", secret="value"
)
assert instance.secret is None
assert instance.name == "test"