├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── ci.yml
│ ├── codspeed.yml
│ ├── gh-pages-release.yml
│ ├── gh-pages.yml
│ └── pypi.yml
├── .gitignore
├── CHANGELOG.rst
├── CONTRIBUTORS.rst
├── LICENSE.txt
├── Makefile
├── README.rst
├── conftest.py
├── docs
├── CHANGELOG.rst
├── CODE_OF_CONDUCT.rst
├── CONTRIBUTING.rst
├── CONTRIBUTORS.rst
├── Makefile
├── ORM_Perf.png
├── _static
│ ├── .keep
│ ├── basic.css
│ └── tortoise.png
├── _templates
│ └── space.html
├── cli.rst
├── conf.py
├── connections.rst
├── contrib.rst
├── contrib
│ ├── aiohttp.rst
│ ├── blacksheep.rst
│ ├── fastapi.rst
│ ├── linters.rst
│ ├── mysql.rst
│ ├── postgres.rst
│ ├── pydantic.rst
│ ├── quart.rst
│ ├── sanic.rst
│ ├── starlette.rst
│ └── unittest.rst
├── databases.rst
├── examples.rst
├── examples
│ ├── aiohttp.rst
│ ├── basic.rst
│ ├── blacksheep.rst
│ ├── fastapi.rst
│ ├── pydantic.rst
│ ├── quart.rst
│ ├── sanic.rst
│ └── starlette.rst
├── exceptions.rst
├── expressions.rst
├── fields.rst
├── functions.rst
├── getting_started.rst
├── index.rst
├── indexes.rst
├── logging.rst
├── manager.rst
├── migration.rst
├── models.rst
├── query.rst
├── reference.rst
├── roadmap.rst
├── router.rst
├── schema.rst
├── setup.rst
├── signals.rst
├── sphinx_autodoc_typehints.py
├── timezone.rst
├── toc.rst
├── transactions.rst
├── type_globals.py
├── validators.rst
└── versions.json
├── examples
├── __init__.py
├── aiohttp
│ ├── README.rst
│ ├── __init__.py
│ ├── main.py
│ └── models.py
├── basic.py
├── basic_comments.py
├── blacksheep
│ ├── README.md
│ ├── __init__.py
│ ├── _tests.py
│ ├── models.py
│ └── server.py
├── complex_filtering.py
├── complex_prefetching.py
├── enum_fields.py
├── fastapi
│ ├── .gitignore
│ ├── README.rst
│ ├── __init__.py
│ ├── _tests.py
│ ├── config.py
│ ├── main.py
│ ├── main_custom_timezone.py
│ ├── models.py
│ ├── routers.py
│ └── schemas.py
├── functions.py
├── global_table_name_generator.py
├── group_by.py
├── manual_sql.py
├── postgres.py
├── pydantic
│ ├── __init__.py
│ ├── basic.py
│ ├── early_init.py
│ ├── recursive.py
│ ├── tutorial_1.py
│ ├── tutorial_2.py
│ ├── tutorial_3.py
│ └── tutorial_4.py
├── quart
│ ├── README.rst
│ ├── __init__.py
│ ├── main.py
│ └── models.py
├── relations.py
├── relations_recursive.py
├── relations_with_unique.py
├── router.py
├── sanic
│ ├── README.rst
│ ├── __init__.py
│ ├── _tests.py
│ ├── main.py
│ └── models.py
├── schema_create.py
├── signals.py
├── starlette
│ ├── README.rst
│ ├── __init__.py
│ ├── main.py
│ └── models.py
├── transactions.py
└── two_databases.py
├── poetry.lock
├── pyproject.toml
├── tests
├── __init__.py
├── backends
│ ├── __init__.py
│ ├── test_capabilities.py
│ ├── test_connection_params.py
│ ├── test_db_url.py
│ ├── test_explain.py
│ ├── test_mysql.py
│ ├── test_postgres.py
│ └── test_reconnect.py
├── benchmarks
│ ├── conftest.py
│ ├── test_bulk_create.py
│ ├── test_create.py
│ ├── test_expressions.py
│ ├── test_field_attribute_lookup.py
│ ├── test_filter.py
│ ├── test_get.py
│ ├── test_relations.py
│ └── test_update.py
├── contrib
│ ├── __init__.py
│ ├── mysql
│ │ ├── __init__.py
│ │ └── fields.py
│ ├── postgres
│ │ ├── __init__.py
│ │ └── test_json.py
│ ├── test_decorator.py
│ ├── test_fastapi.py
│ ├── test_functions.py
│ ├── test_pydantic.py
│ └── test_tester.py
├── fields
│ ├── __init__.py
│ ├── subclass_fields.py
│ ├── subclass_models.py
│ ├── test_array.py
│ ├── test_binary.py
│ ├── test_bool.py
│ ├── test_char.py
│ ├── test_common.py
│ ├── test_db_index.py
│ ├── test_decimal.py
│ ├── test_enum.py
│ ├── test_fk.py
│ ├── test_fk_uuid.py
│ ├── test_fk_with_unique.py
│ ├── test_float.py
│ ├── test_int.py
│ ├── test_json.py
│ ├── test_m2m.py
│ ├── test_m2m_uuid.py
│ ├── test_o2o_with_unique.py
│ ├── test_subclass.py
│ ├── test_subclass_filters.py
│ ├── test_text.py
│ ├── test_time.py
│ └── test_uuid.py
├── model_setup
│ ├── __init__.py
│ ├── init.json
│ ├── init.yaml
│ ├── model_bad_rel1.py
│ ├── model_bad_rel2.py
│ ├── model_bad_rel3.py
│ ├── model_bad_rel4.py
│ ├── model_bad_rel5.py
│ ├── model_bad_rel6.py
│ ├── model_bad_rel7.py
│ ├── model_bad_rel8.py
│ ├── model_bad_rel9.py
│ ├── model_generated_nonint.py
│ ├── model_multiple_pk.py
│ ├── model_nonpk_id.py
│ ├── models__models__bad.py
│ ├── models__models__good.py
│ ├── models_dup1.py
│ ├── models_dup2.py
│ ├── models_dup3.py
│ ├── test__models__.py
│ ├── test_bad_relation_reference.py
│ └── test_init.py
├── schema
│ ├── models_cyclic.py
│ ├── models_fk_1.py
│ ├── models_fk_2.py
│ ├── models_fk_3.py
│ ├── models_m2m_1.py
│ ├── models_m2m_2.py
│ ├── models_mysql_index.py
│ ├── models_no_auto_create_m2m.py
│ ├── models_no_db_constraint.py
│ ├── models_o2o_2.py
│ ├── models_o2o_3.py
│ ├── models_postgres_fields.py
│ ├── models_postgres_index.py
│ ├── models_schema_create.py
│ └── test_generate_schema.py
├── test_aggregation.py
├── test_basic.py
├── test_bulk.py
├── test_callable_default.py
├── test_case_when.py
├── test_concurrency.py
├── test_connection.py
├── test_default.py
├── test_early_init.py
├── test_f.py
├── test_filtering.py
├── test_filters.py
├── test_fuzz.py
├── test_group_by.py
├── test_inheritance.py
├── test_latest_earliest.py
├── test_manager.py
├── test_manual_sql.py
├── test_model_methods.py
├── test_only.py
├── test_order_by.py
├── test_order_by_nested.py
├── test_posix_regex_filter.py
├── test_prefetching.py
├── test_primary_key.py
├── test_q.py
├── test_queryset.py
├── test_queryset_reuse.py
├── test_relations.py
├── test_relations_with_unique.py
├── test_signals.py
├── test_source_field.py
├── test_sql.py
├── test_table_name.py
├── test_transactions.py
├── test_two_databases.py
├── test_unique_together.py
├── test_update.py
├── test_validators.py
├── test_values.py
├── test_version.py
├── testmodels.py
├── testmodels_mysql.py
├── testmodels_postgres.py
└── utils
│ ├── __init__.py
│ ├── test_describe_model.py
│ └── test_run_async.py
└── tortoise
├── __init__.py
├── backends
├── __init__.py
├── asyncpg
│ ├── __init__.py
│ ├── client.py
│ ├── executor.py
│ └── schema_generator.py
├── base
│ ├── __init__.py
│ ├── client.py
│ ├── config_generator.py
│ ├── executor.py
│ └── schema_generator.py
├── base_postgres
│ ├── __init__.py
│ ├── client.py
│ ├── executor.py
│ └── schema_generator.py
├── mssql
│ ├── __init__.py
│ ├── client.py
│ ├── executor.py
│ └── schema_generator.py
├── mysql
│ ├── __init__.py
│ ├── client.py
│ ├── executor.py
│ └── schema_generator.py
├── odbc
│ ├── __init__.py
│ ├── client.py
│ └── executor.py
├── oracle
│ ├── __init__.py
│ ├── client.py
│ ├── executor.py
│ └── schema_generator.py
├── psycopg
│ ├── __init__.py
│ ├── client.py
│ ├── executor.py
│ └── schema_generator.py
└── sqlite
│ ├── __init__.py
│ ├── client.py
│ ├── executor.py
│ └── schema_generator.py
├── connection.py
├── contrib
├── __init__.py
├── aiohttp
│ └── __init__.py
├── blacksheep
│ └── __init__.py
├── fastapi
│ └── __init__.py
├── mysql
│ ├── __init__.py
│ ├── fields.py
│ ├── functions.py
│ ├── indexes.py
│ ├── json_functions.py
│ └── search.py
├── postgres
│ ├── __init__.py
│ ├── array_functions.py
│ ├── fields.py
│ ├── functions.py
│ ├── indexes.py
│ ├── json_functions.py
│ ├── regex.py
│ └── search.py
├── pydantic
│ ├── __init__.py
│ ├── base.py
│ ├── creator.py
│ ├── descriptions.py
│ └── utils.py
├── pylint
│ └── __init__.py
├── quart
│ └── __init__.py
├── sanic
│ └── __init__.py
├── sqlite
│ ├── __init__.py
│ ├── functions.py
│ └── regex.py
├── starlette
│ └── __init__.py
└── test
│ ├── __init__.py
│ └── condition.py
├── converters.py
├── exceptions.py
├── expressions.py
├── fields
├── __init__.py
├── base.py
├── data.py
└── relational.py
├── filters.py
├── functions.py
├── indexes.py
├── log.py
├── manager.py
├── models.py
├── py.typed
├── query_utils.py
├── queryset.py
├── router.py
├── signals.py
├── timezone.py
├── transactions.py
├── utils.py
└── validators.py
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ["https://sponsor.long2ice.io"]
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior, preferably a small code snippet.
12 |
13 | **Expected behavior**
14 | A clear and concise description of what you expected to happen.
15 |
16 | **Additional context**
17 | Add any other context about the problem here.
18 |
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context about the feature request here.
18 |
19 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Description
4 |
5 |
6 | ## Motivation and Context
7 |
8 |
9 |
10 | ## How Has This Been Tested?
11 |
12 |
13 |
14 |
15 | ## Checklist:
16 |
17 |
18 | - [ ] My code follows the code style of this project.
19 | - [ ] My change requires a change to the documentation.
20 | - [ ] I have updated the documentation accordingly.
21 | - [ ] I have added the changelog accordingly.
22 | - [ ] I have read the **CONTRIBUTING** document.
23 | - [ ] I have added tests to cover my changes.
24 | - [ ] All new and existing tests passed.
25 |
26 |
--------------------------------------------------------------------------------
/.github/workflows/codspeed.yml:
--------------------------------------------------------------------------------
1 | name: CodSpeed
2 |
3 | on:
4 | push:
5 | branches:
6 | - develop
7 | pull_request:
8 | # `workflow_dispatch` allows CodSpeed to trigger backtest
9 | # performance analysis in order to generate initial data.
10 | workflow_dispatch:
11 |
12 | jobs:
13 | benchmarks:
14 | name: Run benchmarks
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 | - uses: actions/setup-python@v5
19 | with:
20 | # 3.12 is the minimum reqquired version for profiling enabled
21 | python-version: "3.12"
22 |
23 | - name: Install and configure Poetry
24 | run: |
25 | pip install -U pip poetry
26 | poetry config virtualenvs.create false
27 |
28 | - name: Install dependencies
29 | run: make build
30 |
31 | - name: Run benchmarks
32 | uses: CodSpeedHQ/action@v3
33 | with:
34 | token: ${{ secrets.CODSPEED_TOKEN }}
35 | run: pytest tests/benchmarks --codspeed
36 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages-release.yml:
--------------------------------------------------------------------------------
1 | name: gh-pages-release
2 | on:
3 | release:
4 | types:
5 | - created
6 | jobs:
7 | publish:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | - uses: actions/setup-python@v5
12 | with:
13 | python-version: "3.9"
14 | - name: Install and configure Poetry
15 | run: |
16 | pip install -U pip poetry
17 | poetry config virtualenvs.create false
18 | - name: Build docs
19 | run: make docs
20 | - name: Deploy release docs
21 | uses: peaceiris/actions-gh-pages@v3
22 | with:
23 | personal_token: ${{ secrets.PERSONAL_TOKEN }}
24 | publish_dir: build/html
25 | external_repository: tortoise/tortoise.github.io
26 | publish_branch: master
27 | destination_dir: ${{ github.ref_name }}
28 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: gh-pages
2 | on:
3 | workflow_dispatch:
4 | release:
5 | types:
6 | - created
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: actions/setup-python@v5
13 | with:
14 | python-version: "3.9"
15 | - name: Install and configure Poetry
16 | run: |
17 | pip install -U pip poetry
18 | poetry config virtualenvs.create false
19 | - name: Build docs
20 | run: make docs
21 | - name: Deploy latest docs
22 | uses: peaceiris/actions-gh-pages@v3
23 | with:
24 | personal_token: ${{ secrets.PERSONAL_TOKEN }}
25 | publish_dir: build/html
26 | external_repository: tortoise/tortoise.github.io
27 | publish_branch: main
28 |
--------------------------------------------------------------------------------
/.github/workflows/pypi.yml:
--------------------------------------------------------------------------------
1 | name: pypi
2 | on:
3 | release:
4 | types:
5 | - created
6 | jobs:
7 | publish:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v4
11 | - uses: actions/setup-python@v5
12 | with:
13 | python-version: '3.9'
14 | - name: Install and configure Poetry
15 | run: |
16 | pip install -U pip poetry
17 | poetry config virtualenvs.create false
18 | - name: Install requirements
19 | run: make deps
20 | - name: Build dists
21 | run: make build
22 | - name: Pypi Publish
23 | uses: pypa/gh-action-pypi-publish@release/v1
24 | with:
25 | user: __token__
26 | password: ${{ secrets.pypi_password }}
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Python template
3 | # Byte-compiled / optimized / DLL files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # C extensions
9 | *.so
10 | .idea
11 | _static
12 | _build
13 | _templates
14 |
15 | # Distribution / packaging
16 | .Python
17 | env/
18 | build/
19 | develop-eggs/
20 | dist/
21 | downloads/
22 | eggs/
23 | .eggs/
24 | lib/
25 | lib64/
26 | parts/
27 | sdist/
28 | var/
29 | wheels/
30 | *.egg-info/
31 | .installed.cfg
32 | *.egg
33 |
34 | # PyInstaller
35 | # Usually these files are written by a python script from a template
36 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
37 | *.manifest
38 | *.spec
39 |
40 | # Installer logs
41 | pip-log.txt
42 | pip-delete-this-directory.txt
43 |
44 | # Unit test / coverage reports
45 | htmlcov/
46 | .tox/
47 | .coverage
48 | .coverage.*
49 | .cache
50 | nosetests.xml
51 | coverage.xml
52 | *,cover
53 | .hypothesis/
54 | .vscode/
55 | examples/*.sqlite3
56 | examples/*/*.sqlite3*
57 |
58 | # Translations
59 | *.mo
60 | *.pot
61 |
62 | # Django stuff:
63 | *.log
64 | local_settings.py
65 |
66 | # Flask stuff:
67 | instance/
68 | .webassets-cache
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | target/
78 |
79 | # Jupyter Notebook
80 | .ipynb_checkpoints
81 |
82 | # pyenv
83 | .python-version
84 |
85 | # celery beat schedule file
86 | celerybeat-schedule
87 |
88 | # dotenv
89 | .env
90 | .env3
91 |
92 | # virtualenv
93 | .venv
94 | venv/
95 | ENV/
96 | .pyenv/
97 |
98 | # Spyder project settings
99 | .spyderproject
100 |
101 | # Rope project settings
102 | .ropeproject
103 |
104 | # KDevelop
105 | .kdev4/
106 | *.kdev4
107 |
108 | # MyPy caches
109 | .mypy_cache/
110 |
111 | # macos
112 | .DS_Store
113 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.rst:
--------------------------------------------------------------------------------
1 | ======
2 | Thanks
3 | ======
4 |
5 | Contributors
6 | ============
7 |
8 | * Andrey Bondar ``@abondar``
9 | * Nickolas Grigoriadis ``@grigi``
10 | * ``@etzelwu``
11 | * Alexander Lyon ``@arlyon``
12 | * Florimond Manca ``@florimondmanca``
13 | * Vitali Rebkavets ``@REVIMI``
14 | * Shlomy Balulu ``@shaloba``
15 | * Klaas Nebuhr ``@FirstKlaas``
16 | * Harsha Narayana ``@harshanarayana``
17 | * Terra Brown ``@superloach``
18 | * Nguyễn Hồng Quân ``@hongquan``
19 | * Mateusz Bocian ``@mrstork``
20 | * Vladimir Urushev ``@pilat``
21 | * Adam Wallner ``@wallneradam``
22 | * Zoltán Szeredi ``@zoliszeredi``
23 | * Rebecca Klauser ``@svms1``
24 | * Sina Sohangir ``@sinaso``
25 | * Weiming Dong ``@dongweiming``
26 | * ``@AEnterprise``
27 | * Jinlong Peng ``@long2ice``
28 | * Sang-Heon Jeon ``@lntuition``
29 | * Jong-Yeop Park ``@pjongy``
30 | * ``@sm0k``
31 | * Lev Gorodetskiy ``@droserasprout``
32 | * Hao Gong ``@dongfangtianyu``
33 | * Peng Gao ``@Priestch``
34 | * Mykola Solodukha ``@TrDex``
35 | * Seo Jiyeon ``jiyeonseo``
36 | * Blake Watters ``blakewatters``
37 | * Alexey Tylindus ``@mirrorrim``
38 | * Rodrigo Oliveira ``@allrod5``
39 | * ``@SnkSynthesis``
40 | * Alex Sinichkin ``@AlwxSin``
41 | * ``@Yolley``
42 | * Weiliang Li ``@kigawas``
43 | * Bogdan Evstratenko ``@evstratbg``
44 | * Mike Ryan ``@devsetgo``
45 | * Eugene Dubovskoy ``@drjackild``
46 | * Lương Quang Mạnh ``@lqmanh``
47 | * Mykyta Holubakha ``@Hummer12007``
48 | * Tiago Barrionuevo ``@tiabogar``
49 | * Isaque Alves ``@isaquealves``
50 | * Vinay Karanam ``@vinayinvicible``
51 | * Aleksandr Rozum ``@rozumalex``
52 | * Mojix Coder ``@MojixCoder``
53 | * Paul Serov ``@thakryptex``
54 | * Stanislav Zmiev ``@Ovsyanka83``
55 | * Waket Zheng ``@waketzheng``
56 | * Yuval Ben-Arie ``@yuvalbenarie``
57 | * Stephan Klein ``@privatwolke``
58 | * ``@WizzyGeek``
59 | * Ivan Pakeev ``@ipakeev``
60 | * Abdeldjalil Hezouat ``@Abdeldjalil-H``
61 | * Andrea Magistà ``@vlakius``
62 | * Daniel Szucs ``@Quasar6X``
63 | * Rui Catarino ``@ruitcatarino``
64 | * Lance Moe ``@lancemoe``
65 | * Markus Beckschulte ``@markus-96``
66 | * Frederic Aoustin ``@fraoustin``
67 | * Ludwig Hähne ``@pankrat``
68 |
69 | Special Thanks
70 | ==============
71 |
72 | Huge thanks to https://github.com/kayak/pypika for all heavy lifting.
73 |
--------------------------------------------------------------------------------
/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 |
5 | from tortoise.contrib.test import finalizer, initializer
6 |
7 |
8 | @pytest.fixture(scope="session", autouse=True)
9 | def initialize_tests(request):
10 | # Reduce the default timeout for psycopg because the tests become very slow otherwise
11 | try:
12 | from tortoise.backends.psycopg import PsycopgClient
13 |
14 | PsycopgClient.default_timeout = float(os.environ.get("TORTOISE_POSTGRES_TIMEOUT", "15"))
15 | except ImportError:
16 | pass
17 |
18 | db_url = os.getenv("TORTOISE_TEST_DB", "sqlite://:memory:")
19 | initializer(["tests.testmodels"], db_url=db_url)
20 | request.addfinalizer(finalizer)
21 |
--------------------------------------------------------------------------------
/docs/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | ../CHANGELOG.rst
--------------------------------------------------------------------------------
/docs/CONTRIBUTORS.rst:
--------------------------------------------------------------------------------
1 | ../CONTRIBUTORS.rst
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = tortoise
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/ORM_Perf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/docs/ORM_Perf.png
--------------------------------------------------------------------------------
/docs/_static/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/docs/_static/.keep
--------------------------------------------------------------------------------
/docs/_static/tortoise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/docs/_static/tortoise.png
--------------------------------------------------------------------------------
/docs/_templates/space.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/cli.rst:
--------------------------------------------------------------------------------
1 | .. _cli:
2 |
3 | ===========
4 | TortoiseCLI
5 | ===========
6 |
7 | This document describes how to use `tortoise-cli`, a cli tool for tortoise-orm, build on top of click and ptpython.
8 |
9 | You can see `https://github.com/tortoise/tortoise-cli `_ for more details.
10 |
11 |
12 | Quick Start
13 | ===========
14 |
15 | .. code-block:: shell
16 |
17 | > tortoise-cli -h 23:59:38
18 | Usage: tortoise-cli [OPTIONS] COMMAND [ARGS]...
19 |
20 | Options:
21 | -V, --version Show the version and exit.
22 | -c, --config TEXT TortoiseORM config dictionary path, like settings.TORTOISE_ORM
23 | -h, --help Show this message and exit.
24 |
25 | Commands:
26 | shell Start an interactive shell.
27 |
28 | Usage
29 | =====
30 |
31 | First, you need make a TortoiseORM config object, assuming that in `settings.py`.
32 |
33 | .. code-block:: shell
34 |
35 | TORTOISE_ORM = {
36 | "connections": {
37 | "default": "sqlite://:memory:",
38 | },
39 | "apps": {
40 | "models": {"models": ["examples.models"], "default_connection": "default"},
41 | },
42 | }
43 |
44 |
45 | Interactive shell
46 | =================
47 |
48 | Then you can start an interactive shell for TortoiseORM.
49 |
50 | .. code-block:: shell
51 |
52 | tortoise-cli -c settings.TORTOISE_ORM shell
53 |
54 |
55 | Or you can set config by set environment variable.
56 |
57 | .. code-block:: shell
58 |
59 | export TORTOISE_ORM=settings.TORTOISE_ORM
60 |
61 | Then just run:
62 |
63 | .. code-block:: shell
64 |
65 | tortoise-cli shell
66 |
--------------------------------------------------------------------------------
/docs/connections.rst:
--------------------------------------------------------------------------------
1 | .. _connections:
2 |
3 | ===========
4 | Connections
5 | ===========
6 |
7 | This document describes how to access the underlying connection object (:ref:`BaseDBAsyncClient`) for the aliases defined
8 | as part of the DB config passed to the :meth:`Tortoise.init` call.
9 |
10 | Below is a simple code snippet which shows how the interface can be accessed:
11 |
12 | .. code-block:: python3
13 |
14 | # connections is a singleton instance of the ConnectionHandler class and serves as the
15 | # entrypoint to access all connection management APIs.
16 | from tortoise import connections
17 |
18 |
19 | # Assume that this is the Tortoise configuration used
20 | await Tortoise.init(
21 | {
22 | "connections": {
23 | "default": {
24 | "engine": "tortoise.backends.sqlite",
25 | "credentials": {"file_path": "example.sqlite3"},
26 | }
27 | },
28 | "apps": {
29 | "events": {"models": ["__main__"], "default_connection": "default"}
30 | },
31 | }
32 | )
33 |
34 | conn: BaseDBAsyncClient = connections.get("default")
35 | try:
36 | await conn.execute_query('SELECT * FROM "event"')
37 | except OperationalError:
38 | print("Expected it to fail")
39 |
40 | .. important::
41 | The :ref:`tortoise.connection.ConnectionHandler` class has been implemented with the singleton
42 | pattern in mind and so when the ORM initializes, a singleton instance of this class
43 | ``tortoise.connection.connections`` is created automatically and lives in memory up until the lifetime of the app.
44 | Any attempt to modify or override its behaviour at runtime is risky and not recommended.
45 |
46 |
47 | Please refer to :ref:`this example` for a detailed demonstration of how this API can be used
48 | in practice.
49 |
50 |
51 | API Reference
52 | ===========
53 |
54 | .. _connection_handler:
55 |
56 | .. automodule:: tortoise.connection
57 | :members:
58 | :undoc-members:
--------------------------------------------------------------------------------
/docs/contrib.rst:
--------------------------------------------------------------------------------
1 | Contrib
2 | =======
3 |
4 | .. toctree::
5 | :maxdepth: 3
6 |
7 | contrib/linters
8 | contrib/pydantic
9 | contrib/unittest
10 | contrib/fastapi
11 | contrib/quart
12 | contrib/sanic
13 | contrib/starlette
14 | contrib/aiohttp
15 | contrib/mysql
16 | contrib/postgres
17 | contrib/blacksheep
18 |
--------------------------------------------------------------------------------
/docs/contrib/aiohttp.rst:
--------------------------------------------------------------------------------
1 | .. _contrib_aiohttp:
2 |
3 | ================================
4 | Tortoise-ORM aiohttp integration
5 | ================================
6 |
7 | We have a lightweight integration util ``tortoise.contrib.aiohttp`` which has a single function ``register_tortoise`` which sets up Tortoise-ORM on startup and cleans up on teardown.
8 |
9 | See the :ref:`example_aiohttp`
10 |
11 | Reference
12 | =========
13 |
14 | .. automodule:: tortoise.contrib.aiohttp
15 | :members:
16 | :undoc-members:
17 | :show-inheritance:
18 |
--------------------------------------------------------------------------------
/docs/contrib/blacksheep.rst:
--------------------------------------------------------------------------------
1 | .. _contrib_blacksheep:
2 |
3 | ===================================
4 | Tortoise-ORM BlackSheep integration
5 | ===================================
6 |
7 | We have a lightweight integration util ``tortoise.contrib.blacksheep`` which has a single function ``register_tortoise`` which sets up Tortoise-ORM on startup and cleans up on teardown.
8 |
9 | BlackSheep is an asynchronous web framework to build event based web applications with Python.
10 |
11 |
12 | See the :ref:`example_blacksheep` & have a look at the :ref:`contrib_pydantic` tutorials.
13 |
14 | Reference
15 | =========
16 |
17 | .. automodule:: tortoise.contrib.blacksheep
18 | :members:
19 | :undoc-members:
20 | :show-inheritance:
21 |
--------------------------------------------------------------------------------
/docs/contrib/fastapi.rst:
--------------------------------------------------------------------------------
1 | .. _contrib_fastapi:
2 |
3 | ================================
4 | Tortoise-ORM FastAPI integration
5 | ================================
6 |
7 | We have a lightweight integration util ``tortoise.contrib.fastapi`` which has a class ``RegisterTortoise`` that can be used to set/clean up Tortoise-ORM in lifespan context.
8 |
9 | FastAPI is basically Starlette & Pydantic, but in a very specific way.
10 |
11 |
12 | See the :ref:`example_fastapi` & have a look at the :ref:`contrib_pydantic` tutorials.
13 |
14 | Reference
15 | =========
16 |
17 | .. automodule:: tortoise.contrib.fastapi
18 | :members:
19 | :undoc-members:
20 | :show-inheritance:
21 |
--------------------------------------------------------------------------------
/docs/contrib/linters.rst:
--------------------------------------------------------------------------------
1 | =======
2 | Linters
3 | =======
4 |
5 | .. _pylint:
6 |
7 | PyLint plugin
8 | =============
9 |
10 | Since Tortoise ORM uses MetaClasses to build the Model objects, PyLint will often not understand how the Models behave. We provided a `tortoise.pylint` plugin that enhances PyLints understanding of Models and Fields.
11 |
12 | Usage
13 | -----
14 |
15 | In your projects ``.pylintrc`` file, ensure the following is set:
16 |
17 | .. code-block:: ini
18 |
19 | load-plugins=tortoise.contrib.pylint
20 |
21 |
22 |
--------------------------------------------------------------------------------
/docs/contrib/mysql.rst:
--------------------------------------------------------------------------------
1 | .. _contrib_mysql:
2 |
3 | =====
4 | MySQL
5 | =====
6 |
7 | Indexes
8 | =======
9 |
10 | MySQL specific indexes.
11 |
12 | .. autoclass:: tortoise.contrib.mysql.indexes.FullTextIndex
13 | .. autoclass:: tortoise.contrib.mysql.indexes.SpatialIndex
14 |
15 | Fields
16 | ======
17 |
18 | MySQL specific fields.
19 |
20 | .. autoclass:: tortoise.contrib.mysql.fields.GeometryField
21 | .. autoclass:: tortoise.contrib.mysql.fields.UUIDField
22 |
23 | Search
24 | ======
25 |
26 | MySQL full text search.
27 |
28 | .. autoclass:: tortoise.contrib.mysql.search.SearchCriterion
29 |
--------------------------------------------------------------------------------
/docs/contrib/postgres.rst:
--------------------------------------------------------------------------------
1 | .. _contrib_postgre:
2 |
3 | ========
4 | Postgres
5 | ========
6 |
7 | Indexes
8 | =======
9 |
10 | Postgres specific indexes.
11 |
12 | .. autoclass:: tortoise.contrib.postgres.indexes.BloomIndex
13 | .. autoclass:: tortoise.contrib.postgres.indexes.BrinIndex
14 | .. autoclass:: tortoise.contrib.postgres.indexes.GinIndex
15 | .. autoclass:: tortoise.contrib.postgres.indexes.GistIndex
16 | .. autoclass:: tortoise.contrib.postgres.indexes.HashIndex
17 | .. autoclass:: tortoise.contrib.postgres.indexes.SpGistIndex
18 |
19 | Fields
20 | ======
21 |
22 | Postgres specific fields.
23 |
24 | .. autoclass:: tortoise.contrib.postgres.fields.ArrayField
25 | .. autoclass:: tortoise.contrib.postgres.fields.TSVectorField
26 |
27 |
28 | Functions
29 | =========
30 |
31 | .. autoclass:: tortoise.contrib.postgres.functions.ToTsVector
32 | .. autoclass:: tortoise.contrib.postgres.functions.ToTsQuery
33 | .. autoclass:: tortoise.contrib.postgres.functions.PlainToTsQuery
34 |
35 | Search
36 | ======
37 |
38 | Postgres full text search.
39 |
40 | .. autoclass:: tortoise.contrib.postgres.search.SearchCriterion
41 |
--------------------------------------------------------------------------------
/docs/contrib/quart.rst:
--------------------------------------------------------------------------------
1 | .. _contrib_quart:
2 |
3 | ==============================
4 | Tortoise-ORM Quart integration
5 | ==============================
6 |
7 | We have a lightweight integration util ``tortoise.contrib.quart`` which has a single function ``register_tortoise`` which sets up Tortoise-ORM on startup and cleans up on teardown.
8 |
9 | Note that the modules path can not be ``__main__`` as that changes depending on the launch point. One wants to be able to launch a Quart service from the ASGI runner directly, so all paths need to be explicit.
10 |
11 | See the :ref:`example_quart`
12 |
13 | Usage
14 | =====
15 |
16 | .. code-block:: sh
17 |
18 | QUART_APP=main quart
19 | ...
20 | Commands:
21 | generate-schemas Populate DB with Tortoise-ORM schemas.
22 | run Start and run a development server.
23 | shell Open a shell within the app context.
24 |
25 | # To generate schemas
26 | QUART_APP=main quart generate-schemas
27 |
28 | # To run
29 | QUART_APP=main quart run
30 |
31 |
32 | Reference
33 | =========
34 |
35 | .. automodule:: tortoise.contrib.quart
36 | :members:
37 | :undoc-members:
38 | :show-inheritance:
39 |
--------------------------------------------------------------------------------
/docs/contrib/sanic.rst:
--------------------------------------------------------------------------------
1 | .. _contrib_sanic:
2 |
3 | ==============================
4 | Tortoise-ORM Sanic integration
5 | ==============================
6 |
7 | We have a lightweight integration util ``tortoise.contrib.sanic`` which has a single function ``register_tortoise`` which sets up Tortoise-ORM on startup and cleans up on teardown.
8 |
9 | See the :ref:`example_sanic`
10 |
11 | Reference
12 | =========
13 |
14 | .. automodule:: tortoise.contrib.sanic
15 | :members:
16 | :undoc-members:
17 | :show-inheritance:
18 |
--------------------------------------------------------------------------------
/docs/contrib/starlette.rst:
--------------------------------------------------------------------------------
1 | .. _contrib_starlette:
2 |
3 | ==================================
4 | Tortoise-ORM Starlette integration
5 | ==================================
6 |
7 | We have a lightweight integration util ``tortoise.contrib.starlette`` which has a single function ``register_tortoise`` which sets up Tortoise-ORM on startup and cleans up on teardown.
8 |
9 | See the :ref:`example_starlette`
10 |
11 | Reference
12 | =========
13 |
14 | .. automodule:: tortoise.contrib.starlette
15 | :members:
16 | :undoc-members:
17 | :show-inheritance:
18 |
--------------------------------------------------------------------------------
/docs/examples.rst:
--------------------------------------------------------------------------------
1 | .. _examples:
2 |
3 | ========
4 | Examples
5 | ========
6 |
7 | .. toctree::
8 | :maxdepth: 3
9 |
10 | examples/basic
11 | examples/pydantic
12 | examples/fastapi
13 | examples/quart
14 | examples/sanic
15 | examples/starlette
16 | examples/aiohttp
17 | examples/blacksheep
18 |
--------------------------------------------------------------------------------
/docs/examples/aiohttp.rst:
--------------------------------------------------------------------------------
1 | .. _example_aiohttp:
2 |
3 | ===============
4 | AIOHTTP Example
5 | ===============
6 |
7 | This is an example of the :ref:`contrib_aiohttp`
8 |
9 | **Usage:**
10 |
11 | .. code-block:: sh
12 |
13 | python3 main.py
14 |
15 |
16 | models.py
17 | =========
18 | .. literalinclude:: ../../examples/aiohttp/models.py
19 |
20 | main.py
21 | =======
22 | .. literalinclude:: ../../examples/aiohttp/main.py
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/examples/blacksheep.rst:
--------------------------------------------------------------------------------
1 | .. _example_blacksheep:
2 |
3 | ===================
4 | BlackSheep Examples
5 | ===================
6 |
7 | This is an example of the :ref:`contrib_blacksheep`
8 |
9 | **Usage:**
10 |
11 | .. code-block:: sh
12 |
13 | uvicorn server:app --reload
14 |
15 |
16 | .. rst-class:: emphasize-children
17 |
18 | Basic non-relational example
19 | ============================
20 |
21 | models.py
22 | ---------
23 | .. literalinclude:: ../../examples/blacksheep/models.py
24 |
25 | test_api.py
26 | --------
27 | .. literalinclude:: ../../examples/blacksheep/test_api.py
28 |
29 | server.py
30 | -------
31 | .. literalinclude:: ../../examples/blacksheep/server.py
32 |
--------------------------------------------------------------------------------
/docs/examples/fastapi.rst:
--------------------------------------------------------------------------------
1 | .. _example_fastapi:
2 |
3 | ================
4 | FastAPI Examples
5 | ================
6 |
7 | This is an example of the :ref:`contrib_fastapi`
8 |
9 | **Usage:**
10 |
11 | .. code-block:: sh
12 |
13 | uvicorn main:app --reload
14 |
15 |
16 | .. rst-class:: emphasize-children
17 |
18 | Basic non-relational example
19 | ============================
20 |
21 | models.py
22 | ---------
23 | .. literalinclude:: ../../examples/fastapi/models.py
24 |
25 | tests.py
26 | --------
27 | .. literalinclude:: ../../examples/fastapi/_tests.py
28 |
29 | main.py
30 | -------
31 | .. literalinclude:: ../../examples/fastapi/main.py
32 |
--------------------------------------------------------------------------------
/docs/examples/pydantic.rst:
--------------------------------------------------------------------------------
1 | .. _examples_pydantic:
2 |
3 | =================
4 | Pydantic Examples
5 | =================
6 |
7 | See :ref:`contrib_pydantic`
8 |
9 | .. rst-class:: html-toggle
10 |
11 | .. _example_pydantic_basic:
12 |
13 | Basic Pydantic
14 | ==============
15 | .. literalinclude:: ../../examples/pydantic/basic.py
16 |
17 |
18 | .. rst-class:: html-toggle
19 |
20 | .. _example_pydantic_early_init:
21 |
22 | Early model Init
23 | ================
24 | .. literalinclude:: ../../examples/pydantic/early_init.py
25 |
26 |
27 | .. rst-class:: html-toggle
28 |
29 | .. _example_pydantic_recursive:
30 |
31 | Recursive models + Computed fields
32 | ==================================
33 | .. literalinclude:: ../../examples/pydantic/recursive.py
34 |
35 |
36 | .. _example_pydantic_tutorials:
37 |
38 | .. rst-class:: emphasize-children
39 |
40 | Tutorial sources
41 | ================
42 |
43 | .. _example_pydantic_tut1:
44 |
45 | .. rst-class:: html-toggle
46 |
47 | 1: Basic usage
48 | --------------
49 | .. literalinclude:: ../../examples/pydantic/tutorial_1.py
50 |
51 | .. _example_pydantic_tut2:
52 |
53 | .. rst-class:: html-toggle
54 |
55 | 2: Querysets & Lists
56 | --------------------
57 | .. literalinclude:: ../../examples/pydantic/tutorial_2.py
58 |
59 | .. _example_pydantic_tut3:
60 |
61 | .. rst-class:: html-toggle
62 |
63 | 3: Relations & Early-init
64 | -------------------------
65 | .. literalinclude:: ../../examples/pydantic/tutorial_3.py
66 |
67 | .. _example_pydantic_tut4:
68 |
69 | .. rst-class:: html-toggle
70 |
71 | 4: PydanticMeta & Callables
72 | ---------------------------
73 | .. literalinclude:: ../../examples/pydantic/tutorial_4.py
74 |
--------------------------------------------------------------------------------
/docs/examples/quart.rst:
--------------------------------------------------------------------------------
1 | .. _example_quart:
2 |
3 | =============
4 | Quart Example
5 | =============
6 |
7 | This is an example of the :ref:`contrib_quart`
8 |
9 | **Usage:**
10 |
11 | .. code-block:: sh
12 |
13 | QUART_APP=main quart
14 | ...
15 | Commands:
16 | generate-schemas Populate DB with Tortoise-ORM schemas.
17 | run Start and run a development server.
18 | shell Open a shell within the app context.
19 |
20 | # To generate schemas
21 | QUART_APP=main quart generate-schemas
22 |
23 | # To run
24 | QUART_APP=main quart run
25 |
26 |
27 | models.py
28 | =========
29 | .. literalinclude:: ../../examples/quart/models.py
30 |
31 | main.py
32 | =======
33 | .. literalinclude:: ../../examples/quart/main.py
34 |
35 |
36 |
--------------------------------------------------------------------------------
/docs/examples/sanic.rst:
--------------------------------------------------------------------------------
1 | .. _example_sanic:
2 |
3 | =============
4 | Sanic Example
5 | =============
6 |
7 | This is an example of the :ref:`contrib_sanic`
8 |
9 | **Usage:**
10 |
11 | .. code-block:: sh
12 |
13 | python3 main.py
14 |
15 |
16 | models.py
17 | =========
18 | .. literalinclude:: ../../examples/sanic/models.py
19 |
20 | main.py
21 | =======
22 | .. literalinclude:: ../../examples/sanic/main.py
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/examples/starlette.rst:
--------------------------------------------------------------------------------
1 | .. _example_starlette:
2 |
3 | =================
4 | Starlette Example
5 | =================
6 |
7 | This is an example of the :ref:`contrib_starlette`
8 |
9 | **Usage:**
10 |
11 | .. code-block:: sh
12 |
13 | python3 main.py
14 |
15 |
16 | models.py
17 | =========
18 | .. literalinclude:: ../../examples/starlette/models.py
19 |
20 | main.py
21 | =======
22 | .. literalinclude:: ../../examples/starlette/main.py
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/exceptions.rst:
--------------------------------------------------------------------------------
1 | ==========
2 | Exceptions
3 | ==========
4 |
5 |
6 | .. automodule:: tortoise.exceptions
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
11 |
--------------------------------------------------------------------------------
/docs/indexes.rst:
--------------------------------------------------------------------------------
1 | .. _index:
2 |
3 | =======
4 | Indexes
5 | =======
6 |
7 | Default tortoise use `BTree` index when define index use `db_index=True` in field, or define indexes use in `Meta` class, but if you want use other index types, like `FullTextIndex` in `MySQL`, or `GinIndex` in `Postgres`, you should use `tortoise.indexes.Index` and its subclasses.
8 |
9 | Usage
10 | =====
11 |
12 | Following is example which use `FullTextIndex` and `SpatialIndex` of `MySQL`:
13 |
14 | .. code-block:: python3
15 |
16 | from tortoise import Model, fields
17 | from tortoise.contrib.mysql.fields import GeometryField
18 | from tortoise.contrib.mysql.indexes import FullTextIndex, SpatialIndex
19 |
20 |
21 | class Index(Model):
22 | full_text = fields.TextField()
23 | geometry = GeometryField()
24 |
25 | class Meta:
26 | indexes = [
27 | FullTextIndex(fields={"full_text"}, parser_name="ngram"),
28 | SpatialIndex(fields={"geometry"}),
29 | ]
30 |
31 | Some built-in indexes can be found in `tortoise.contrib.mysql.indexes` and `tortoise.contrib.postgres.indexes`.
32 |
33 | Extending Index
34 | ===============
35 |
36 | Extending index is simply, you just need to inherit the `tortoise.indexes.Index`, following is example how to create `FullTextIndex`:
37 |
38 | .. code-block:: python3
39 |
40 | from typing import Optional, Set
41 | from pypika_tortoise.terms import Term
42 | from tortoise.indexes import Index
43 |
44 | class FullTextIndex(Index):
45 | INDEX_TYPE = "FULLTEXT"
46 |
47 | def __init__(
48 | self,
49 | *expressions: Term,
50 | fields: Optional[Set[str]] = None,
51 | name: Optional[str] = None,
52 | parser_name: Optional[str] = None,
53 | ):
54 | super().__init__(*expressions, fields=fields, name=name)
55 | if parser_name:
56 | self.extra = f" WITH PARSER {parser_name}"
57 |
58 | Differently for `Postgres`, you should inherit `tortoise.contrib.postgres.indexes.PostgresIndex`:
59 |
60 | .. code-block:: python3
61 |
62 | class BloomIndex(PostgreSQLIndex):
63 | INDEX_TYPE = "BLOOM"
64 |
--------------------------------------------------------------------------------
/docs/manager.rst:
--------------------------------------------------------------------------------
1 | .. _manager:
2 |
3 | =======
4 | Manager
5 | =======
6 |
7 | A Manager is the interface through which database query operations are provided to tortoise models.
8 |
9 | There is one default Manager for every tortoise model.
10 |
11 | Usage
12 | =====
13 |
14 | There are two ways to use a Manager, one is use `manager` in `Meta` to override the default `manager`, another is define manager in model:
15 |
16 | .. code-block:: python3
17 |
18 | from tortoise.manager import Manager
19 |
20 | class StatusManager(Manager):
21 | def get_queryset(self):
22 | return super(StatusManager, self).get_queryset().filter(status=1)
23 |
24 |
25 | class ManagerModel(Model):
26 | status = fields.IntField(default=0)
27 | all_objects = Manager()
28 |
29 | class Meta:
30 | manager = StatusManager()
31 |
32 |
33 | After override default manager, all queries like `Model.get()`, `Model.filter()` will be comply with the behavior of custom manager.
34 |
35 | In the example above, you can never get the objects which status is equal to `0` with default manager, but you can use the manager `all_objects` defined in model to get all objects.
36 |
37 | .. code-block:: python3
38 |
39 | m1 = await ManagerModel.create()
40 | m2 = await ManagerModel.create(status=1)
41 |
42 | self.assertEqual(await ManagerModel.all().count(), 1)
43 | self.assertEqual(await ManagerModel.all_objects.count(), 2)
44 |
45 | self.assertIsNone(await ManagerModel.get_or_none(pk=m1.pk))
46 | self.assertIsNotNone(await ManagerModel.all_objects.get_or_none(pk=m1.pk))
47 | self.assertIsNotNone(await ManagerModel.get_or_none(pk=m2.pk))
48 |
--------------------------------------------------------------------------------
/docs/reference.rst:
--------------------------------------------------------------------------------
1 | Reference
2 | =========
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | setup
8 | databases
9 | models
10 | fields
11 | indexes
12 | timezone
13 | schema
14 | query
15 | manager
16 | functions
17 | expressions
18 | transactions
19 | connections
20 | exceptions
21 | signals
22 | migration
23 | validators
24 | logging
25 | router
26 | cli
27 |
--------------------------------------------------------------------------------
/docs/roadmap.rst:
--------------------------------------------------------------------------------
1 | =======
2 | Roadmap
3 | =======
4 |
5 | Mid-term
6 | ========
7 |
8 | Here we have all the features that is slightly further out, in no particular order:
9 |
10 | * Performance work:
11 | * [done] Sub queries
12 | * [done] Change to all-parametrized queries
13 | * Faster MySQL driver (possibly based on mysqlclient)
14 | * Consider using Cython to accelerate critical loops
15 |
16 | * Convenience/Ease-Of-Use work:
17 | * Make ``DELETE`` honour ``limit`` and ``offset``
18 | * [done] ``.filter(field=None)`` to work as expected
19 |
20 | * Expand in the ``init`` framework:
21 | * Ability to have Management Commands
22 | * Ability to define Management Commands
23 | * Make it simple to inspect Models and Management Commands without using private APIs.
24 |
25 | * Migrations
26 | * Comprehensive schema Migrations
27 | * Automatic forward Migration building
28 | * Ability to easily run arbitrary code in a migration
29 | * Ability to get a the Models for that exact time of the migration, to ensure safe & consistent data migrations
30 | * Cross-DB support
31 | * Fixtures as a property of a migration
32 |
33 | * Serialization support
34 | * Add deserialization support
35 | * Make default serializers support some validation
36 | * Provide clean way to replace serializers with custom solution
37 |
38 | * Extra DB support
39 | * CockroachDB
40 | * Firebird
41 |
42 | * Enhanced test support
43 | * ``hypothesis`` strategy builder
44 |
45 | * Fields
46 | * Expand on standard provided fields
47 |
48 | * Documentation
49 | * Tutorials
50 |
51 | Long-term
52 | =========
53 |
54 | Become the de facto Python AsyncIO ORM.
55 |
--------------------------------------------------------------------------------
/docs/router.rst:
--------------------------------------------------------------------------------
1 | .. _router:
2 |
3 | ======
4 | Router
5 | ======
6 |
7 | The easiest way to use multiple databases is to set up a database routing scheme. The default routing scheme ensures that objects remain 'sticky' to their original database (i.e., an object retrieved from the foo database will be saved on the same database). The default routing scheme ensures that if a database isn't specified, all queries fall back to the default database.
8 |
9 | Usage
10 | =====
11 |
12 | Define Router
13 | -------------
14 |
15 | Define a router is simple, you need just write a class that has `db_for_read` and `db_for_write` methods.
16 |
17 | .. code-block:: python3
18 |
19 | class Router:
20 | def db_for_read(self, model: Type[Model]):
21 | return "slave"
22 |
23 | def db_for_write(self, model: Type[Model]):
24 | return "master"
25 |
26 | The two methods return a connection string defined in configuration.
27 |
28 | Config Router
29 | -------------
30 |
31 | Just put it in configuration of tortoise or in `Tortoise.init` method.
32 |
33 | .. code-block:: python3
34 |
35 | config = {
36 | "connections": {"master": "sqlite:///tmp/test.db", "slave": "sqlite:///tmp/test.db"},
37 | "apps": {
38 | "models": {
39 | "models": ["__main__"],
40 | "default_connection": "master",
41 | }
42 | },
43 | "routers": ["path.Router"],
44 | "use_tz": False,
45 | "timezone": "UTC",
46 | }
47 | await Tortoise.init(config=config)
48 | # or
49 | routers = config.pop('routers')
50 | await Tortoise.init(config=config, routers=routers)
51 |
52 | After that, all `select` operations will use `slave` connection, all `create/update/delete` operations will use `master` connection.
53 |
--------------------------------------------------------------------------------
/docs/schema.rst:
--------------------------------------------------------------------------------
1 | .. _schema:
2 |
3 | ===============
4 | Schema Creation
5 | ===============
6 |
7 | Here we create connection to SQLite database client and then we discover & initialize models.
8 |
9 | .. automethod:: tortoise.Tortoise.generate_schemas
10 | :noindex:
11 |
12 | ``generate_schema`` generates schema on empty database.
13 | There is also the default option when generating the schemas to set the ``safe`` parameter to ``True`` which will only insert the tables if they don't already exist.
14 |
15 |
16 | Helper Functions
17 | ================
18 |
19 | .. automodule:: tortoise.utils
20 | :members: get_schema_sql, generate_schema_for_client
21 |
--------------------------------------------------------------------------------
/docs/setup.rst:
--------------------------------------------------------------------------------
1 | ======
2 | Set up
3 | ======
4 |
5 | .. _init_app:
6 |
7 | Init app
8 | ========
9 |
10 | After you defined all your models, tortoise needs you to init them, in order to create backward relations between models and match your db client with appropriate models.
11 |
12 | You can do it like this:
13 |
14 | .. code-block:: python3
15 |
16 | from tortoise import Tortoise
17 |
18 | async def init():
19 | # Here we create a SQLite DB using file "db.sqlite3"
20 | # also specify the app name of "models"
21 | # which contain models from "app.models"
22 | await Tortoise.init(
23 | db_url='sqlite://db.sqlite3',
24 | modules={'models': ['app.models']}
25 | )
26 | # Generate the schema
27 | await Tortoise.generate_schemas()
28 |
29 |
30 | Here we create connection to SQLite database client and then we discover & initialize models.
31 |
32 | ``generate_schema`` generates schema on empty database, you shouldn't run it on every app init, run it just once, maybe out of your main code.
33 | There is also the option when generating the schemas to set the ``safe`` parameter to ``True`` which will only insert the tables if they don't already exist.
34 |
35 | If you define the variable ``__models__`` in the ``app.models`` module (or wherever you specify to load your models from), ``generate_schema`` will use that list, rather than automatically finding models for you.
36 |
37 | .. _cleaningup:
38 |
39 | The Importance of cleaning up
40 | =============================
41 |
42 | Tortoise ORM will keep connections open to external Databases. As an ``asyncio`` Python library, it needs to have the connections closed properly or the Python interpreter may still wait for the completion of said connections.
43 |
44 | To ensure connections are closed please ensure that ``Tortoise.close_connections()`` is called:
45 |
46 | .. code-block:: python3
47 |
48 | await Tortoise.close_connections()
49 |
50 | The small helper function ``tortoise.run_async()`` will ensure that connections are closed.
51 |
52 | Reference
53 | =========
54 |
55 | .. automodule:: tortoise
56 | :members:
57 | :undoc-members:
58 |
59 |
--------------------------------------------------------------------------------
/docs/signals.rst:
--------------------------------------------------------------------------------
1 | =======
2 | Signals
3 | =======
4 |
5 | There are four signals that defined now, they will be send by models.
6 |
7 | .. automodule:: tortoise.signals
8 | :members:
9 | :undoc-members:
10 | :show-inheritance:
11 |
--------------------------------------------------------------------------------
/docs/timezone.rst:
--------------------------------------------------------------------------------
1 | ========
2 | Timezone
3 | ========
4 |
5 | .. _timezone:
6 |
7 | Introduction
8 | ============
9 | The design of timezone is inspired by `Django` but also has differences. There are two config items `use_tz` and `timezone` affect timezone in tortoise, which can be set when call `Tortoise.init`. And in different DBMS there also are different behaviors.
10 |
11 | use_tz
12 | ------
13 | When set `use_tz = True`, `tortoise` will always store `UTC` time in database no matter what `timezone` set. And `MySQL` use field type `DATETIME(6)`, `PostgreSQL` use `TIMESTAMPTZ`, `SQLite` use `TIMESTAMP` when generate schema.
14 | For `TimeField`, `MySQL` use `TIME(6)`, `PostgreSQL` use `TIMETZ` and `SQLite` use `TIME`.
15 |
16 | timezone
17 | --------
18 | The `timezone` determine what `timezone` is when select `DateTimeField` and `TimeField` from database, no matter what `timezone` your database is. And you should use `tortoise.timezone.now()` get aware time instead of native time `datetime.datetime.now()`.
19 |
20 | Reference
21 | =========
22 |
23 | .. automodule:: tortoise.timezone
24 | :members:
25 | :undoc-members:
26 |
--------------------------------------------------------------------------------
/docs/toc.rst:
--------------------------------------------------------------------------------
1 | Table Of Contents
2 | =================
3 |
4 | .. toctree::
5 | :maxdepth: 5
6 | :includehidden:
7 |
8 | index
9 | getting_started
10 | reference
11 | examples
12 | contrib
13 | CHANGELOG
14 | roadmap
15 | CONTRIBUTING
16 | CONTRIBUTORS
17 |
--------------------------------------------------------------------------------
/docs/transactions.rst:
--------------------------------------------------------------------------------
1 | .. _transactions:
2 |
3 | ============
4 | Transactions
5 | ============
6 |
7 | Tortoise ORM provides a simple way to manage transactions. You can use the
8 | ``atomic()`` decorator or ``in_transaction()`` context manager.
9 |
10 | ``atomic()`` and ``in_transaction()`` can be nested. The inner blocks will create transaction savepoints,
11 | and if an exception is raised and then caught outside of a nested block, the transaction will be rolled back
12 | to the state before the block was entered. The outermost block will be the one that actually commits the transaction.
13 | The savepoints are supported for Postgres, MySQL, MSSQL and SQLite. For other databases, it is advised to
14 | propagate the exception to the outermost block to ensure that the transaction is rolled back.
15 |
16 | .. code-block:: python3
17 |
18 | # this block will commit changes on exit
19 | async with in_transaction():
20 | await MyModel.create(name='foo')
21 | try:
22 | # this block will create a savepoint and rollback to it if an exception is raised
23 | async with in_transaction():
24 | await MyModel.create(name='bar')
25 | # this will rollback to the savepoint, meaning that
26 | # the 'bar' record will not be created, however,
27 | # the 'foo' record will be created
28 | raise Exception()
29 | except Exception:
30 | pass
31 |
32 | When using ``asyncio.gather`` or similar ways to spin up concurrent tasks in a transaction block,
33 | avoid having nested transaction blocks in the concurrent tasks. Transactions are stateful and nested
34 | blocks are expected to run sequentially, not concurrently.
35 |
36 |
37 | .. automodule:: tortoise.transactions
38 | :members:
39 | :undoc-members:
40 |
--------------------------------------------------------------------------------
/docs/type_globals.py:
--------------------------------------------------------------------------------
1 | from tortoise import *
2 | from tortoise.queryset import Q
3 | from tortoise.backends.base.client import TransactionContext, TransactionalDBClient
4 |
--------------------------------------------------------------------------------
/docs/validators.rst:
--------------------------------------------------------------------------------
1 | .. _validators:
2 |
3 | ==========
4 | Validators
5 | ==========
6 |
7 | A validator is a callable for model field that takes a value and raises a `ValidationError` if it doesn’t meet some criteria.
8 |
9 | Usage
10 | =====
11 |
12 | You can pass a list of validators to `Field` parameter `validators`:
13 |
14 | .. code-block:: python3
15 |
16 | class ValidatorModel(Model):
17 | regex = fields.CharField(max_length=100, null=True, validators=[RegexValidator("abc.+", re.I)])
18 |
19 | # oh no, this will raise ValidationError!
20 | await ValidatorModel.create(regex="ccc")
21 | # this is great!
22 | await ValidatorModel.create(regex="abcd")
23 |
24 | Built-in Validators
25 | ===================
26 |
27 | Here is the list of built-in validators:
28 |
29 | .. automodule:: tortoise.validators
30 | :members:
31 | :undoc-members:
32 |
33 | Custom Validator
34 | ================
35 |
36 | There are two methods to write a custom validator, one you can write a function by passing a given value, another you can inherit `tortoise.validators.Validator` and implement `__call__`.
37 |
38 | Here is a example to write a custom validator to validate the given value is an even number:
39 |
40 | .. code-block:: python3
41 |
42 | from tortoise.validators import Validator
43 | from tortoise.exceptions import ValidationError
44 |
45 | class EvenNumberValidator(Validator):
46 | """
47 | A validator to validate whether the given value is an even number or not.
48 | """
49 | def __call__(self, value: int):
50 | if value % 2 != 0:
51 | raise ValidationError(f"Value '{value}' is not an even number")
52 |
53 | # or use function instead of class
54 | def validate_even_number(value:int):
55 | if value % 2 != 0:
56 | raise ValidationError(f"Value '{value}' is not an even number")
57 |
--------------------------------------------------------------------------------
/docs/versions.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "version": "https://tortoise.github.io",
4 | "title": "latest",
5 | "aliases": []
6 | }
7 | ]
8 |
--------------------------------------------------------------------------------
/examples/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/examples/__init__.py
--------------------------------------------------------------------------------
/examples/aiohttp/README.rst:
--------------------------------------------------------------------------------
1 | Tortoise-ORM aiohttp example
2 | ============================
3 |
4 | We have a lightweight integration util ``tortoise.contrib.aiohttp`` which has a single function ``register_tortoise`` which sets up Tortoise-ORM on startup and cleans up on teardown.
5 |
6 | Usage
7 | -----
8 |
9 | .. code-block:: sh
10 |
11 | python3 main.py
12 |
--------------------------------------------------------------------------------
/examples/aiohttp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/examples/aiohttp/__init__.py
--------------------------------------------------------------------------------
/examples/aiohttp/main.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=E0401,E0611
2 | import logging
3 |
4 | from aiohttp import web
5 | from models import Users
6 |
7 | from tortoise.contrib.aiohttp import register_tortoise
8 |
9 | logging.basicConfig(level=logging.DEBUG)
10 |
11 |
12 | async def list_all(request):
13 | users = await Users.all()
14 | return web.json_response({"users": [str(user) for user in users]})
15 |
16 |
17 | async def add_user(request):
18 | user = await Users.create(name="New User")
19 | return web.json_response({"user": str(user)})
20 |
21 |
22 | app = web.Application()
23 | app.add_routes([web.get("/", list_all), web.post("/user", add_user)])
24 | register_tortoise(
25 | app, db_url="sqlite://:memory:", modules={"models": ["models"]}, generate_schemas=True
26 | )
27 |
28 |
29 | if __name__ == "__main__":
30 | web.run_app(app, port=5000)
31 |
--------------------------------------------------------------------------------
/examples/aiohttp/models.py:
--------------------------------------------------------------------------------
1 | from tortoise import Model, fields
2 |
3 |
4 | class Users(Model):
5 | id = fields.IntField(primary_key=True)
6 | name = fields.CharField(50)
7 |
8 | def __str__(self):
9 | return f"User {self.id}: {self.name}"
10 |
--------------------------------------------------------------------------------
/examples/basic.py:
--------------------------------------------------------------------------------
1 | """
2 | This example demonstrates most basic operations with single model
3 | """
4 |
5 | from tortoise import fields, run_async
6 | from tortoise.contrib.test import init_memory_sqlite
7 | from tortoise.models import Model
8 |
9 |
10 | class Event(Model):
11 | id = fields.IntField(primary_key=True)
12 | name = fields.TextField()
13 | datetime = fields.DatetimeField(null=True)
14 |
15 | class Meta:
16 | table = "event"
17 |
18 | def __str__(self):
19 | return self.name
20 |
21 |
22 | @init_memory_sqlite
23 | async def run() -> None:
24 | event = await Event.create(name="Test")
25 | await Event.filter(id=event.id).update(name="Updated name")
26 |
27 | print(await Event.filter(name="Updated name").first())
28 | # >>> Updated name
29 |
30 | await Event(name="Test 2").save()
31 | print(await Event.all().values_list("id", flat=True))
32 | # >>> [1, 2]
33 | print(await Event.all().values("id", "name"))
34 | # >>> [{'id': 1, 'name': 'Updated name'}, {'id': 2, 'name': 'Test 2'}]
35 |
36 |
37 | if __name__ == "__main__":
38 | run_async(run())
39 |
--------------------------------------------------------------------------------
/examples/basic_comments.py:
--------------------------------------------------------------------------------
1 | """
2 | This example demonstrates most basic operations with single model
3 | and a Table definition generation with comment support
4 | """
5 |
6 | from tortoise import Tortoise, fields, run_async
7 | from tortoise.models import Model
8 |
9 |
10 | class Event(Model):
11 | id = fields.IntField(primary_key=True)
12 | name = fields.TextField(description="Name of the event that corresponds to an action")
13 | datetime = fields.DatetimeField(
14 | null=True, description="Datetime of when the event was generated"
15 | )
16 |
17 | class Meta:
18 | table = "event"
19 | table_description = "This table contains a list of all the example events"
20 |
21 | def __str__(self):
22 | return self.name
23 |
24 |
25 | async def run():
26 | await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
27 | await Tortoise.generate_schemas()
28 |
29 | event = await Event.create(name="Test")
30 | await Event.filter(id=event.id).update(name="Updated name")
31 |
32 | print(await Event.filter(name="Updated name").first())
33 |
34 | await Event(name="Test 2").save()
35 | print(await Event.all().values_list("id", flat=True))
36 | print(await Event.all().values("id", "name"))
37 |
38 |
39 | if __name__ == "__main__":
40 | run_async(run())
41 |
--------------------------------------------------------------------------------
/examples/blacksheep/README.md:
--------------------------------------------------------------------------------
1 | Tortoise-ORM BlackSheep example
2 | ============================
3 |
4 | We have a lightweight integration util ``tortoise.contrib.blacksheep`` which has a single function ``register_tortoise`` which sets up Tortoise-ORM on startup and cleans up on teardown.
5 |
6 | Usage
7 | -----
8 |
9 | .. code-block:: sh
10 |
11 | uvicorn server:app --reload
12 |
--------------------------------------------------------------------------------
/examples/blacksheep/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/examples/blacksheep/__init__.py
--------------------------------------------------------------------------------
/examples/blacksheep/_tests.py:
--------------------------------------------------------------------------------
1 | # mypy: no-disallow-untyped-decorators
2 | # pylint: disable=E0611,E0401
3 | import pytest
4 | import pytest_asyncio
5 | from blacksheep import JSONContent
6 | from blacksheep.testing import TestClient
7 | from models import Users
8 | from server import app
9 |
10 |
11 | @pytest_asyncio.fixture(scope="session", loop_scope="session")
12 | async def client(api):
13 | return TestClient(api)
14 |
15 |
16 | @pytest_asyncio.fixture(scope="session", loop_scope="session")
17 | async def api():
18 | await app.start()
19 | yield app
20 | await app.stop()
21 |
22 |
23 | @pytest.mark.asyncio
24 | async def test_get_uses_list(client: TestClient) -> None:
25 | username = "john"
26 | await Users.create(username=username)
27 |
28 | response = await client.get("/")
29 | assert response.status == 200 # nosec
30 |
31 | data = await response.json()
32 | assert len(data) == 1 # nosec
33 |
34 | user_data = data[0]
35 | assert user_data["username"] == username # nosec
36 | user_id = user_data["id"]
37 |
38 | user_obj = await Users.get(id=user_id)
39 | assert str(user_obj.id) == user_id # nosec
40 |
41 |
42 | @pytest.mark.asyncio
43 | async def test_create_user(client: TestClient) -> None:
44 | username = "john"
45 |
46 | response = await client.post("/", content=JSONContent({"username": username}))
47 | assert response.status == 201 # nosec
48 |
49 | data = await response.json()
50 | assert data["username"] == username # nosec
51 | user_id = data["id"]
52 |
53 | user_obj = await Users.get(id=user_id)
54 | assert str(user_obj.id) == user_id # nosec
55 |
56 |
57 | @pytest.mark.asyncio
58 | async def test_update_user(client: TestClient) -> None: # nosec
59 | user = await Users.create(username="john")
60 |
61 | username = "john_doe"
62 | response = await client.put(f"/{user.id}", content=JSONContent({"username": username}))
63 | assert response.status == 200 # nosec
64 |
65 | data = await response.json()
66 | assert data["username"] == username # nosec
67 | user_id = data["id"]
68 |
69 | user_obj = await Users.get(id=user_id)
70 | assert str(user_obj.id) == user_id # nosec
71 |
72 |
73 | @pytest.mark.asyncio
74 | async def test_delete_user(client: TestClient) -> None:
75 | user = await Users.create(username="john")
76 |
77 | response = await client.delete(f"/{user.id}")
78 | assert response.status == 204 # nosec
79 |
80 | data = await response.json()
81 | assert data is None # nosec
82 |
83 | assert await Users.filter(id=user.id).exists() is False # nosec
84 |
--------------------------------------------------------------------------------
/examples/blacksheep/models.py:
--------------------------------------------------------------------------------
1 | from tortoise import fields, models
2 | from tortoise.contrib.pydantic import pydantic_model_creator
3 |
4 |
5 | class Users(models.Model):
6 | id = fields.UUIDField(primary_key=True)
7 | username = fields.CharField(max_length=63)
8 |
9 | def __str__(self) -> str:
10 | return f"User {self.id}: {self.username}"
11 |
12 |
13 | UserPydanticOut = pydantic_model_creator(Users, name="UserOut")
14 | UserPydanticIn = pydantic_model_creator(Users, name="UserIn", exclude_readonly=True)
15 |
--------------------------------------------------------------------------------
/examples/blacksheep/server.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=E0401,E0611
2 | from __future__ import annotations
3 |
4 | from uuid import UUID
5 |
6 | from blacksheep import Response
7 | from blacksheep.server import Application
8 | from blacksheep.server.openapi.v3 import Info, OpenAPIHandler
9 | from blacksheep.server.responses import created, no_content, ok
10 | from models import UserPydanticIn, UserPydanticOut, Users
11 |
12 | from tortoise.contrib.blacksheep import register_tortoise
13 |
14 | app = Application()
15 | register_tortoise(
16 | app,
17 | db_url="sqlite://:memory:",
18 | modules={"models": ["models"]},
19 | generate_schemas=True,
20 | add_exception_handlers=True,
21 | )
22 |
23 |
24 | docs = OpenAPIHandler(info=Info(title="Tortoise ORM BlackSheep example", version="0.0.1"))
25 | docs.bind_app(app)
26 |
27 |
28 | @app.router.get("/")
29 | async def users_list() -> UserPydanticOut:
30 | return ok(await UserPydanticOut.from_queryset(Users.all()))
31 |
32 |
33 | @app.router.post("/")
34 | async def users_create(user: UserPydanticIn) -> UserPydanticOut:
35 | user = await Users.create(**user.model_dump(exclude_unset=True))
36 | return created(await UserPydanticOut.from_tortoise_orm(user))
37 |
38 |
39 | @app.router.patch("/{id}")
40 | async def users_patch(id: UUID, user: UserPydanticIn) -> UserPydanticOut:
41 | await Users.filter(id=id).update(**user.model_dump(exclude_unset=True))
42 | return ok(await UserPydanticOut.from_tortoise_orm(await Users.get(id=id)))
43 |
44 |
45 | @app.router.put("/{id}")
46 | async def users_put(id: UUID, user: UserPydanticIn) -> UserPydanticOut:
47 | await Users.filter(id=id).update(**user.model_dump())
48 | return ok(await UserPydanticOut.from_tortoise_orm(await Users.get(id=id)))
49 |
50 |
51 | @app.router.delete("/{id}")
52 | async def users_delete(id: UUID) -> Response:
53 | await Users.filter(id=id).delete()
54 | return no_content()
55 |
--------------------------------------------------------------------------------
/examples/complex_prefetching.py:
--------------------------------------------------------------------------------
1 | from tortoise import Tortoise, fields, run_async
2 | from tortoise.models import Model
3 | from tortoise.query_utils import Prefetch
4 |
5 |
6 | class Tournament(Model):
7 | id = fields.IntField(primary_key=True)
8 | name = fields.TextField()
9 |
10 | events: fields.ReverseRelation["Event"]
11 |
12 | def __str__(self):
13 | return self.name
14 |
15 |
16 | class Event(Model):
17 | id = fields.IntField(primary_key=True)
18 | name = fields.TextField()
19 | tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
20 | "models.Tournament", related_name="events"
21 | )
22 | participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
23 | "models.Team", related_name="events", through="event_team"
24 | )
25 |
26 | def __str__(self):
27 | return self.name
28 |
29 |
30 | class Team(Model):
31 | id = fields.IntField(primary_key=True)
32 | name = fields.TextField()
33 |
34 | events: fields.ManyToManyRelation[Event]
35 |
36 | def __str__(self):
37 | return self.name
38 |
39 |
40 | async def run():
41 | await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
42 | await Tortoise.generate_schemas()
43 |
44 | tournament = await Tournament.create(name="tournament")
45 | await Event.create(name="First", tournament=tournament)
46 | await Event.create(name="Second", tournament=tournament)
47 | tournament_with_filtered = (
48 | await Tournament.all()
49 | .prefetch_related(Prefetch("events", queryset=Event.filter(name="First")))
50 | .first()
51 | )
52 | print(tournament_with_filtered)
53 | print(await Tournament.first().prefetch_related("events"))
54 |
55 | tournament_with_filtered_to_attr = (
56 | await Tournament.all()
57 | .prefetch_related(
58 | Prefetch("events", queryset=Event.filter(name="First"), to_attr="to_attr_events_first"),
59 | Prefetch(
60 | "events", queryset=Event.filter(name="Second"), to_attr="to_attr_events_second"
61 | ),
62 | )
63 | .first()
64 | )
65 | print(tournament_with_filtered_to_attr.to_attr_events_first)
66 | print(tournament_with_filtered_to_attr.to_attr_events_second)
67 |
68 |
69 | if __name__ == "__main__":
70 | run_async(run())
71 |
--------------------------------------------------------------------------------
/examples/enum_fields.py:
--------------------------------------------------------------------------------
1 | from enum import Enum, IntEnum
2 |
3 | from tortoise import Tortoise, fields, run_async
4 | from tortoise.models import Model
5 |
6 |
7 | class Service(IntEnum):
8 | python_programming = 1
9 | database_design = 2
10 | system_administration = 3
11 |
12 |
13 | class Currency(str, Enum):
14 | HUF = "HUF"
15 | EUR = "EUR"
16 | USD = "USD"
17 |
18 |
19 | class EnumFields(Model):
20 | service: Service = fields.IntEnumField(Service)
21 | currency: Currency = fields.CharEnumField(Currency, default=Currency.HUF)
22 |
23 |
24 | async def run():
25 | await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
26 | await Tortoise.generate_schemas()
27 |
28 | obj0 = await EnumFields.create(service=Service.python_programming, currency=Currency.USD)
29 | # also you can use valid int and str value directly
30 | await EnumFields.create(service=1, currency="USD")
31 |
32 | try:
33 | # invalid enum value will raise ValueError
34 | await EnumFields.create(service=4, currency="XXX")
35 | except ValueError:
36 | print("Value is invalid")
37 |
38 | await EnumFields.filter(pk=obj0.pk).update(
39 | service=Service.database_design, currency=Currency.HUF
40 | )
41 | # also you can use valid int and str value directly
42 | await EnumFields.filter(pk=obj0.pk).update(service=2, currency="HUF")
43 |
44 |
45 | if __name__ == "__main__":
46 | run_async(run())
47 |
--------------------------------------------------------------------------------
/examples/fastapi/.gitignore:
--------------------------------------------------------------------------------
1 | db.sqlite3
2 |
--------------------------------------------------------------------------------
/examples/fastapi/README.rst:
--------------------------------------------------------------------------------
1 | Tortoise-ORM FastAPI example
2 | ============================
3 |
4 | We have a lightweight integration util ``tortoise.contrib.fastapi`` which has a class ``RegisterTortoise`` that can be used to set/clean up Tortoise-ORM in lifespan context.
5 |
6 | Usage
7 | -----
8 |
9 | .. code-block:: sh
10 |
11 | uvicorn main:app --reload
12 |
--------------------------------------------------------------------------------
/examples/fastapi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/examples/fastapi/__init__.py
--------------------------------------------------------------------------------
/examples/fastapi/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | from functools import partial
3 |
4 | from tortoise.contrib.fastapi import RegisterTortoise
5 |
6 | register_orm = partial(
7 | RegisterTortoise,
8 | db_url=os.getenv("DB_URL", "sqlite://db.sqlite3"),
9 | modules={"models": ["models"]},
10 | generate_schemas=True,
11 | )
12 |
--------------------------------------------------------------------------------
/examples/fastapi/main.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=E0611,E0401
2 | import os
3 | from collections.abc import AsyncGenerator
4 | from contextlib import asynccontextmanager
5 |
6 | from fastapi import FastAPI
7 | from routers import router as users_router
8 |
9 | from examples.fastapi.config import register_orm
10 | from tortoise import Tortoise, generate_config
11 | from tortoise.contrib.fastapi import RegisterTortoise, tortoise_exception_handlers
12 |
13 |
14 | @asynccontextmanager
15 | async def lifespan_test(app: FastAPI) -> AsyncGenerator[None, None]:
16 | config = generate_config(
17 | os.getenv("TORTOISE_TEST_DB", "sqlite://:memory:"),
18 | app_modules={"models": ["models"]},
19 | testing=True,
20 | connection_label="models",
21 | )
22 | async with RegisterTortoise(
23 | app=app,
24 | config=config,
25 | generate_schemas=True,
26 | _create_db=True,
27 | ):
28 | # db connected
29 | yield
30 | # app teardown
31 | # db connections closed
32 | await Tortoise._drop_databases()
33 |
34 |
35 | @asynccontextmanager
36 | async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
37 | if getattr(app.state, "testing", None):
38 | async with lifespan_test(app) as _:
39 | yield
40 | else:
41 | # app startup
42 | async with register_orm(app):
43 | # db connected
44 | yield
45 | # app teardown
46 | # db connections closed
47 |
48 |
49 | app = FastAPI(
50 | title="Tortoise ORM FastAPI example",
51 | lifespan=lifespan,
52 | exception_handlers=tortoise_exception_handlers(),
53 | )
54 | app.include_router(users_router, prefix="")
55 |
--------------------------------------------------------------------------------
/examples/fastapi/main_custom_timezone.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=E0611,E0401
2 | from collections.abc import AsyncGenerator
3 | from contextlib import asynccontextmanager
4 |
5 | from config import register_orm
6 | from fastapi import FastAPI
7 | from routers import router as users_router
8 |
9 |
10 | @asynccontextmanager
11 | async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
12 | # app startup
13 | async with register_orm(
14 | app,
15 | use_tz=False,
16 | timezone="Asia/Shanghai",
17 | add_exception_handlers=True,
18 | ):
19 | # db connected
20 | yield
21 | # app teardown
22 | # db connections closed
23 |
24 |
25 | app = FastAPI(title="Tortoise ORM FastAPI example", lifespan=lifespan)
26 | app.include_router(users_router, prefix="")
27 |
--------------------------------------------------------------------------------
/examples/fastapi/models.py:
--------------------------------------------------------------------------------
1 | from tortoise import fields, models
2 |
3 |
4 | class Users(models.Model):
5 | """
6 | The User model
7 | """
8 |
9 | id = fields.IntField(primary_key=True)
10 | #: This is a username
11 | username = fields.CharField(max_length=20, unique=True)
12 | name = fields.CharField(max_length=50, null=True)
13 | family_name = fields.CharField(max_length=50, null=True)
14 | category = fields.CharField(max_length=30, default="misc")
15 | password_hash = fields.CharField(max_length=128, null=True)
16 | created_at = fields.DatetimeField(auto_now_add=True)
17 | modified_at = fields.DatetimeField(auto_now=True)
18 |
19 | def full_name(self) -> str:
20 | """
21 | Returns the best name
22 | """
23 | if self.name or self.family_name:
24 | return f"{self.name or ''} {self.family_name or ''}".strip()
25 | return self.username
26 |
27 | class PydanticMeta:
28 | computed = ["full_name"]
29 | exclude = ["password_hash"]
30 |
--------------------------------------------------------------------------------
/examples/fastapi/routers.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, HTTPException
2 | from models import Users
3 | from schemas import Status, User_Pydantic, UserIn_Pydantic
4 |
5 | router = APIRouter()
6 |
7 |
8 | @router.get("/users", response_model=list[User_Pydantic])
9 | async def get_users():
10 | return await User_Pydantic.from_queryset(Users.all())
11 |
12 |
13 | @router.post("/users", response_model=User_Pydantic)
14 | async def create_user(user: UserIn_Pydantic):
15 | user_obj = await Users.create(**user.model_dump(exclude_unset=True))
16 | return await User_Pydantic.from_tortoise_orm(user_obj)
17 |
18 |
19 | @router.get("/user/{user_id}", response_model=User_Pydantic)
20 | async def get_user(user_id: int):
21 | return await User_Pydantic.from_queryset_single(Users.get(id=user_id))
22 |
23 |
24 | @router.put("/user/{user_id}", response_model=User_Pydantic)
25 | async def update_user(user_id: int, user: UserIn_Pydantic):
26 | await Users.filter(id=user_id).update(**user.model_dump(exclude_unset=True))
27 | return await User_Pydantic.from_queryset_single(Users.get(id=user_id))
28 |
29 |
30 | @router.delete("/user/{user_id}", response_model=Status)
31 | async def delete_user(user_id: int):
32 | deleted_count = await Users.filter(id=user_id).delete()
33 | if not deleted_count:
34 | raise HTTPException(status_code=404, detail=f"User {user_id} not found")
35 | return Status(message=f"Deleted user {user_id}")
36 |
37 |
38 | @router.get("/404")
39 | async def get_404():
40 | await Users.get(id=0)
41 |
42 |
43 | @router.get("/422")
44 | async def get_422():
45 | obj = await Users.create(username="foo")
46 | await Users.create(username=obj.username)
47 |
--------------------------------------------------------------------------------
/examples/fastapi/schemas.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | from models import Users
6 | from pydantic import BaseModel
7 |
8 | from tortoise.contrib.pydantic import PydanticModel, pydantic_model_creator
9 |
10 | if TYPE_CHECKING: # pragma: nocoverage
11 |
12 | class UserIn_Pydantic(Users, PydanticModel): # type:ignore[misc]
13 | pass
14 |
15 | class User_Pydantic(Users, PydanticModel): # type:ignore[misc]
16 | pass
17 |
18 | else:
19 | User_Pydantic = pydantic_model_creator(Users, name="User")
20 | UserIn_Pydantic = pydantic_model_creator(Users, name="UserIn", exclude_readonly=True)
21 |
22 |
23 | class Status(BaseModel):
24 | message: str
25 |
--------------------------------------------------------------------------------
/examples/global_table_name_generator.py:
--------------------------------------------------------------------------------
1 | """
2 | This example demonstrates how to use the global table name generator to automatically
3 | generate snake_case table names for all models, and how explicit table names take precedence.
4 | """
5 |
6 | from tortoise import Tortoise, fields, run_async
7 | from tortoise.models import Model
8 |
9 |
10 | def snake_case_table_names(cls):
11 | """Convert CamelCase class name to snake_case table name"""
12 | name = cls.__name__
13 | return "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_")
14 |
15 |
16 | class UserProfile(Model):
17 | id = fields.IntField(primary_key=True)
18 | name = fields.TextField()
19 | created_at = fields.DatetimeField(auto_now_add=True)
20 |
21 | def __str__(self):
22 | return self.name
23 |
24 |
25 | class BlogPost(Model):
26 | id = fields.IntField(primary_key=True)
27 | title = fields.TextField()
28 | author: fields.ForeignKeyRelation[UserProfile] = fields.ForeignKeyField(
29 | "models.UserProfile", related_name="posts"
30 | )
31 |
32 | class Meta:
33 | table = "custom_blog_posts"
34 |
35 | def __str__(self):
36 | return self.title
37 |
38 |
39 | async def run():
40 | # Initialize with snake_case table name generator
41 | await Tortoise.init(
42 | db_url="sqlite://:memory:",
43 | modules={"models": ["__main__"]},
44 | table_name_generator=snake_case_table_names,
45 | )
46 | await Tortoise.generate_schemas()
47 |
48 | # UserProfile uses generated name, BlogPost uses explicit table name
49 | print(f"UserProfile table name: {UserProfile._meta.db_table}") # >>> user_profile
50 | print(f"BlogPost table name: {BlogPost._meta.db_table}") # >>> custom_blog_posts
51 |
52 | await Tortoise.close_connections()
53 |
54 |
55 | if __name__ == "__main__":
56 | run_async(run())
57 |
--------------------------------------------------------------------------------
/examples/manual_sql.py:
--------------------------------------------------------------------------------
1 | """
2 | This example demonstrates executing manual SQL queries
3 | """
4 |
5 | from tortoise import Tortoise, connections, fields, run_async
6 | from tortoise.models import Model
7 | from tortoise.transactions import in_transaction
8 |
9 |
10 | class Event(Model):
11 | id = fields.IntField(primary_key=True)
12 | name = fields.TextField()
13 | timestamp = fields.DatetimeField(auto_now_add=True)
14 |
15 |
16 | async def run():
17 | await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
18 | await Tortoise.generate_schemas()
19 |
20 | # Need to get a connection. Unless explicitly specified, the name should be 'default'
21 | conn = connections.get("default")
22 |
23 | # Now we can execute queries in the normal autocommit mode
24 | await conn.execute_query("INSERT INTO event (name) VALUES ('Foo')")
25 |
26 | # You can also you parameters, but you need to use the right param strings for each dialect
27 | await conn.execute_query("INSERT INTO event (name) VALUES (?)", ["Bar"])
28 |
29 | # To do a transaction you'd need to use the in_transaction context manager
30 | async with in_transaction("default") as tconn:
31 | await tconn.execute_query("INSERT INTO event (name) VALUES ('Moo')")
32 | # Unless an exception happens it should commit automatically
33 |
34 | # This transaction is rolled back
35 | async with in_transaction("default") as tconn:
36 | await tconn.execute_query("INSERT INTO event (name) VALUES ('Sheep')")
37 | # Rollback to fail transaction
38 | await tconn.rollback()
39 |
40 | # Consider using execute_query_dict to get return values as a dict
41 | val = await conn.execute_query_dict("SELECT * FROM event")
42 | print(val)
43 | # Note that the result doesn't contain the rolled-back "Sheep" entry.
44 |
45 |
46 | if __name__ == "__main__":
47 | run_async(run())
48 |
--------------------------------------------------------------------------------
/examples/postgres.py:
--------------------------------------------------------------------------------
1 | """
2 | This example showcases postgres features
3 | """
4 |
5 | from tortoise import Tortoise, fields, run_async
6 | from tortoise.models import Model
7 |
8 |
9 | class Report(Model):
10 | id = fields.IntField(primary_key=True)
11 | content = fields.JSONField[dict]()
12 |
13 | def __str__(self):
14 | return str(self.id)
15 |
16 |
17 | async def run():
18 | await Tortoise.init(
19 | {
20 | "connections": {
21 | "default": {
22 | "engine": "tortoise.backends.asyncpg",
23 | "credentials": {
24 | "host": "localhost",
25 | "port": "5432",
26 | "user": "tortoise",
27 | "password": "qwerty123",
28 | "database": "test",
29 | },
30 | }
31 | },
32 | "apps": {"models": {"models": ["__main__"], "default_connection": "default"}},
33 | },
34 | _create_db=True,
35 | )
36 | await Tortoise.generate_schemas()
37 |
38 | report_data = {"foo": "bar"}
39 | print(await Report.create(content=report_data))
40 | print(await Report.filter(content=report_data).first())
41 | await Tortoise._drop_databases()
42 |
43 |
44 | if __name__ == "__main__":
45 | run_async(run())
46 |
--------------------------------------------------------------------------------
/examples/pydantic/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/examples/pydantic/__init__.py
--------------------------------------------------------------------------------
/examples/pydantic/early_init.py:
--------------------------------------------------------------------------------
1 | """
2 | This example demonstrates pydantic serialisation, and how to use early partial init.
3 | """
4 |
5 | from tortoise import Tortoise, fields
6 | from tortoise.contrib.pydantic import pydantic_model_creator
7 | from tortoise.models import Model
8 |
9 |
10 | class Tournament(Model):
11 | id = fields.IntField(primary_key=True)
12 | name = fields.TextField()
13 | created_at = fields.DatetimeField(auto_now_add=True)
14 |
15 | events: fields.ReverseRelation["Event"]
16 |
17 | class Meta:
18 | ordering = ["name"]
19 |
20 |
21 | class Event(Model):
22 | id = fields.IntField(primary_key=True)
23 | name = fields.TextField()
24 | created_at = fields.DatetimeField(auto_now_add=True)
25 | tournament: fields.ForeignKeyNullableRelation[Tournament] = fields.ForeignKeyField(
26 | "models.Tournament", related_name="events", null=True
27 | )
28 |
29 | class Meta:
30 | ordering = ["name"]
31 |
32 |
33 | Event_TooEarly = pydantic_model_creator(Event)
34 | print("Relations are missing if models not initialized:")
35 | print(Event_TooEarly.schema_json(indent=4))
36 |
37 |
38 | Tortoise.init_models(["__main__"], "models")
39 |
40 | Event_Pydantic = pydantic_model_creator(Event)
41 | print("\nRelations are now present:")
42 | print(Event_Pydantic.schema_json(indent=4))
43 |
44 | # Now we can use the pydantic model early if needed
45 |
--------------------------------------------------------------------------------
/examples/pydantic/tutorial_1.py:
--------------------------------------------------------------------------------
1 | """
2 | Pydantic tutorial 1
3 |
4 | Here we introduce:
5 | * Creating a Pydantic model from a Tortoise model
6 | * Docstrings & doc-comments are used
7 | * Evaluating the generated schema
8 | * Simple serialisation with both .model_dump() and .model_dump_json()
9 | """
10 |
11 | from tortoise import Tortoise, fields, run_async
12 | from tortoise.contrib.pydantic import pydantic_model_creator
13 | from tortoise.models import Model
14 |
15 |
16 | class Tournament(Model):
17 | """
18 | This references a Tournament
19 | """
20 |
21 | id = fields.IntField(primary_key=True)
22 | name = fields.CharField(max_length=100)
23 | #: The date-time the Tournament record was created at
24 | created_at = fields.DatetimeField(auto_now_add=True)
25 |
26 |
27 | Tournament_Pydantic = pydantic_model_creator(Tournament)
28 | # Print JSON-schema
29 | print(Tournament_Pydantic.schema_json(indent=4))
30 |
31 |
32 | async def run():
33 | await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
34 | await Tortoise.generate_schemas()
35 |
36 | # Create object
37 | tournament = await Tournament.create(name="New Tournament")
38 | # Serialise it
39 | tourpy = await Tournament_Pydantic.from_tortoise_orm(tournament)
40 |
41 | # As Python dict with Python objects (e.g. datetime)
42 | print(tourpy.model_dump())
43 | # As serialised JSON (e.g. datetime is ISO8601 string representation)
44 | print(tourpy.model_dump_json(indent=4))
45 |
46 |
47 | if __name__ == "__main__":
48 | run_async(run())
49 |
--------------------------------------------------------------------------------
/examples/pydantic/tutorial_2.py:
--------------------------------------------------------------------------------
1 | """
2 | Pydantic tutorial 2
3 |
4 | Here we introduce:
5 | * Creating a list-model to serialise a queryset
6 | * Default sorting is honoured
7 | """
8 |
9 | from tortoise import Tortoise, fields, run_async
10 | from tortoise.contrib.pydantic import pydantic_queryset_creator
11 | from tortoise.models import Model
12 |
13 |
14 | class Tournament(Model):
15 | """
16 | This references a Tournament
17 | """
18 |
19 | id = fields.IntField(primary_key=True)
20 | name = fields.CharField(max_length=100)
21 | #: The date-time the Tournament record was created at
22 | created_at = fields.DatetimeField(auto_now_add=True)
23 |
24 | class Meta:
25 | # Define the default ordering
26 | # the pydantic serialiser will use this to order the results
27 | ordering = ["name"]
28 |
29 |
30 | # Create a list of models for population from a queryset.
31 | Tournament_Pydantic_List = pydantic_queryset_creator(Tournament)
32 | # Print JSON-schema
33 | print(Tournament_Pydantic_List.schema_json(indent=4))
34 |
35 |
36 | async def run():
37 | await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
38 | await Tortoise.generate_schemas()
39 |
40 | # Create objects
41 | await Tournament.create(name="New Tournament")
42 | await Tournament.create(name="Another")
43 | await Tournament.create(name="Last Tournament")
44 |
45 | # Serialise it
46 | tourpy = await Tournament_Pydantic_List.from_queryset(Tournament.all())
47 |
48 | # As Python dict with Python objects (e.g. datetime)
49 | # Note that the root element is 'root' that contains the root element.
50 | print(tourpy.model_dump())
51 | # As serialised JSON (e.g. datetime is ISO8601 string representation)
52 | print(tourpy.model_dump_json(indent=4))
53 |
54 |
55 | if __name__ == "__main__":
56 | run_async(run())
57 |
--------------------------------------------------------------------------------
/examples/pydantic/tutorial_3.py:
--------------------------------------------------------------------------------
1 | """
2 | Pydantic tutorial 3
3 |
4 | Here we introduce:
5 | * Relationships
6 | * Early model init
7 | """
8 |
9 | from tortoise import Tortoise, fields, run_async
10 | from tortoise.contrib.pydantic import pydantic_model_creator
11 | from tortoise.models import Model
12 |
13 |
14 | class Tournament(Model):
15 | """
16 | This references a Tournament
17 | """
18 |
19 | id = fields.IntField(primary_key=True)
20 | name = fields.CharField(max_length=100)
21 | #: The date-time the Tournament record was created at
22 | created_at = fields.DatetimeField(auto_now_add=True)
23 |
24 |
25 | class Event(Model):
26 | """
27 | This references an Event in a Tournament
28 | """
29 |
30 | id = fields.IntField(primary_key=True)
31 | name = fields.CharField(max_length=100)
32 | created_at = fields.DatetimeField(auto_now_add=True)
33 |
34 | tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
35 | "models.Tournament", related_name="events", description="The Tournament this happens in"
36 | )
37 |
38 |
39 | # Early model, does not include relations
40 | Tournament_Pydantic_Early = pydantic_model_creator(Tournament)
41 | # Print JSON-schema
42 | print(Tournament_Pydantic_Early.schema_json(indent=4))
43 |
44 |
45 | # Initialise model structure early. This does not init any database structures
46 | Tortoise.init_models(["__main__"], "models")
47 |
48 |
49 | # We now have a complete model
50 | Tournament_Pydantic = pydantic_model_creator(Tournament)
51 | # Print JSON-schema
52 | print(Tournament_Pydantic.schema_json(indent=4))
53 |
54 | # Note how both schema's don't follow relations back.
55 | Event_Pydantic = pydantic_model_creator(Event)
56 | # Print JSON-schema
57 | print(Event_Pydantic.schema_json(indent=4))
58 |
59 |
60 | async def run():
61 | await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
62 | await Tortoise.generate_schemas()
63 |
64 | # Create objects
65 | tournament = await Tournament.create(name="New Tournament")
66 | event = await Event.create(name="The Event", tournament=tournament)
67 |
68 | # Serialise Tournament
69 | tourpy = await Tournament_Pydantic.from_tortoise_orm(tournament)
70 |
71 | # As serialised JSON
72 | print(tourpy.model_dump_json(indent=4))
73 |
74 | # Serialise Event
75 | eventpy = await Event_Pydantic.from_tortoise_orm(event)
76 |
77 | # As serialised JSON
78 | print(eventpy.model_dump_json(indent=4))
79 |
80 |
81 | if __name__ == "__main__":
82 | run_async(run())
83 |
--------------------------------------------------------------------------------
/examples/quart/README.rst:
--------------------------------------------------------------------------------
1 | Tortoise-ORM Quart example
2 | ==========================
3 |
4 | We have a lightweight integration util ``tortoise.contrib.quart`` which has a single function ``register_tortoise`` which sets up Tortoise-ORM on startup and cleans up on teardown.
5 |
6 | Note that the modules path can not be ``__main__`` as that changes depending on the launch point. One wants to be able to launch a Quart service from the ASGI runner directly, so all paths need to be explicit.
7 |
8 | Usage
9 | -----
10 |
11 | .. code-block:: sh
12 |
13 | QUART_APP=main quart
14 | ...
15 | Commands:
16 | generate-schemas Populate DB with Tortoise-ORM schemas.
17 | run Start and run a development server.
18 | shell Open a shell within the app context.
19 |
20 | # To generate schemas
21 | QUART_APP=main quart generate-schemas
22 |
23 | # To run
24 | QUART_APP=main quart run
25 |
--------------------------------------------------------------------------------
/examples/quart/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/examples/quart/__init__.py
--------------------------------------------------------------------------------
/examples/quart/main.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=E0401,E0611
2 | import asyncio
3 | import logging
4 | from random import choice
5 |
6 | from models import Users, Workers
7 | from quart import Quart, jsonify
8 |
9 | from tortoise.contrib.quart import register_tortoise
10 |
11 | logging.basicConfig(level=logging.DEBUG)
12 |
13 |
14 | STATUSES = ["New", "Old", "Gone"]
15 | app = Quart(__name__)
16 |
17 |
18 | @app.route("/")
19 | async def list_all():
20 | users, workers = await asyncio.gather(Users.all(), Workers.all())
21 | return jsonify(
22 | {"users": [str(user) for user in users], "workers": [str(worker) for worker in workers]}
23 | )
24 |
25 |
26 | @app.route("/user")
27 | async def add_user():
28 | user = await Users.create(status=choice(STATUSES)) # nosec
29 | return str(user)
30 |
31 |
32 | @app.route("/worker")
33 | async def add_worker():
34 | worker = await Workers.create(status=choice(STATUSES)) # nosec
35 | return str(worker)
36 |
37 |
38 | register_tortoise(
39 | app,
40 | db_url="mysql://root:@127.0.0.1:3306/quart",
41 | modules={"models": ["models"]},
42 | generate_schemas=False,
43 | )
44 |
45 |
46 | if __name__ == "__main__":
47 | app.run(port=5000)
48 |
--------------------------------------------------------------------------------
/examples/quart/models.py:
--------------------------------------------------------------------------------
1 | from tortoise import Model, fields
2 |
3 |
4 | class Users(Model):
5 | id = fields.IntField(primary_key=True)
6 | status = fields.CharField(20)
7 |
8 | def __str__(self):
9 | return f"User {self.id}: {self.status}"
10 |
11 |
12 | class Workers(Model):
13 | id = fields.IntField(primary_key=True)
14 | status = fields.CharField(20)
15 |
16 | def __str__(self):
17 | return f"Worker {self.id}: {self.status}"
18 |
--------------------------------------------------------------------------------
/examples/router.py:
--------------------------------------------------------------------------------
1 | """
2 | This example to use router to implement read/write separation
3 | """
4 |
5 | from tortoise import Model, Tortoise, fields, run_async
6 |
7 |
8 | class Event(Model):
9 | id = fields.IntField(primary_key=True)
10 | name = fields.TextField()
11 | datetime = fields.DatetimeField(null=True)
12 |
13 | class Meta:
14 | table = "event"
15 |
16 | def __str__(self):
17 | return self.name
18 |
19 |
20 | class Router:
21 | def db_for_read(self, model: type[Model]):
22 | return "slave"
23 |
24 | def db_for_write(self, model: type[Model]):
25 | return "master"
26 |
27 |
28 | async def run():
29 | config = {
30 | "connections": {"master": "sqlite:///tmp/test.db", "slave": "sqlite:///tmp/test.db"},
31 | "apps": {
32 | "models": {
33 | "models": ["__main__"],
34 | "default_connection": "master",
35 | }
36 | },
37 | "routers": ["__main__.Router"],
38 | "use_tz": False,
39 | "timezone": "UTC",
40 | }
41 | await Tortoise.init(config=config)
42 | await Tortoise.generate_schemas()
43 | # this will use connection master
44 | event = await Event.create(name="Test")
45 | # this will use connection slave
46 | await Event.get(pk=event.pk)
47 |
48 |
49 | if __name__ == "__main__":
50 | run_async(run())
51 |
--------------------------------------------------------------------------------
/examples/sanic/README.rst:
--------------------------------------------------------------------------------
1 | Tortoise-ORM Sanic example
2 | ==========================
3 |
4 | We have a lightweight integration util ``tortoise.contrib.sanic`` which has a single function ``register_tortoise`` which sets up Tortoise-ORM on startup and cleans up on teardown.
5 |
6 | Usage
7 | -----
8 |
9 | .. code-block:: sh
10 |
11 | python3 main.py
12 |
--------------------------------------------------------------------------------
/examples/sanic/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/examples/sanic/__init__.py
--------------------------------------------------------------------------------
/examples/sanic/_tests.py:
--------------------------------------------------------------------------------
1 | import re
2 | from pathlib import Path
3 |
4 | import pytest
5 | from sanic_testing import TestManager
6 |
7 | try:
8 | import main
9 | except ImportError:
10 | if (cwd := Path.cwd()) == (parent := Path(__file__).parent):
11 | dirpath = "."
12 | else:
13 | dirpath = str(parent.relative_to(cwd))
14 | print(f"You may need to explicitly declare python path:\n\nexport PYTHONPATH={dirpath}\n")
15 | raise
16 |
17 |
18 | @pytest.fixture(scope="module")
19 | def anyio_backend() -> str:
20 | return "asyncio"
21 |
22 |
23 | @pytest.fixture
24 | def app():
25 | sanic_app = main.app
26 | TestManager(sanic_app)
27 | return sanic_app
28 |
29 |
30 | @pytest.mark.anyio
31 | async def test_basic_asgi_client(app):
32 | request, response = await app.asgi_client.get("/")
33 | assert response.status == 200
34 | assert b'{"users":[' in response.body
35 |
36 | request, response = await app.asgi_client.post("/user")
37 | assert response.status == 200
38 | assert re.match(rb'{"user":"User \d+: New User"}$', response.body)
39 |
--------------------------------------------------------------------------------
/examples/sanic/main.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=E0401,E0611
2 | import logging
3 |
4 | from models import Users
5 | from sanic import Sanic, response
6 |
7 | from tortoise.contrib.sanic import register_tortoise
8 |
9 | logging.basicConfig(level=logging.DEBUG)
10 |
11 | app = Sanic(__name__)
12 |
13 |
14 | @app.route("/")
15 | async def list_all(request):
16 | users = await Users.all()
17 | return response.json({"users": [str(user) for user in users]})
18 |
19 |
20 | @app.post("/user")
21 | async def add_user(request):
22 | user = await Users.create(name="New User")
23 | return response.json({"user": str(user)})
24 |
25 |
26 | register_tortoise(
27 | app, db_url="sqlite://db.sqlite3", modules={"models": ["models"]}, generate_schemas=True
28 | )
29 |
30 |
31 | if __name__ == "__main__":
32 | app.run(port=5000, debug=True)
33 |
--------------------------------------------------------------------------------
/examples/sanic/models.py:
--------------------------------------------------------------------------------
1 | from tortoise import Model, fields
2 |
3 |
4 | class Users(Model):
5 | id = fields.IntField(primary_key=True)
6 | name = fields.CharField(50)
7 |
8 | def __str__(self):
9 | return f"User {self.id}: {self.name}"
10 |
--------------------------------------------------------------------------------
/examples/signals.py:
--------------------------------------------------------------------------------
1 | """
2 | This example demonstrates model signals usage
3 | """
4 |
5 | from __future__ import annotations
6 |
7 | from tortoise import BaseDBAsyncClient, Tortoise, fields, run_async
8 | from tortoise.models import Model
9 | from tortoise.signals import post_delete, post_save, pre_delete, pre_save
10 |
11 |
12 | class Signal(Model):
13 | id = fields.IntField(primary_key=True)
14 | name = fields.TextField()
15 |
16 | class Meta:
17 | table = "signal"
18 |
19 | def __str__(self):
20 | return self.name
21 |
22 |
23 | @pre_save(Signal)
24 | async def signal_pre_save(sender: type[Signal], instance: Signal, using_db, update_fields) -> None:
25 | print(sender, instance, using_db, update_fields)
26 |
27 |
28 | @post_save(Signal)
29 | async def signal_post_save(
30 | sender: type[Signal],
31 | instance: Signal,
32 | created: bool,
33 | using_db: BaseDBAsyncClient | None,
34 | update_fields: list[str],
35 | ) -> None:
36 | print(sender, instance, using_db, created, update_fields)
37 |
38 |
39 | @pre_delete(Signal)
40 | async def signal_pre_delete(
41 | sender: type[Signal], instance: Signal, using_db: BaseDBAsyncClient | None
42 | ) -> None:
43 | print(sender, instance, using_db)
44 |
45 |
46 | @post_delete(Signal)
47 | async def signal_post_delete(
48 | sender: type[Signal], instance: Signal, using_db: BaseDBAsyncClient | None
49 | ) -> None:
50 | print(sender, instance, using_db)
51 |
52 |
53 | async def run():
54 | await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
55 | await Tortoise.generate_schemas()
56 | # pre_save,post_save will be send
57 | signal = await Signal.create(name="Signal")
58 | signal.name = "Signal_Save"
59 |
60 | # pre_save,post_save will be send
61 | await signal.save(update_fields=["name"])
62 |
63 | # pre_delete,post_delete will be send
64 | await signal.delete()
65 |
66 |
67 | if __name__ == "__main__":
68 | run_async(run())
69 |
--------------------------------------------------------------------------------
/examples/starlette/README.rst:
--------------------------------------------------------------------------------
1 | Tortoise-ORM Starlette example
2 | ==========================
3 |
4 | We have a lightweight integration util ``tortoise.contrib.starlette`` which has a single function ``register_tortoise`` which sets up Tortoise-ORM on startup and cleans up on teardown.
5 |
6 | Usage
7 | -----
8 |
9 | .. code-block:: sh
10 |
11 | python3 main.py
12 |
--------------------------------------------------------------------------------
/examples/starlette/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/examples/starlette/__init__.py
--------------------------------------------------------------------------------
/examples/starlette/main.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=E0401,E0611
2 | import logging
3 | from json import JSONDecodeError
4 |
5 | from models import Users
6 | from starlette.applications import Starlette
7 | from starlette.exceptions import HTTPException
8 | from starlette.requests import Request
9 | from starlette.responses import JSONResponse
10 | from starlette.status import HTTP_201_CREATED, HTTP_400_BAD_REQUEST
11 | from uvicorn.main import run
12 |
13 | from tortoise.contrib.starlette import register_tortoise
14 |
15 | logging.basicConfig(level=logging.DEBUG)
16 |
17 | app = Starlette()
18 |
19 |
20 | @app.route("/", methods=["GET"])
21 | async def list_all(_: Request) -> JSONResponse:
22 | users = await Users.all()
23 | return JSONResponse({"users": [str(user) for user in users]})
24 |
25 |
26 | @app.route("/user", methods=["POST"])
27 | async def add_user(request: Request) -> JSONResponse:
28 | try:
29 | payload = await request.json()
30 | username = payload["username"]
31 | except JSONDecodeError:
32 | raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail="cannot parse request body")
33 | except KeyError:
34 | raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail="username is required")
35 |
36 | user = await Users.create(username=username)
37 | return JSONResponse({"user": str(user)}, status_code=HTTP_201_CREATED)
38 |
39 |
40 | register_tortoise(
41 | app, db_url="sqlite://:memory:", modules={"models": ["models"]}, generate_schemas=True
42 | )
43 |
44 | if __name__ == "__main__":
45 | run(app)
46 |
--------------------------------------------------------------------------------
/examples/starlette/models.py:
--------------------------------------------------------------------------------
1 | from tortoise import fields, models
2 |
3 |
4 | class Users(models.Model):
5 | id = fields.IntField(primary_key=True)
6 | username = fields.CharField(max_length=20)
7 |
8 | def __str__(self) -> str:
9 | return f"User {self.id}: {self.username}"
10 |
--------------------------------------------------------------------------------
/examples/transactions.py:
--------------------------------------------------------------------------------
1 | """
2 | This example demonstrates how you can use transactions with tortoise
3 | """
4 |
5 | from tortoise import Tortoise, fields, run_async
6 | from tortoise.exceptions import OperationalError
7 | from tortoise.models import Model
8 | from tortoise.transactions import atomic, in_transaction
9 |
10 |
11 | class Event(Model):
12 | id = fields.IntField(primary_key=True)
13 | name = fields.TextField()
14 |
15 | class Meta:
16 | table = "event"
17 |
18 | def __str__(self):
19 | return self.name
20 |
21 |
22 | async def run():
23 | await Tortoise.init(db_url="sqlite://:memory:", modules={"models": ["__main__"]})
24 | await Tortoise.generate_schemas()
25 |
26 | try:
27 | async with in_transaction() as connection:
28 | event = Event(name="Test")
29 | await event.save(using_db=connection)
30 | await Event.filter(id=event.id).using_db(connection).update(name="Updated name")
31 | saved_event = await Event.filter(name="Updated name").using_db(connection).first()
32 | await connection.execute_query("SELECT * FROM non_existent_table")
33 | except OperationalError:
34 | pass
35 | saved_event = await Event.filter(name="Updated name").first()
36 | print(saved_event)
37 |
38 | @atomic()
39 | async def bound_to_fall():
40 | event = await Event.create(name="Test")
41 | await Event.filter(id=event.id).update(name="Updated name")
42 | saved_event = await Event.filter(name="Updated name").first()
43 | print(saved_event.name)
44 | raise OperationalError()
45 |
46 | try:
47 | await bound_to_fall()
48 | except OperationalError:
49 | pass
50 | saved_event = await Event.filter(name="Updated name").first()
51 | print(saved_event)
52 |
53 |
54 | if __name__ == "__main__":
55 | run_async(run())
56 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tests/__init__.py
--------------------------------------------------------------------------------
/tests/backends/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tests/backends/__init__.py
--------------------------------------------------------------------------------
/tests/backends/test_capabilities.py:
--------------------------------------------------------------------------------
1 | from tortoise import connections
2 | from tortoise.contrib import test
3 |
4 |
5 | class TestCapabilities(test.TestCase):
6 | # pylint: disable=E1101
7 |
8 | async def asyncSetUp(self) -> None:
9 | await super().asyncSetUp()
10 | self.db = connections.get("models")
11 | self.caps = self.db.capabilities
12 |
13 | def test_str(self):
14 | self.assertIn("requires_limit", str(self.caps))
15 |
16 | def test_immutability_1(self):
17 | self.assertIsInstance(self.caps.dialect, str)
18 | with self.assertRaises(AttributeError):
19 | self.caps.dialect = "foo"
20 |
21 | @test.expectedFailure
22 | @test.requireCapability(connection_name="other")
23 | def test_connection_name(self):
24 | # Will fail with a `KeyError` since the connection `"other"` does not exist.
25 | pass
26 |
27 | @test.requireCapability(dialect="sqlite")
28 | @test.expectedFailure
29 | def test_actually_runs(self):
30 | self.assertTrue(False) # pylint: disable=W1503
31 |
32 | def test_attribute_error(self):
33 | with self.assertRaises(AttributeError):
34 | self.caps.bar = "foo"
35 |
36 | @test.requireCapability(dialect="sqlite")
37 | def test_dialect_sqlite(self):
38 | self.assertEqual(self.caps.dialect, "sqlite")
39 |
40 | @test.requireCapability(dialect="mysql")
41 | def test_dialect_mysql(self):
42 | self.assertEqual(self.caps.dialect, "mysql")
43 |
44 | @test.requireCapability(dialect="postgres")
45 | def test_dialect_postgres(self):
46 | self.assertEqual(self.caps.dialect, "postgres")
47 |
--------------------------------------------------------------------------------
/tests/backends/test_explain.py:
--------------------------------------------------------------------------------
1 | from tests.testmodels import Tournament
2 | from tortoise.contrib import test
3 | from tortoise.contrib.test.condition import NotEQ
4 |
5 |
6 | class TestExplain(test.TestCase):
7 | @test.requireCapability(dialect=NotEQ("mssql"))
8 | async def test_explain(self):
9 | # NOTE: we do not provide any guarantee on the format of the value
10 | # returned by `.explain()`, as it heavily depends on the database.
11 | # This test merely checks that one is able to run `.explain()`
12 | # without errors for each backend.
13 | plan = await Tournament.all().explain()
14 | # This should have returned *some* information.
15 | self.assertGreater(len(str(plan)), 20)
16 |
--------------------------------------------------------------------------------
/tests/backends/test_mysql.py:
--------------------------------------------------------------------------------
1 | """
2 | Test some mysql-specific features
3 | """
4 |
5 | import ssl
6 |
7 | from tortoise import Tortoise
8 | from tortoise.contrib import test
9 |
10 |
11 | class TestMySQL(test.SimpleTestCase):
12 | async def asyncSetUp(self):
13 | if Tortoise._inited:
14 | await self._tearDownDB()
15 | self.db_config = test.getDBConfig(app_label="models", modules=["tests.testmodels"])
16 | if self.db_config["connections"]["models"]["engine"] != "tortoise.backends.mysql":
17 | raise test.SkipTest("MySQL only")
18 |
19 | async def asyncTearDown(self) -> None:
20 | if Tortoise._inited:
21 | await Tortoise._drop_databases()
22 | await super().asyncTearDown()
23 |
24 | async def test_bad_charset(self):
25 | self.db_config["connections"]["models"]["credentials"]["charset"] = "terrible"
26 | with self.assertRaisesRegex(ConnectionError, "Unknown charset"):
27 | await Tortoise.init(self.db_config, _create_db=True)
28 |
29 | async def test_ssl_true(self):
30 | self.db_config["connections"]["models"]["credentials"]["ssl"] = True
31 | try:
32 | import asyncmy # noqa pylint: disable=unused-import
33 |
34 | # setting read_timeout for asyncmy. Otherwise, it will hang forever.
35 | self.db_config["connections"]["models"]["credentials"]["read_timeout"] = 1
36 | except ImportError:
37 | pass
38 |
39 | with self.assertRaises(ConnectionError):
40 | await Tortoise.init(self.db_config, _create_db=True)
41 |
42 | async def test_ssl_custom(self):
43 | # Expect connectionerror or pass
44 | ctx = ssl.create_default_context()
45 | ctx.check_hostname = False
46 | ctx.verify_mode = ssl.CERT_NONE
47 |
48 | self.db_config["connections"]["models"]["credentials"]["ssl"] = ctx
49 | try:
50 | await Tortoise.init(self.db_config, _create_db=True)
51 | except ConnectionError:
52 | pass
53 |
--------------------------------------------------------------------------------
/tests/backends/test_reconnect.py:
--------------------------------------------------------------------------------
1 | from tests.testmodels import Tournament
2 | from tortoise import connections
3 | from tortoise.contrib import test
4 | from tortoise.transactions import in_transaction
5 |
6 |
7 | @test.requireCapability(daemon=True)
8 | class TestReconnect(test.IsolatedTestCase):
9 | async def test_reconnect(self):
10 | await Tournament.create(name="1")
11 |
12 | await connections.get("models")._expire_connections()
13 |
14 | await Tournament.create(name="2")
15 |
16 | await connections.get("models")._expire_connections()
17 |
18 | await Tournament.create(name="3")
19 |
20 | self.assertEqual(
21 | [f"{a.id}:{a.name}" for a in await Tournament.all()], ["1:1", "2:2", "3:3"]
22 | )
23 |
24 | @test.requireCapability(supports_transactions=True)
25 | async def test_reconnect_transaction_start(self):
26 | async with in_transaction():
27 | await Tournament.create(name="1")
28 |
29 | await connections.get("models")._expire_connections()
30 |
31 | async with in_transaction():
32 | await Tournament.create(name="2")
33 |
34 | await connections.get("models")._expire_connections()
35 |
36 | async with in_transaction():
37 | self.assertEqual([f"{a.id}:{a.name}" for a in await Tournament.all()], ["1:1", "2:2"])
38 |
--------------------------------------------------------------------------------
/tests/benchmarks/test_bulk_create.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import random
3 |
4 | from tests.testmodels import BenchmarkFewFields, BenchmarkManyFields
5 |
6 |
7 | def test_bulk_create_few_fields(benchmark):
8 | loop = asyncio.get_event_loop()
9 |
10 | data = [
11 | BenchmarkFewFields(
12 | level=random.choice([10, 20, 30, 40, 50]), # nosec
13 | text=f"Insert from C, item {i}",
14 | )
15 | for i in range(100)
16 | ]
17 |
18 | @benchmark
19 | def bench():
20 | async def _bench():
21 | await BenchmarkFewFields.bulk_create(data)
22 |
23 | loop.run_until_complete(_bench())
24 |
25 |
26 | def test_bulk_create_many_fields(benchmark, gen_many_fields_data):
27 | loop = asyncio.get_event_loop()
28 |
29 | data = [BenchmarkManyFields(**gen_many_fields_data()) for _ in range(100)]
30 |
31 | @benchmark
32 | def bench():
33 | async def _bench():
34 | await BenchmarkManyFields.bulk_create(data)
35 |
36 | loop.run_until_complete(_bench())
37 |
--------------------------------------------------------------------------------
/tests/benchmarks/test_create.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import random
3 |
4 | from tests.testmodels import BenchmarkFewFields, BenchmarkManyFields
5 |
6 |
7 | def test_create_few_fields(benchmark):
8 | loop = asyncio.get_event_loop()
9 |
10 | @benchmark
11 | def bench():
12 | async def _bench():
13 | level = random.randint(0, 100) # nosec
14 | await BenchmarkFewFields.create(level=level, text="test")
15 |
16 | loop.run_until_complete(_bench())
17 |
18 |
19 | def test_create_many_fields(benchmark, gen_many_fields_data):
20 | loop = asyncio.get_event_loop()
21 |
22 | @benchmark
23 | def bench():
24 | async def _bench():
25 | await BenchmarkManyFields.create(**gen_many_fields_data())
26 |
27 | loop.run_until_complete(_bench())
28 |
--------------------------------------------------------------------------------
/tests/benchmarks/test_expressions.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from tests.testmodels import BenchmarkFewFields, DecimalFields
4 | from tortoise.expressions import F
5 | from tortoise.functions import Count
6 |
7 |
8 | def test_expressions_count(benchmark, few_fields_benchmark_dataset):
9 | loop = asyncio.get_event_loop()
10 |
11 | @benchmark
12 | def bench():
13 | async def _bench():
14 | await BenchmarkFewFields.annotate(text_count=Count("text"))
15 |
16 | loop.run_until_complete(_bench())
17 |
18 |
19 | def test_expressions_f(benchmark, create_decimals):
20 | loop = asyncio.get_event_loop()
21 |
22 | @benchmark
23 | def bench():
24 | async def _bench():
25 | await DecimalFields.annotate(d=F("decimal")).all()
26 |
27 | loop.run_until_complete(_bench())
28 |
--------------------------------------------------------------------------------
/tests/benchmarks/test_field_attribute_lookup.py:
--------------------------------------------------------------------------------
1 | from tortoise.fields import Field
2 |
3 |
4 | class MyField(Field):
5 | @property
6 | def MY_PROPERTY(self):
7 | return f"hi from {self.__class__.__name__}!"
8 |
9 | OTHER_PROPERTY = "something else"
10 |
11 | class _db_property:
12 | def __init__(self, field: "Field"):
13 | self.field = field
14 |
15 | @property
16 | def MY_PROPERTY(self):
17 | return f"hi from {self.__class__.__name__} of {self.field.__class__.__name__}!"
18 |
19 | class _db_cls_attribute:
20 | MY_PROPERTY = "cls_attribute"
21 |
22 |
23 | def test_field_attribute_lookup_get_for_dialect(benchmark):
24 | field = MyField()
25 |
26 | @benchmark
27 | def bench():
28 | field.get_for_dialect("property", "MY_PROPERTY")
29 | field.get_for_dialect("postgres", "MY_PROPERTY")
30 | field.get_for_dialect("cls_attribute", "MY_PROPERTY")
31 | field.get_for_dialect("property", "OTHER_PROPERTY")
32 | field.get_for_dialect("property", "MY_PROPERTY")
33 |
--------------------------------------------------------------------------------
/tests/benchmarks/test_filter.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import random
3 | from decimal import Decimal
4 |
5 | from tests.testmodels import BenchmarkFewFields, BenchmarkManyFields
6 |
7 |
8 | def test_filter_few_fields(benchmark, few_fields_benchmark_dataset):
9 | loop = asyncio.get_event_loop()
10 | levels = list(set([o.level for o in few_fields_benchmark_dataset]))
11 |
12 | @benchmark
13 | def bench():
14 | async def _bench():
15 | await BenchmarkFewFields.filter(level__in=random.sample(levels, 5)).limit(5)
16 |
17 | loop.run_until_complete(_bench())
18 |
19 |
20 | def test_filter_many_filters(benchmark, many_fields_benchmark_dataset):
21 | loop = asyncio.get_event_loop()
22 | levels = list(set([o.level for o in many_fields_benchmark_dataset]))
23 |
24 | @benchmark
25 | def bench():
26 | async def _bench():
27 | await BenchmarkManyFields.filter(
28 | level__in=random.sample(levels, 5),
29 | col_float1__gt=0,
30 | col_smallint1=2,
31 | col_int1__lt=2000001,
32 | col_bigint1__in=[99999999],
33 | col_char1__contains="value1",
34 | col_text1="Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa,Moo,Foo,Baa,Waa",
35 | col_decimal1=Decimal("2.2"),
36 | ).limit(5)
37 |
38 | loop.run_until_complete(_bench())
39 |
--------------------------------------------------------------------------------
/tests/benchmarks/test_get.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import random
3 |
4 | from tests.testmodels import BenchmarkFewFields, BenchmarkManyFields
5 |
6 |
7 | def test_get_few_fields(benchmark, few_fields_benchmark_dataset):
8 | loop = asyncio.get_event_loop()
9 | minid = min(o.id for o in few_fields_benchmark_dataset)
10 | maxid = max(o.id for o in few_fields_benchmark_dataset)
11 |
12 | @benchmark
13 | def bench():
14 | async def _bench():
15 | randid = random.randint(minid, maxid) # nosec
16 | await BenchmarkFewFields.get(id=randid)
17 |
18 | loop.run_until_complete(_bench())
19 |
20 |
21 | def test_get_many_fields(benchmark, many_fields_benchmark_dataset):
22 | loop = asyncio.get_event_loop()
23 | minid = min(o.id for o in many_fields_benchmark_dataset)
24 | maxid = max(o.id for o in many_fields_benchmark_dataset)
25 |
26 | @benchmark
27 | def bench():
28 | async def _bench():
29 | randid = random.randint(minid, maxid) # nosec
30 | await BenchmarkManyFields.get(id=randid)
31 |
32 | loop.run_until_complete(_bench())
33 |
--------------------------------------------------------------------------------
/tests/benchmarks/test_relations.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from tests.testmodels import Event
4 |
5 |
6 | def test_relations_values_related_m2m(benchmark, create_team_with_participants):
7 | loop = asyncio.get_event_loop()
8 |
9 | @benchmark
10 | def bench():
11 | async def _bench():
12 | await Event.all().values("participants__name")
13 |
14 | loop.run_until_complete(_bench())
15 |
--------------------------------------------------------------------------------
/tests/contrib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tests/contrib/__init__.py
--------------------------------------------------------------------------------
/tests/contrib/mysql/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tests/contrib/mysql/__init__.py
--------------------------------------------------------------------------------
/tests/contrib/mysql/fields.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 | from tests import testmodels_mysql
4 | from tortoise.contrib import test
5 | from tortoise.exceptions import IntegrityError
6 |
7 |
8 | class TestMySQLUUIDFields(test.TestCase):
9 | async def test_empty(self):
10 | with self.assertRaises(IntegrityError):
11 | await testmodels_mysql.UUIDFields.create()
12 |
13 | async def test_create(self):
14 | data = uuid.uuid4()
15 | obj0 = await testmodels_mysql.UUIDFields.create(data=data)
16 | self.assertIsInstance(obj0.data, bytes)
17 | self.assertIsInstance(obj0.data_auto, bytes)
18 | self.assertEqual(obj0.data_null, None)
19 | obj = await testmodels_mysql.UUIDFields.get(id=obj0.id)
20 | self.assertIsInstance(obj.data, uuid.UUID)
21 | self.assertIsInstance(obj.data_auto, uuid.UUID)
22 | self.assertEqual(obj.data, data)
23 | self.assertEqual(obj.data_null, None)
24 | await obj.save()
25 | obj2 = await testmodels_mysql.UUIDFields.get(id=obj.id)
26 | self.assertEqual(obj, obj2)
27 |
28 | await obj.delete()
29 | obj = await testmodels_mysql.UUIDFields.filter(id=obj0.id).first()
30 | self.assertEqual(obj, None)
31 |
32 | async def test_update(self):
33 | data = uuid.uuid4()
34 | data2 = uuid.uuid4()
35 | obj0 = await testmodels_mysql.UUIDFields.create(data=data)
36 | await testmodels_mysql.UUIDFields.filter(id=obj0.id).update(data=data2)
37 | obj = await testmodels_mysql.UUIDFields.get(id=obj0.id)
38 | self.assertEqual(obj.data, data2)
39 | self.assertEqual(obj.data_null, None)
40 |
41 | async def test_create_not_null(self):
42 | data = uuid.uuid4()
43 | obj0 = await testmodels_mysql.UUIDFields.create(data=data, data_null=data)
44 | obj = await testmodels_mysql.UUIDFields.get(id=obj0.id)
45 | self.assertEqual(obj.data, data)
46 | self.assertEqual(obj.data_null, data)
47 |
--------------------------------------------------------------------------------
/tests/contrib/postgres/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tests/contrib/postgres/__init__.py
--------------------------------------------------------------------------------
/tests/contrib/test_fastapi.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import AsyncMock, patch
2 |
3 | from fastapi import FastAPI
4 |
5 | from tortoise.contrib import test
6 | from tortoise.contrib.fastapi import RegisterTortoise
7 |
8 |
9 | class TestRegisterTortoise(test.TestCase):
10 | @test.requireCapability(dialect="sqlite")
11 | @patch("tortoise.Tortoise.init")
12 | @patch("tortoise.connections.close_all")
13 | async def test_await(
14 | self,
15 | mocked_close: AsyncMock,
16 | mocked_init: AsyncMock,
17 | ) -> None:
18 | app = FastAPI()
19 | orm = await RegisterTortoise(
20 | app,
21 | db_url="sqlite://:memory:",
22 | modules={"models": ["__main__"]},
23 | )
24 | mocked_init.assert_awaited_once()
25 | mocked_init.assert_called_once_with(
26 | config=None,
27 | config_file=None,
28 | db_url="sqlite://:memory:",
29 | modules={"models": ["__main__"]},
30 | use_tz=False,
31 | timezone="UTC",
32 | _create_db=False,
33 | )
34 | await orm.close_orm()
35 | mocked_close.assert_awaited_once()
36 |
--------------------------------------------------------------------------------
/tests/contrib/test_functions.py:
--------------------------------------------------------------------------------
1 | from tests.testmodels import IntFields
2 | from tortoise import connections
3 | from tortoise.contrib import test
4 | from tortoise.contrib.mysql.functions import Rand
5 | from tortoise.contrib.postgres.functions import Random as PostgresRandom
6 | from tortoise.contrib.sqlite.functions import Random as SqliteRandom
7 |
8 |
9 | class TestFunction(test.TestCase):
10 | async def asyncSetUp(self):
11 | await super().asyncSetUp()
12 | self.intfields = [await IntFields.create(intnum=val) for val in range(10)]
13 | self.db = connections.get("models")
14 |
15 | @test.requireCapability(dialect="mysql")
16 | async def test_mysql_func_rand(self):
17 | sql = IntFields.all().annotate(randnum=Rand()).values("intnum", "randnum").sql()
18 | expected_sql = "SELECT `intnum` `intnum`,RAND() `randnum` FROM `intfields`"
19 | self.assertEqual(sql, expected_sql)
20 |
21 | @test.requireCapability(dialect="mysql")
22 | async def test_mysql_func_rand_with_seed(self):
23 | sql = IntFields.all().annotate(randnum=Rand(0)).values("intnum", "randnum").sql()
24 | expected_sql = "SELECT `intnum` `intnum`,RAND(%s) `randnum` FROM `intfields`"
25 | self.assertEqual(sql, expected_sql)
26 |
27 | @test.requireCapability(dialect="postgres")
28 | async def test_postgres_func_rand(self):
29 | sql = IntFields.all().annotate(randnum=PostgresRandom()).values("intnum", "randnum").sql()
30 | expected_sql = 'SELECT "intnum" "intnum",RANDOM() "randnum" FROM "intfields"'
31 | self.assertEqual(sql, expected_sql)
32 |
33 | @test.requireCapability(dialect="sqlite")
34 | async def test_sqlite_func_rand(self):
35 | sql = IntFields.all().annotate(randnum=SqliteRandom()).values("intnum", "randnum").sql()
36 | expected_sql = 'SELECT "intnum" "intnum",RANDOM() "randnum" FROM "intfields"'
37 | self.assertEqual(sql, expected_sql)
38 |
--------------------------------------------------------------------------------
/tests/contrib/test_tester.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=W1503
2 | from tortoise.contrib import test
3 |
4 |
5 | class TestTesterSync(test.SimpleTestCase):
6 | def setUp(self):
7 | self.moo = "SET"
8 |
9 | def tearDown(self):
10 | self.assertEqual(self.moo, "SET")
11 |
12 | @test.skip("Skip it")
13 | def test_skip(self):
14 | self.assertTrue(False)
15 |
16 | @test.expectedFailure
17 | def test_fail(self):
18 | self.assertTrue(False)
19 |
20 | def test_moo(self):
21 | self.assertEqual(self.moo, "SET")
22 |
23 |
24 | class TestTesterASync(test.SimpleTestCase):
25 | async def asyncSetUp(self):
26 | await super().asyncSetUp()
27 | self.baa = "TES"
28 |
29 | def tearDown(self):
30 | self.assertEqual(self.baa, "TES")
31 |
32 | @test.skip("Skip it")
33 | async def test_skip(self):
34 | self.assertTrue(False)
35 |
36 | @test.expectedFailure
37 | async def test_fail(self):
38 | self.assertTrue(False)
39 |
40 | async def test_moo(self):
41 | self.assertEqual(self.baa, "TES")
42 |
--------------------------------------------------------------------------------
/tests/fields/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tests/fields/__init__.py
--------------------------------------------------------------------------------
/tests/fields/subclass_fields.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from enum import Enum, IntEnum
4 | from typing import Any
5 |
6 | from tortoise import ConfigurationError
7 | from tortoise.fields import CharField, IntField
8 |
9 |
10 | class EnumField(CharField):
11 | """
12 | An example extension to CharField that serializes Enums
13 | to and from a Text representation in the DB.
14 | """
15 |
16 | __slots__ = ("enum_type",)
17 |
18 | def __init__(self, enum_type: type[Enum], **kwargs):
19 | super().__init__(128, **kwargs)
20 | if not issubclass(enum_type, Enum):
21 | raise ConfigurationError(f"{enum_type} is not a subclass of Enum!")
22 | self.enum_type = enum_type
23 |
24 | def to_db_value(self, value, instance):
25 | self.validate(value)
26 |
27 | if value is None:
28 | return None
29 |
30 | if not isinstance(value, self.enum_type):
31 | raise TypeError(f"Expected type {self.enum_type}, got {value}")
32 |
33 | return value.value
34 |
35 | def to_python_value(self, value):
36 | if value is None or isinstance(value, self.enum_type):
37 | return value
38 |
39 | try:
40 | return self.enum_type(value)
41 | except ValueError:
42 | raise ValueError(f"Database value {value} does not exist on Enum {self.enum_type}.")
43 |
44 |
45 | class IntEnumField(IntField):
46 | """
47 | An example extension to CharField that serializes Enums
48 | to and from a Text representation in the DB.
49 | """
50 |
51 | __slots__ = ("enum_type",)
52 |
53 | def __init__(self, enum_type: type[IntEnum], **kwargs):
54 | super().__init__(**kwargs)
55 | if not issubclass(enum_type, IntEnum):
56 | raise ConfigurationError(f"{enum_type} is not a subclass of IntEnum!")
57 | self.enum_type = enum_type
58 |
59 | def to_db_value(self, value: Any, instance) -> Any:
60 | self.validate(value)
61 |
62 | if value is None:
63 | return value
64 | if not isinstance(value, self.enum_type):
65 | raise TypeError(f"Expected type {self.enum_type}, got {value}")
66 |
67 | return value.value
68 |
69 | def to_python_value(self, value: Any) -> Any:
70 | if value is None or isinstance(value, self.enum_type):
71 | return value
72 |
73 | try:
74 | return self.enum_type(value)
75 | except ValueError:
76 | raise ValueError(f"Database value {value} does not exist on Enum {self.enum_type}.")
77 |
--------------------------------------------------------------------------------
/tests/fields/subclass_models.py:
--------------------------------------------------------------------------------
1 | from enum import Enum, IntEnum
2 |
3 | from tests.fields.subclass_fields import EnumField, IntEnumField
4 | from tortoise import fields
5 | from tortoise.models import Model
6 |
7 |
8 | class RacePlacingEnum(Enum):
9 | FIRST = "first"
10 | SECOND = "second"
11 | THIRD = "third"
12 | RUNNER_UP = "runner_up"
13 | DNF = "dnf"
14 |
15 |
16 | class RaceParticipant(Model):
17 | id = fields.IntField(primary_key=True)
18 | first_name = fields.CharField(max_length=64)
19 | place = EnumField(RacePlacingEnum, default=RacePlacingEnum.DNF)
20 | predicted_place = EnumField(RacePlacingEnum, null=True)
21 |
22 |
23 | class ContactTypeEnum(IntEnum):
24 | work = 1
25 | home = 2
26 | other = 3
27 |
28 |
29 | class Contact(Model):
30 | id = fields.IntField(primary_key=True)
31 | type = IntEnumField(ContactTypeEnum, default=ContactTypeEnum.other)
32 |
--------------------------------------------------------------------------------
/tests/fields/test_binary.py:
--------------------------------------------------------------------------------
1 | from tests import testmodels
2 | from tortoise.contrib import test
3 | from tortoise.exceptions import ConfigurationError, IntegrityError
4 | from tortoise.fields import BinaryField
5 |
6 |
7 | class TestBinaryFields(test.TestCase):
8 | async def test_empty(self):
9 | with self.assertRaises(IntegrityError):
10 | await testmodels.BinaryFields.create()
11 |
12 | async def test_create(self):
13 | obj0 = await testmodels.BinaryFields.create(binary=bytes(range(256)) * 500)
14 | obj = await testmodels.BinaryFields.get(id=obj0.id)
15 | self.assertEqual(obj.binary, bytes(range(256)) * 500)
16 | self.assertEqual(obj.binary_null, None)
17 | await obj.save()
18 | obj2 = await testmodels.BinaryFields.get(id=obj.id)
19 | self.assertEqual(obj, obj2)
20 |
21 | async def test_values(self):
22 | obj0 = await testmodels.BinaryFields.create(
23 | binary=bytes(range(256)), binary_null=bytes(range(255, -1, -1))
24 | )
25 | values = await testmodels.BinaryFields.get(id=obj0.id).values("binary", "binary_null")
26 | self.assertEqual(values["binary"], bytes(range(256)))
27 | self.assertEqual(values["binary_null"], bytes(range(255, -1, -1)))
28 |
29 | async def test_values_list(self):
30 | obj0 = await testmodels.BinaryFields.create(binary=bytes(range(256)))
31 | values = await testmodels.BinaryFields.get(id=obj0.id).values_list("binary", flat=True)
32 | self.assertEqual(values, bytes(range(256)))
33 |
34 | def test_unique_fail(self):
35 | with self.assertRaisesRegex(ConfigurationError, "can't be indexed"):
36 | BinaryField(unique=True)
37 |
38 | def test_index_fail(self):
39 | with self.assertRaisesRegex(ConfigurationError, "can't be indexed"):
40 | with self.assertWarnsRegex(
41 | DeprecationWarning, "`index` is deprecated, please use `db_index` instead"
42 | ):
43 | BinaryField(index=True)
44 | with self.assertRaisesRegex(ConfigurationError, "can't be indexed"):
45 | BinaryField(db_index=True)
46 |
--------------------------------------------------------------------------------
/tests/fields/test_bool.py:
--------------------------------------------------------------------------------
1 | from tests import testmodels
2 | from tortoise.contrib import test
3 | from tortoise.exceptions import IntegrityError
4 |
5 |
6 | class TestBooleanFields(test.TestCase):
7 | async def test_empty(self):
8 | with self.assertRaises(IntegrityError):
9 | await testmodels.BooleanFields.create()
10 |
11 | async def test_create(self):
12 | obj0 = await testmodels.BooleanFields.create(boolean=True)
13 | obj = await testmodels.BooleanFields.get(id=obj0.id)
14 | self.assertIs(obj.boolean, True)
15 | self.assertIs(obj.boolean_null, None)
16 | await obj.save()
17 | obj2 = await testmodels.BooleanFields.get(id=obj.id)
18 | self.assertEqual(obj, obj2)
19 |
20 | async def test_update(self):
21 | obj0 = await testmodels.BooleanFields.create(boolean=False)
22 | await testmodels.BooleanFields.filter(id=obj0.id).update(boolean=False)
23 | obj = await testmodels.BooleanFields.get(id=obj0.id)
24 | self.assertIs(obj.boolean, False)
25 | self.assertIs(obj.boolean_null, None)
26 |
27 | async def test_values(self):
28 | obj0 = await testmodels.BooleanFields.create(boolean=True)
29 | values = await testmodels.BooleanFields.get(id=obj0.id).values("boolean")
30 | self.assertIs(values["boolean"], True)
31 |
32 | async def test_values_list(self):
33 | obj0 = await testmodels.BooleanFields.create(boolean=True)
34 | values = await testmodels.BooleanFields.get(id=obj0.id).values_list("boolean", flat=True)
35 | self.assertIs(values, True)
36 |
--------------------------------------------------------------------------------
/tests/fields/test_char.py:
--------------------------------------------------------------------------------
1 | from tests import testmodels
2 | from tortoise import fields
3 | from tortoise.contrib import test
4 | from tortoise.exceptions import ConfigurationError, ValidationError
5 |
6 |
7 | class TestCharFields(test.TestCase):
8 | def test_max_length_missing(self):
9 | with self.assertRaisesRegex(
10 | TypeError, "missing 1 required positional argument: 'max_length'"
11 | ):
12 | fields.CharField() # pylint: disable=E1120
13 |
14 | def test_max_length_bad(self):
15 | with self.assertRaisesRegex(ConfigurationError, "'max_length' must be >= 1"):
16 | fields.CharField(max_length=0)
17 |
18 | async def test_empty(self):
19 | with self.assertRaises(ValidationError):
20 | await testmodels.CharFields.create()
21 |
22 | async def test_create(self):
23 | obj0 = await testmodels.CharFields.create(char="moo")
24 | obj = await testmodels.CharFields.get(id=obj0.id)
25 | self.assertEqual(obj.char, "moo")
26 | self.assertEqual(obj.char_null, None)
27 | await obj.save()
28 | obj2 = await testmodels.CharFields.get(id=obj.id)
29 | self.assertEqual(obj, obj2)
30 |
31 | async def test_update(self):
32 | obj0 = await testmodels.CharFields.create(char="moo")
33 | await testmodels.CharFields.filter(id=obj0.id).update(char="ba'a")
34 | obj = await testmodels.CharFields.get(id=obj0.id)
35 | self.assertEqual(obj.char, "ba'a")
36 | self.assertEqual(obj.char_null, None)
37 |
38 | async def test_cast(self):
39 | obj0 = await testmodels.CharFields.create(char=33)
40 | obj = await testmodels.CharFields.get(id=obj0.id)
41 | self.assertEqual(obj.char, "33")
42 |
43 | async def test_values(self):
44 | obj0 = await testmodels.CharFields.create(char="moo")
45 | values = await testmodels.CharFields.get(id=obj0.id).values("char")
46 | self.assertEqual(values["char"], "moo")
47 |
48 | async def test_values_list(self):
49 | obj0 = await testmodels.CharFields.create(char="moo")
50 | values = await testmodels.CharFields.get(id=obj0.id).values_list("char", flat=True)
51 | self.assertEqual(values, "moo")
52 |
--------------------------------------------------------------------------------
/tests/fields/test_common.py:
--------------------------------------------------------------------------------
1 | from tortoise import fields
2 | from tortoise.contrib import test
3 |
4 |
5 | class TestRequired(test.SimpleTestCase):
6 | async def test_required_by_default(self):
7 | self.assertTrue(fields.Field().required)
8 |
9 | async def test_if_generated_then_not_required(self):
10 | self.assertFalse(fields.Field(generated=True).required)
11 |
12 | async def test_if_null_then_not_required(self):
13 | self.assertFalse(fields.Field(null=True).required)
14 |
15 | async def test_if_has_non_null_default_then_not_required(self):
16 | self.assertFalse(fields.TextField(default="").required)
17 |
18 | async def test_if_null_default_then_required(self):
19 | self.assertTrue(fields.TextField(default=None).required)
20 |
--------------------------------------------------------------------------------
/tests/fields/test_float.py:
--------------------------------------------------------------------------------
1 | from decimal import Decimal
2 |
3 | from tests import testmodels
4 | from tortoise.contrib import test
5 | from tortoise.exceptions import IntegrityError
6 | from tortoise.expressions import F
7 |
8 |
9 | class TestFloatFields(test.TestCase):
10 | async def test_empty(self):
11 | with self.assertRaises(IntegrityError):
12 | await testmodels.FloatFields.create()
13 |
14 | async def test_create(self):
15 | obj0 = await testmodels.FloatFields.create(floatnum=1.23)
16 | obj = await testmodels.FloatFields.get(id=obj0.id)
17 | self.assertEqual(obj.floatnum, 1.23)
18 | self.assertNotEqual(Decimal(obj.floatnum), Decimal("1.23"))
19 | self.assertEqual(obj.floatnum_null, None)
20 | await obj.save()
21 | obj2 = await testmodels.FloatFields.get(id=obj.id)
22 | self.assertEqual(obj, obj2)
23 |
24 | async def test_update(self):
25 | obj0 = await testmodels.FloatFields.create(floatnum=1.23)
26 | await testmodels.FloatFields.filter(id=obj0.id).update(floatnum=2.34)
27 | obj = await testmodels.FloatFields.get(id=obj0.id)
28 | self.assertEqual(obj.floatnum, 2.34)
29 | self.assertNotEqual(Decimal(obj.floatnum), Decimal("2.34"))
30 | self.assertEqual(obj.floatnum_null, None)
31 |
32 | async def test_cast_int(self):
33 | obj0 = await testmodels.FloatFields.create(floatnum=123)
34 | obj = await testmodels.FloatFields.get(id=obj0.id)
35 | self.assertEqual(obj.floatnum, 123)
36 |
37 | async def test_cast_decimal(self):
38 | obj0 = await testmodels.FloatFields.create(floatnum=Decimal("1.23"))
39 | obj = await testmodels.FloatFields.get(id=obj0.id)
40 | self.assertEqual(obj.floatnum, 1.23)
41 |
42 | async def test_values(self):
43 | obj0 = await testmodels.FloatFields.create(floatnum=1.23)
44 | values = await testmodels.FloatFields.filter(id=obj0.id).values("floatnum")
45 | self.assertEqual(values[0]["floatnum"], 1.23)
46 |
47 | async def test_values_list(self):
48 | obj0 = await testmodels.FloatFields.create(floatnum=1.23)
49 | values = await testmodels.FloatFields.filter(id=obj0.id).values_list("floatnum")
50 | self.assertEqual(list(values[0]), [1.23])
51 |
52 | async def test_f_expression(self):
53 | obj0 = await testmodels.FloatFields.create(floatnum=1.23)
54 | await obj0.filter(id=obj0.id).update(floatnum=F("floatnum") + 0.01)
55 | obj1 = await testmodels.FloatFields.get(id=obj0.id)
56 | self.assertEqual(obj1.floatnum, 1.24)
57 |
--------------------------------------------------------------------------------
/tests/fields/test_text.py:
--------------------------------------------------------------------------------
1 | from tests import testmodels
2 | from tortoise.contrib import test
3 | from tortoise.exceptions import ConfigurationError, IntegrityError
4 | from tortoise.fields import TextField
5 |
6 |
7 | class TestTextFields(test.TestCase):
8 | async def test_empty(self):
9 | with self.assertRaises(IntegrityError):
10 | await testmodels.TextFields.create()
11 |
12 | async def test_create(self):
13 | obj0 = await testmodels.TextFields.create(text="baaa" * 32000)
14 | obj = await testmodels.TextFields.get(id=obj0.id)
15 | self.assertEqual(obj.text, "baaa" * 32000)
16 | self.assertEqual(obj.text_null, None)
17 | await obj.save()
18 | obj2 = await testmodels.TextFields.get(id=obj.id)
19 | self.assertEqual(obj, obj2)
20 |
21 | async def test_values(self):
22 | obj0 = await testmodels.TextFields.create(text="baa")
23 | values = await testmodels.TextFields.get(id=obj0.id).values("text")
24 | self.assertEqual(values["text"], "baa")
25 |
26 | async def test_values_list(self):
27 | obj0 = await testmodels.TextFields.create(text="baa")
28 | values = await testmodels.TextFields.get(id=obj0.id).values_list("text", flat=True)
29 | self.assertEqual(values, "baa")
30 |
31 | def test_unique_fail(self):
32 | msg = "TextField can't be indexed, consider CharField"
33 | with self.assertRaisesRegex(ConfigurationError, msg):
34 | with self.assertWarnsRegex(
35 | DeprecationWarning, "`index` is deprecated, please use `db_index` instead"
36 | ):
37 | TextField(index=True)
38 | with self.assertRaisesRegex(ConfigurationError, msg):
39 | TextField(db_index=True)
40 |
41 | def test_index_fail(self):
42 | with self.assertRaisesRegex(ConfigurationError, "can't be indexed, consider CharField"):
43 | TextField(index=True)
44 |
45 | def test_pk_deprecated(self):
46 | with self.assertWarnsRegex(
47 | DeprecationWarning, "TextField as a PrimaryKey is Deprecated, use CharField"
48 | ):
49 | TextField(primary_key=True)
50 |
--------------------------------------------------------------------------------
/tests/fields/test_uuid.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 | from tests import testmodels
4 | from tortoise.contrib import test
5 | from tortoise.exceptions import IntegrityError
6 |
7 |
8 | class TestUUIDFields(test.TestCase):
9 | async def test_empty(self):
10 | with self.assertRaises(IntegrityError):
11 | await testmodels.UUIDFields.create()
12 |
13 | async def test_create(self):
14 | data = uuid.uuid4()
15 | obj0 = await testmodels.UUIDFields.create(data=data)
16 | self.assertIsInstance(obj0.data, uuid.UUID)
17 | self.assertIsInstance(obj0.data_auto, uuid.UUID)
18 | self.assertEqual(obj0.data_null, None)
19 | obj = await testmodels.UUIDFields.get(id=obj0.id)
20 | self.assertIsInstance(obj.data, uuid.UUID)
21 | self.assertIsInstance(obj.data_auto, uuid.UUID)
22 | self.assertEqual(obj.data, data)
23 | self.assertEqual(obj.data_null, None)
24 | await obj.save()
25 | obj2 = await testmodels.UUIDFields.get(id=obj.id)
26 | self.assertEqual(obj, obj2)
27 |
28 | await obj.delete()
29 | obj = await testmodels.UUIDFields.filter(id=obj0.id).first()
30 | self.assertEqual(obj, None)
31 |
32 | async def test_update(self):
33 | data = uuid.uuid4()
34 | data2 = uuid.uuid4()
35 | obj0 = await testmodels.UUIDFields.create(data=data)
36 | await testmodels.UUIDFields.filter(id=obj0.id).update(data=data2)
37 | obj = await testmodels.UUIDFields.get(id=obj0.id)
38 | self.assertEqual(obj.data, data2)
39 | self.assertEqual(obj.data_null, None)
40 |
41 | async def test_create_not_null(self):
42 | data = uuid.uuid4()
43 | obj0 = await testmodels.UUIDFields.create(data=data, data_null=data)
44 | obj = await testmodels.UUIDFields.get(id=obj0.id)
45 | self.assertEqual(obj.data, data)
46 | self.assertEqual(obj.data_null, data)
47 |
--------------------------------------------------------------------------------
/tests/model_setup/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tests/model_setup/__init__.py
--------------------------------------------------------------------------------
/tests/model_setup/init.json:
--------------------------------------------------------------------------------
1 | {
2 | "connections": {
3 | "default": {
4 | "engine": "tortoise.backends.sqlite",
5 | "credentials": {
6 | "file_path": "/tmp/tester.sqlite"
7 | }
8 | }
9 | },
10 | "apps": {
11 | "models": {
12 | "models": [
13 | "tests.testmodels"
14 | ],
15 | "default_connection": "default"
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/model_setup/init.yaml:
--------------------------------------------------------------------------------
1 | apps:
2 | models:
3 | default_connection: default
4 | models:
5 | - tests.testmodels
6 | connections:
7 | default:
8 | credentials:
9 | file_path: /tmp/tester.sqlite
10 | engine: tortoise.backends.sqlite
11 |
--------------------------------------------------------------------------------
/tests/model_setup/model_bad_rel1.py:
--------------------------------------------------------------------------------
1 | """
2 | Testing Models for a bad/wrong relation reference
3 | """
4 |
5 | from tortoise import fields
6 | from tortoise.models import Model
7 |
8 |
9 | class Tournament(Model):
10 | id = fields.IntField(primary_key=True)
11 |
12 |
13 | class Event(Model):
14 | tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
15 | "app.Tournament", related_name="events"
16 | )
17 |
--------------------------------------------------------------------------------
/tests/model_setup/model_bad_rel2.py:
--------------------------------------------------------------------------------
1 | """
2 | Testing Models for a bad/wrong relation reference
3 | The model 'Tour' does not exist
4 | """
5 |
6 | from __future__ import annotations
7 |
8 | from typing import Any
9 |
10 | from tortoise import fields
11 | from tortoise.models import Model
12 |
13 |
14 | class Tournament(Model):
15 | id = fields.IntField(primary_key=True)
16 |
17 |
18 | class Event(Model):
19 | tournament: fields.ForeignKeyRelation[Any] = fields.ForeignKeyField(
20 | "models.Tour", related_name="events"
21 | )
22 |
--------------------------------------------------------------------------------
/tests/model_setup/model_bad_rel3.py:
--------------------------------------------------------------------------------
1 | """
2 | Testing Models for a bad/wrong relation reference
3 | Wrong reference. App missing.
4 | """
5 |
6 | from tortoise import fields
7 | from tortoise.models import Model
8 |
9 |
10 | class Tournament(Model):
11 | id = fields.IntField(primary_key=True)
12 |
13 |
14 | class Event(Model):
15 | tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
16 | "Tournament", related_name="events"
17 | )
18 |
--------------------------------------------------------------------------------
/tests/model_setup/model_bad_rel4.py:
--------------------------------------------------------------------------------
1 | """
2 | Testing Models for a bad/wrong relation reference
3 | Wrong reference. Two '.' in reference.
4 | """
5 |
6 | from tortoise import fields
7 | from tortoise.models import Model
8 |
9 |
10 | class Tournament(Model):
11 | id = fields.IntField(primary_key=True)
12 |
13 |
14 | class Event(Model):
15 | tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
16 | "models.app.Tournament", related_name="events"
17 | )
18 |
--------------------------------------------------------------------------------
/tests/model_setup/model_bad_rel5.py:
--------------------------------------------------------------------------------
1 | """
2 | Testing Models for a bad/wrong relation reference
3 | Wrong reference. App missing.
4 | """
5 |
6 | from tortoise import fields
7 | from tortoise.models import Model
8 |
9 |
10 | class Tournament(Model):
11 | id = fields.IntField(primary_key=True)
12 |
13 |
14 | class Event(Model):
15 | tournament: fields.OneToOneRelation[Tournament] = fields.OneToOneField("Tournament")
16 |
--------------------------------------------------------------------------------
/tests/model_setup/model_bad_rel6.py:
--------------------------------------------------------------------------------
1 | """
2 | Testing Models for a bad/wrong relation reference
3 | Wrong reference. fk field parameter `to_field` with non unique field.
4 | """
5 |
6 | from tortoise import fields
7 | from tortoise.models import Model
8 |
9 |
10 | class Tournament(Model):
11 | uuid = fields.UUIDField(unique=False)
12 |
13 |
14 | class Event(Model):
15 | tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
16 | "models.Tournament", related_name="events", to_field="uuid"
17 | )
18 |
--------------------------------------------------------------------------------
/tests/model_setup/model_bad_rel7.py:
--------------------------------------------------------------------------------
1 | """
2 | Testing Models for a bad/wrong relation reference
3 | Wrong reference. fk field parameter `to_field` with non exist field.
4 | """
5 |
6 | from tortoise import fields
7 | from tortoise.models import Model
8 |
9 |
10 | class Tournament(Model):
11 | uuid = fields.UUIDField(unique=True)
12 |
13 |
14 | class Event(Model):
15 | tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
16 | "models.Tournament", related_name="events", to_field="uuids"
17 | )
18 |
--------------------------------------------------------------------------------
/tests/model_setup/model_bad_rel8.py:
--------------------------------------------------------------------------------
1 | """
2 | Testing Models for a bad/wrong relation reference
3 | Wrong reference. o2o field parameter `to_field` with non unique field.
4 | """
5 |
6 | from tortoise import fields
7 | from tortoise.models import Model
8 |
9 |
10 | class Tournament(Model):
11 | uuid = fields.UUIDField(unique=False)
12 |
13 |
14 | class Event(Model):
15 | tournament: fields.OneToOneRelation[Tournament] = fields.OneToOneField(
16 | "models.Tournament", related_name="events", to_field="uuid"
17 | )
18 |
--------------------------------------------------------------------------------
/tests/model_setup/model_bad_rel9.py:
--------------------------------------------------------------------------------
1 | """
2 | Testing Models for a bad/wrong relation reference
3 | Wrong reference. o2o field parameter `to_field` with non exist field.
4 | """
5 |
6 | from tortoise import fields
7 | from tortoise.models import Model
8 |
9 |
10 | class Tournament(Model):
11 | uuid = fields.UUIDField(unique=True)
12 |
13 |
14 | class Event(Model):
15 | tournament: fields.OneToOneRelation[Tournament] = fields.OneToOneField(
16 | "models.Tournament", related_name="events", to_field="uuids"
17 | )
18 |
--------------------------------------------------------------------------------
/tests/model_setup/model_generated_nonint.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — Generated non-int PK
3 | """
4 |
5 | from tortoise import fields
6 | from tortoise.models import Model
7 |
8 |
9 | class Tournament(Model):
10 | val = fields.CharField(max_length=50, primary_key=True, generated=True)
11 |
--------------------------------------------------------------------------------
/tests/model_setup/model_multiple_pk.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — Multiple PK
3 | """
4 |
5 | from tortoise import fields
6 | from tortoise.models import Model
7 |
8 |
9 | class Tournament(Model):
10 | id = fields.IntField(primary_key=True)
11 | id2 = fields.IntField(primary_key=True)
12 |
--------------------------------------------------------------------------------
/tests/model_setup/model_nonpk_id.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — Model with field id, but NO PK
3 | """
4 |
5 | from tortoise import fields
6 | from tortoise.models import Model
7 |
8 |
9 | class Tournament(Model):
10 | id = fields.CharField(max_length=50)
11 |
--------------------------------------------------------------------------------
/tests/model_setup/models__models__bad.py:
--------------------------------------------------------------------------------
1 | """
2 | 'Failure' tests for __models__
3 | """
4 |
5 | from tortoise import fields
6 | from tortoise.models import Model
7 |
8 |
9 | class BadTournament(Model):
10 | id = fields.IntField(primary_key=True)
11 | name = fields.TextField()
12 | created = fields.DatetimeField(auto_now_add=True, db_index=True)
13 |
14 | def __str__(self):
15 | return self.name
16 |
17 |
18 | class GoodTournament(Model):
19 | id = fields.IntField(primary_key=True)
20 | name = fields.TextField()
21 | created = fields.DatetimeField(auto_now_add=True, db_index=True)
22 |
23 | def __str__(self):
24 | return self.name
25 |
26 |
27 | class Tmp:
28 | class InAClassTournament(Model):
29 | id = fields.IntField(primary_key=True)
30 | name = fields.TextField()
31 | created = fields.DatetimeField(auto_now_add=True, db_index=True)
32 |
33 | def __str__(self):
34 | return self.name
35 |
36 |
37 | __models__ = [BadTournament]
38 |
--------------------------------------------------------------------------------
/tests/model_setup/models__models__good.py:
--------------------------------------------------------------------------------
1 | """
2 | 'Success' tests for __models__
3 | """
4 |
5 | from tortoise import fields
6 | from tortoise.models import Model
7 |
8 |
9 | class BadTournament(Model):
10 | id = fields.IntField(primary_key=True)
11 | name = fields.TextField()
12 | created = fields.DatetimeField(auto_now_add=True, db_index=True)
13 |
14 | def __str__(self):
15 | return self.name
16 |
17 |
18 | class GoodTournament(Model):
19 | id = fields.IntField(primary_key=True)
20 | name = fields.TextField()
21 | created = fields.DatetimeField(auto_now_add=True, db_index=True)
22 |
23 | def __str__(self):
24 | return self.name
25 |
26 |
27 | class Tmp:
28 | class InAClassTournament(Model):
29 | id = fields.IntField(primary_key=True)
30 | name = fields.TextField()
31 | created = fields.DatetimeField(auto_now_add=True, db_index=True)
32 |
33 | def __str__(self):
34 | return self.name
35 |
36 |
37 | __models__ = [GoodTournament, Tmp.InAClassTournament]
38 |
--------------------------------------------------------------------------------
/tests/model_setup/models_dup1.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — Duplicate 1
3 | """
4 |
5 | from tortoise import fields
6 | from tortoise.models import Model
7 |
8 |
9 | class Tournament(Model):
10 | id = fields.IntField(primary_key=True)
11 |
12 |
13 | class Event(Model):
14 | tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
15 | "models.Tournament", related_name="events"
16 | )
17 |
18 |
19 | class Party(Model):
20 | tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
21 | "models.Tournament", related_name="events"
22 | )
23 |
--------------------------------------------------------------------------------
/tests/model_setup/models_dup2.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — Duplicate 1
3 | """
4 |
5 | from tortoise import fields
6 | from tortoise.models import Model
7 |
8 |
9 | class Event(Model):
10 | participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
11 | "models.Team", related_name="events", through="event_team"
12 | )
13 |
14 |
15 | class Party(Model):
16 | participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
17 | "models.Team", related_name="events", through="event_team"
18 | )
19 |
20 |
21 | class Team(Model):
22 | id = fields.IntField(primary_key=True)
23 |
--------------------------------------------------------------------------------
/tests/model_setup/models_dup3.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — Duplicate 3
3 | """
4 |
5 | from tortoise import fields
6 | from tortoise.models import Model
7 |
8 |
9 | class Tournament(Model):
10 | id = fields.IntField(primary_key=True)
11 | event = fields.CharField(max_length=32)
12 |
13 |
14 | class Event(Model):
15 | tournament: fields.OneToOneRelation[Tournament] = fields.OneToOneField(
16 | "models.Tournament", related_name="event"
17 | )
18 |
--------------------------------------------------------------------------------
/tests/model_setup/test__models__.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for __models__
3 | """
4 |
5 | import re
6 | from unittest.mock import AsyncMock, patch
7 |
8 | from tortoise import Tortoise, connections
9 | from tortoise.contrib import test
10 | from tortoise.exceptions import ConfigurationError
11 | from tortoise.utils import get_schema_sql
12 |
13 |
14 | class TestGenerateSchema(test.SimpleTestCase):
15 | async def asyncSetUp(self):
16 | await super().asyncSetUp()
17 | try:
18 | Tortoise.apps = {}
19 | Tortoise._inited = False
20 | except ConfigurationError:
21 | pass
22 | Tortoise._inited = False
23 | self.sqls = ""
24 | self.post_sqls = ""
25 | self.engine = test.getDBConfig(app_label="models", modules=[])["connections"]["models"][
26 | "engine"
27 | ]
28 |
29 | async def init_for(self, module: str, safe=False) -> None:
30 | if self.engine != "tortoise.backends.sqlite":
31 | raise test.SkipTest("sqlite only")
32 | with patch(
33 | "tortoise.backends.sqlite.client.SqliteClient.create_connection", new=AsyncMock()
34 | ):
35 | await Tortoise.init(
36 | {
37 | "connections": {
38 | "default": {
39 | "engine": "tortoise.backends.sqlite",
40 | "credentials": {"file_path": ":memory:"},
41 | }
42 | },
43 | "apps": {"models": {"models": [module], "default_connection": "default"}},
44 | }
45 | )
46 | self.sqls = get_schema_sql(connections.get("default"), safe).split(";\n")
47 |
48 | def get_sql(self, text: str) -> str:
49 | return str(re.sub(r"[ \t\n\r]+", " ", [sql for sql in self.sqls if text in sql][0]))
50 |
51 | async def test_good(self):
52 | await self.init_for("tests.model_setup.models__models__good")
53 | self.assertIn("goodtournament", "; ".join(self.sqls))
54 | self.assertIn("inaclasstournament", "; ".join(self.sqls))
55 | self.assertNotIn("badtournament", "; ".join(self.sqls))
56 |
57 | async def test_bad(self):
58 | await self.init_for("tests.model_setup.models__models__bad")
59 | self.assertNotIn("goodtournament", "; ".join(self.sqls))
60 | self.assertNotIn("inaclasstournament", "; ".join(self.sqls))
61 | self.assertIn("badtournament", "; ".join(self.sqls))
62 |
--------------------------------------------------------------------------------
/tests/schema/models_cyclic.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — Cyclic
3 | """
4 |
5 | from tortoise import fields
6 | from tortoise.models import Model
7 |
8 |
9 | class One(Model):
10 | tournament: fields.ForeignKeyRelation["Two"] = fields.ForeignKeyField(
11 | "models.Two", related_name="events"
12 | )
13 |
14 |
15 | class Two(Model):
16 | tournament: fields.ForeignKeyRelation["Three"] = fields.ForeignKeyField(
17 | "models.Three", related_name="events"
18 | )
19 |
20 |
21 | class Three(Model):
22 | tournament: fields.ForeignKeyRelation[One] = fields.ForeignKeyField(
23 | "models.One", related_name="events"
24 | )
25 |
--------------------------------------------------------------------------------
/tests/schema/models_fk_1.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — FK bad model name
3 | """
4 |
5 | from __future__ import annotations
6 |
7 | from typing import Any
8 |
9 | from tortoise import fields
10 | from tortoise.models import Model
11 |
12 |
13 | class One(Model):
14 | tournament: fields.ForeignKeyRelation[Any] = fields.ForeignKeyField("moo")
15 |
--------------------------------------------------------------------------------
/tests/schema/models_fk_2.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — Bad on_delete parameter
3 | """
4 |
5 | from tests.schema.models_cyclic import Two
6 | from tortoise import fields
7 | from tortoise.models import Model
8 |
9 |
10 | class One(Model):
11 | tournament: fields.ForeignKeyRelation[Two] = fields.ForeignKeyField(
12 | "models.Two",
13 | on_delete="WABOOM", # type:ignore
14 | )
15 |
--------------------------------------------------------------------------------
/tests/schema/models_fk_3.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — on_delete SET_NULL without null=True
3 | """
4 |
5 | from tests.schema.models_cyclic import Two
6 | from tortoise import fields
7 | from tortoise.models import Model
8 |
9 |
10 | class One(Model):
11 | tournament: fields.ForeignKeyRelation[Two] = fields.ForeignKeyField(
12 | "models.Two", on_delete=fields.SET_NULL
13 | )
14 |
--------------------------------------------------------------------------------
/tests/schema/models_m2m_1.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — Cyclic
3 | """
4 |
5 | from tests.schema.models_cyclic import Two
6 | from tortoise import fields
7 | from tortoise.models import Model
8 |
9 |
10 | class One(Model):
11 | tournament: fields.ManyToManyRelation[Two] = fields.ManyToManyField("Two")
12 |
--------------------------------------------------------------------------------
/tests/schema/models_m2m_2.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — Multi ManyToMany fields
3 | """
4 |
5 | from __future__ import annotations
6 |
7 | from tortoise import Model, fields
8 |
9 |
10 | class One(Model):
11 | threes: fields.ManyToManyRelation[Three]
12 |
13 |
14 | class Two(Model):
15 | threes: fields.ManyToManyRelation[Three]
16 |
17 |
18 | class Three(Model):
19 | ones: fields.ManyToManyRelation[One] = fields.ManyToManyField("models.One")
20 | twos: fields.ManyToManyRelation[Two] = fields.ManyToManyField("models.Two")
21 |
--------------------------------------------------------------------------------
/tests/schema/models_mysql_index.py:
--------------------------------------------------------------------------------
1 | from tortoise import Model, fields
2 | from tortoise.contrib.mysql.fields import GeometryField
3 | from tortoise.contrib.mysql.indexes import FullTextIndex, SpatialIndex
4 |
5 |
6 | class Index(Model):
7 | full_text = fields.TextField()
8 | geometry = GeometryField()
9 |
10 | class Meta:
11 | indexes = [
12 | FullTextIndex(fields=("full_text",), parser_name="ngram"),
13 | SpatialIndex(fields=("geometry",)),
14 | ]
15 |
--------------------------------------------------------------------------------
/tests/schema/models_no_db_constraint.py:
--------------------------------------------------------------------------------
1 | """
2 | This example demonstrates SQL Schema generation for each DB type supported without db fk constraint.
3 | """
4 |
5 | from tortoise import fields
6 | from tortoise.models import Model
7 |
8 |
9 | class Tournament(Model):
10 | tid = fields.SmallIntField(primary_key=True)
11 | name = fields.CharField(max_length=100, description="Tournament name", db_index=True)
12 | created = fields.DatetimeField(auto_now_add=True, description="Created */'`/* datetime")
13 |
14 | class Meta:
15 | table_description = "What Tournaments */'`/* we have"
16 |
17 |
18 | class Event(Model):
19 | id = fields.BigIntField(primary_key=True, description="Event ID")
20 | name = fields.TextField()
21 | tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
22 | "models.Tournament",
23 | db_constraint=False,
24 | related_name="events",
25 | description="FK to tournament",
26 | )
27 | participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
28 | "models.Team",
29 | db_constraint=False,
30 | related_name="events",
31 | through="teamevents",
32 | description="How participants relate",
33 | )
34 | modified = fields.DatetimeField(auto_now=True)
35 | prize = fields.DecimalField(max_digits=10, decimal_places=2, null=True)
36 | token = fields.CharField(max_length=100, description="Unique token", unique=True)
37 | key = fields.CharField(max_length=100)
38 |
39 | class Meta:
40 | table_description = "This table contains a list of all the events"
41 | unique_together = [("name", "prize"), ["tournament", "key"]]
42 |
43 |
44 | class Team(Model):
45 | name = fields.CharField(max_length=50, primary_key=True, description="The TEAM name (and PK)")
46 | key = fields.IntField()
47 | manager: fields.ForeignKeyNullableRelation["Team"] = fields.ForeignKeyField(
48 | "models.Team", db_constraint=False, related_name="team_members", null=True
49 | )
50 | talks_to: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
51 | "models.Team", db_constraint=False, related_name="gets_talked_to"
52 | )
53 |
54 | class Meta:
55 | table_description = "The TEAMS!"
56 | indexes = [("manager", "key"), ["manager_id", "name"]]
57 |
--------------------------------------------------------------------------------
/tests/schema/models_o2o_2.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — Bad on_delete parameter
3 | """
4 |
5 | from tests.schema.models_cyclic import Two
6 | from tortoise import fields
7 | from tortoise.models import Model
8 |
9 |
10 | class One(Model):
11 | tournament: fields.OneToOneRelation[Two] = fields.OneToOneField(
12 | "models.Two",
13 | on_delete="WABOOM", # type:ignore
14 | )
15 |
--------------------------------------------------------------------------------
/tests/schema/models_o2o_3.py:
--------------------------------------------------------------------------------
1 | """
2 | This is the testing Models — on_delete SET_NULL without null=True
3 | """
4 |
5 | from tests.schema.models_cyclic import Two
6 | from tortoise import fields
7 | from tortoise.models import Model
8 |
9 |
10 | class One(Model):
11 | tournament: fields.OneToOneRelation[Two] = fields.OneToOneField(
12 | "models.Two", on_delete=fields.SET_NULL
13 | )
14 |
--------------------------------------------------------------------------------
/tests/schema/models_postgres_fields.py:
--------------------------------------------------------------------------------
1 | from tortoise import Model
2 | from tortoise.contrib.postgres.fields import ArrayField, TSVectorField
3 |
4 |
5 | class PostgresFields(Model):
6 | tsvector = TSVectorField()
7 | text_array = ArrayField(element_type="text", default=["a", "b", "c"])
8 | varchar_array = ArrayField(element_type="varchar(32)", default=["aa", "bbb", "cccc"])
9 | int_array = ArrayField(element_type="int", default=[1, 2, 3], null=True)
10 | real_array = ArrayField(
11 | element_type="real",
12 | default=[1.1, 2.2, 3.3],
13 | description="this is array of real numbers",
14 | )
15 |
16 | class Meta:
17 | table = "postgres_fields"
18 |
--------------------------------------------------------------------------------
/tests/schema/models_postgres_index.py:
--------------------------------------------------------------------------------
1 | from tortoise import Model, fields
2 | from tortoise.contrib.postgres.fields import TSVectorField
3 | from tortoise.contrib.postgres.indexes import (
4 | BloomIndex,
5 | BrinIndex,
6 | GinIndex,
7 | GistIndex,
8 | HashIndex,
9 | PostgreSQLIndex,
10 | SpGistIndex,
11 | )
12 |
13 |
14 | class Index(Model):
15 | bloom = fields.CharField(max_length=200)
16 | brin = fields.CharField(max_length=200)
17 | gin = TSVectorField()
18 | gist = TSVectorField()
19 | sp_gist = fields.CharField(max_length=200)
20 | hash = fields.CharField(max_length=200)
21 | partial = fields.CharField(max_length=200)
22 |
23 | class Meta:
24 | indexes = [
25 | BloomIndex(fields=("bloom",)),
26 | BrinIndex(fields=("brin",)),
27 | GinIndex(fields=("gin",)),
28 | GistIndex(fields=("gist",)),
29 | SpGistIndex(fields=("sp_gist",)),
30 | HashIndex(fields=("hash",)),
31 | PostgreSQLIndex(fields=("partial",), condition={"id": 1}),
32 | ]
33 |
--------------------------------------------------------------------------------
/tests/test_basic.py:
--------------------------------------------------------------------------------
1 | from tests.testmodels import OldStyleModel, Tournament
2 | from tortoise.contrib import test
3 |
4 |
5 | class TestBasic(test.TestCase):
6 | async def test_basic(self):
7 | tournament = await Tournament.create(name="Test")
8 | await Tournament.filter(id=tournament.id).update(name="Updated name")
9 | saved_event = await Tournament.filter(name="Updated name").first()
10 | self.assertEqual(saved_event.id, tournament.id)
11 | await Tournament(name="Test 2").save()
12 | self.assertEqual(
13 | await Tournament.all().values_list("id", flat=True),
14 | [tournament.id, tournament.id + 1],
15 | )
16 | self.assertListSortEqual(
17 | await Tournament.all().values("id", "name"),
18 | [
19 | {"id": tournament.id, "name": "Updated name"},
20 | {"id": tournament.id + 1, "name": "Test 2"},
21 | ],
22 | sorted_key="id",
23 | )
24 |
25 | async def test_basic_oldstyle(self):
26 | obj = await OldStyleModel.create(external_id=123)
27 | assert obj.pk
28 |
29 | assert OldStyleModel._meta.fields_map["id"].pk
30 | assert OldStyleModel._meta.fields_map["external_id"].index
31 |
--------------------------------------------------------------------------------
/tests/test_callable_default.py:
--------------------------------------------------------------------------------
1 | from tests import testmodels
2 | from tortoise.contrib import test
3 |
4 |
5 | class TestCallableDefault(test.TestCase):
6 | async def test_default_create(self):
7 | model = await testmodels.CallableDefault.create()
8 | self.assertEqual(model.callable_default, "callable_default")
9 | self.assertEqual(model.async_default, "async_callable_default")
10 |
11 | async def test_default_by_save(self):
12 | saved_model = testmodels.CallableDefault()
13 | await saved_model.save()
14 | self.assertEqual(saved_model.callable_default, "callable_default")
15 | self.assertEqual(saved_model.async_default, "async_callable_default")
16 |
17 | async def test_async_default_change(self):
18 | default_change = testmodels.CallableDefault()
19 | default_change.async_default = "changed"
20 | await default_change.save()
21 | self.assertEqual(default_change.async_default, "changed")
22 |
--------------------------------------------------------------------------------
/tests/test_default.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from decimal import Decimal
3 |
4 | import pytz
5 |
6 | from tests.testmodels import DefaultModel
7 | from tortoise import connections
8 | from tortoise.backends.asyncpg import AsyncpgDBClient
9 | from tortoise.backends.mssql import MSSQLClient
10 | from tortoise.backends.mysql import MySQLClient
11 | from tortoise.backends.oracle import OracleClient
12 | from tortoise.backends.psycopg import PsycopgClient
13 | from tortoise.backends.sqlite import SqliteClient
14 | from tortoise.contrib import test
15 |
16 |
17 | class TestDefault(test.TestCase):
18 | async def asyncSetUp(self) -> None:
19 | await super().asyncSetUp()
20 | db = connections.get("models")
21 | if isinstance(db, MySQLClient):
22 | await db.execute_query(
23 | "insert into defaultmodel (`int_default`,`float_default`,`decimal_default`,`bool_default`,`char_default`,`date_default`,`datetime_default`) values (DEFAULT,DEFAULT,DEFAULT,DEFAULT,DEFAULT,DEFAULT,DEFAULT)",
24 | )
25 | elif isinstance(db, SqliteClient):
26 | await db.execute_query(
27 | "insert into defaultmodel default values",
28 | )
29 | elif isinstance(db, (AsyncpgDBClient, PsycopgClient, MSSQLClient)):
30 | await db.execute_query(
31 | 'insert into defaultmodel ("int_default","float_default","decimal_default","bool_default","char_default","date_default","datetime_default") values (DEFAULT,DEFAULT,DEFAULT,DEFAULT,DEFAULT,DEFAULT,DEFAULT)',
32 | )
33 | elif isinstance(db, OracleClient):
34 | await db.execute_query(
35 | 'insert into "defaultmodel" ("int_default","float_default","decimal_default","bool_default","char_default","date_default","datetime_default") values (DEFAULT,DEFAULT,DEFAULT,DEFAULT,DEFAULT,DEFAULT,DEFAULT)',
36 | )
37 |
38 | async def test_default(self):
39 | default_model = await DefaultModel.first()
40 | self.assertEqual(default_model.int_default, 1)
41 | self.assertEqual(default_model.float_default, 1.5)
42 | self.assertEqual(default_model.decimal_default, Decimal(1))
43 | self.assertTrue(default_model.bool_default)
44 | self.assertEqual(default_model.char_default, "tortoise")
45 | self.assertEqual(default_model.date_default, datetime.date(year=2020, month=5, day=21))
46 | self.assertEqual(
47 | default_model.datetime_default,
48 | datetime.datetime(year=2020, month=5, day=20, tzinfo=pytz.utc),
49 | )
50 |
--------------------------------------------------------------------------------
/tests/test_f.py:
--------------------------------------------------------------------------------
1 | from tortoise.contrib import test
2 | from tortoise.expressions import Connector, F
3 |
4 |
5 | class TestF(test.TestCase):
6 | def test_arithmetic(self):
7 | f = F("name")
8 |
9 | negated = -f
10 | self.assertEqual(negated.connector, Connector.mul)
11 | self.assertEqual(negated.right.value, -1)
12 |
13 | added = f + 1
14 | self.assertEqual(added.connector, Connector.add)
15 | self.assertEqual(added.right.value, 1)
16 |
17 | radded = 1 + f
18 | self.assertEqual(radded.connector, Connector.add)
19 | self.assertEqual(radded.left.value, 1)
20 | self.assertEqual(radded.right, f)
21 |
22 | subbed = f - 1
23 | self.assertEqual(subbed.connector, Connector.sub)
24 | self.assertEqual(subbed.right.value, 1)
25 |
26 | rsubbed = 1 - f
27 | self.assertEqual(rsubbed.connector, Connector.sub)
28 | self.assertEqual(rsubbed.left.value, 1)
29 |
30 | mulled = f * 2
31 | self.assertEqual(mulled.connector, Connector.mul)
32 | self.assertEqual(mulled.right.value, 2)
33 |
34 | rmulled = 2 * f
35 | self.assertEqual(rmulled.connector, Connector.mul)
36 | self.assertEqual(rmulled.left.value, 2)
37 |
38 | divved = f / 2
39 | self.assertEqual(divved.connector, Connector.div)
40 | self.assertEqual(divved.right.value, 2)
41 |
42 | rdivved = 2 / f
43 | self.assertEqual(rdivved.connector, Connector.div)
44 | self.assertEqual(rdivved.left.value, 2)
45 |
46 | powed = f**2
47 | self.assertEqual(powed.connector, Connector.pow)
48 | self.assertEqual(powed.right.value, 2)
49 |
50 | rpowed = 2**f
51 | self.assertEqual(rpowed.connector, Connector.pow)
52 | self.assertEqual(rpowed.left.value, 2)
53 |
54 | modded = f % 2
55 | self.assertEqual(modded.connector, Connector.mod)
56 | self.assertEqual(modded.right.value, 2)
57 |
58 | rmodded = 2 % f
59 | self.assertEqual(rmodded.connector, Connector.mod)
60 | self.assertEqual(rmodded.left.value, 2)
61 |
--------------------------------------------------------------------------------
/tests/test_inheritance.py:
--------------------------------------------------------------------------------
1 | from tests.testmodels import MyAbstractBaseModel, MyDerivedModel
2 | from tortoise.contrib import test
3 |
4 |
5 | class TestInheritance(test.TestCase):
6 | async def test_basic(self):
7 | model = MyDerivedModel(name="test")
8 | self.assertTrue(hasattr(MyAbstractBaseModel(), "name"))
9 | self.assertTrue(hasattr(model, "created_at"))
10 | self.assertTrue(hasattr(model, "modified_at"))
11 | self.assertTrue(hasattr(model, "name"))
12 | self.assertTrue(hasattr(model, "first_name"))
13 | await model.save()
14 | self.assertIsNotNone(model.created_at)
15 | self.assertIsNotNone(model.modified_at)
16 |
--------------------------------------------------------------------------------
/tests/test_manager.py:
--------------------------------------------------------------------------------
1 | from tests.testmodels import ManagerModel, ManagerModelExtra
2 | from tortoise.contrib import test
3 |
4 |
5 | class TestManager(test.TestCase):
6 | async def test_manager(self):
7 | m1 = await ManagerModel.create()
8 | m2 = await ManagerModel.create(status=1)
9 |
10 | self.assertEqual(await ManagerModel.all().active().count(), 1)
11 | self.assertEqual(await ManagerModel.all_objects.count(), 2)
12 |
13 | self.assertIsNone(await ManagerModel.all().active().get_or_none(pk=m1.pk))
14 | self.assertIsNotNone(await ManagerModel.all_objects.get_or_none(pk=m1.pk))
15 | self.assertIsNotNone(await ManagerModel.get_or_none(pk=m2.pk))
16 |
17 | await ManagerModelExtra.create(extra="extra")
18 | self.assertEqual(await ManagerModelExtra.all_objects.count(), 1)
19 | self.assertEqual(await ManagerModelExtra.all().count(), 1)
20 |
--------------------------------------------------------------------------------
/tests/test_manual_sql.py:
--------------------------------------------------------------------------------
1 | from tortoise import connections
2 | from tortoise.contrib import test
3 | from tortoise.transactions import in_transaction
4 |
5 |
6 | class TestManualSQL(test.TruncationTestCase):
7 | async def test_simple_insert(self):
8 | conn = connections.get("models")
9 | await conn.execute_query("INSERT INTO author (name) VALUES ('Foo')")
10 | self.assertEqual(
11 | await conn.execute_query_dict("SELECT name FROM author"), [{"name": "Foo"}]
12 | )
13 |
14 | async def test_in_transaction(self):
15 | async with in_transaction() as conn:
16 | await conn.execute_query("INSERT INTO author (name) VALUES ('Foo')")
17 |
18 | conn = connections.get("models")
19 | self.assertEqual(
20 | await conn.execute_query_dict("SELECT name FROM author"), [{"name": "Foo"}]
21 | )
22 |
23 | @test.requireCapability(supports_transactions=True)
24 | async def test_in_transaction_exception(self):
25 | try:
26 | async with in_transaction() as conn:
27 | await conn.execute_query("INSERT INTO author (name) VALUES ('Foo')")
28 | raise ValueError("oops")
29 | except ValueError:
30 | pass
31 |
32 | conn = connections.get("models")
33 | self.assertEqual(await conn.execute_query_dict("SELECT name FROM author"), [])
34 |
35 | @test.requireCapability(supports_transactions=True)
36 | async def test_in_transaction_rollback(self):
37 | async with in_transaction() as conn:
38 | await conn.execute_query("INSERT INTO author (name) VALUES ('Foo')")
39 | await conn.rollback()
40 |
41 | conn = connections.get("models")
42 | self.assertEqual(await conn.execute_query_dict("SELECT name FROM author"), [])
43 |
44 | async def test_in_transaction_commit(self):
45 | try:
46 | async with in_transaction() as conn:
47 | await conn.execute_query("INSERT INTO author (name) VALUES ('Foo')")
48 | await conn.commit()
49 | raise ValueError("oops")
50 | except ValueError:
51 | pass
52 |
53 | conn = connections.get("models")
54 | self.assertEqual(
55 | await conn.execute_query_dict("SELECT name FROM author"), [{"name": "Foo"}]
56 | )
57 |
--------------------------------------------------------------------------------
/tests/test_order_by_nested.py:
--------------------------------------------------------------------------------
1 | from tests.testmodels import Event, Tournament
2 | from tortoise.contrib import test
3 | from tortoise.contrib.test.condition import NotEQ
4 |
5 |
6 | class TestOrderByNested(test.TestCase):
7 | @test.requireCapability(dialect=NotEQ("oracle"))
8 | async def test_basic(self):
9 | await Event.create(
10 | name="Event 1", tournament=await Tournament.create(name="Tournament 1", desc="B")
11 | )
12 | await Event.create(
13 | name="Event 2", tournament=await Tournament.create(name="Tournament 2", desc="A")
14 | )
15 |
16 | self.assertEqual(
17 | await Event.all().order_by("-name").values("name"),
18 | [{"name": "Event 2"}, {"name": "Event 1"}],
19 | )
20 |
21 | self.assertEqual(
22 | await Event.all().prefetch_related("tournament").values("tournament__desc"),
23 | [{"tournament__desc": "B"}, {"tournament__desc": "A"}],
24 | )
25 |
26 | self.assertEqual(
27 | await Event.all()
28 | .prefetch_related("tournament")
29 | .order_by("tournament__desc")
30 | .values("tournament__desc"),
31 | [{"tournament__desc": "A"}, {"tournament__desc": "B"}],
32 | )
33 |
--------------------------------------------------------------------------------
/tests/test_relations_with_unique.py:
--------------------------------------------------------------------------------
1 | from tests.testmodels import Principal, School, Student
2 | from tortoise.contrib import test
3 | from tortoise.query_utils import Prefetch
4 |
5 |
6 | class TestRelationsWithUnique(test.TestCase):
7 | async def test_relation_with_unique(self):
8 | school1 = await School.create(id=1024, name="School1")
9 | student1 = await Student.create(name="Sang-Heon Jeon1", school_id=school1.id)
10 |
11 | student_schools = await Student.filter(name="Sang-Heon Jeon1").values(
12 | "name", "school__name"
13 | )
14 | self.assertEqual(student_schools[0], {"name": "Sang-Heon Jeon1", "school__name": "School1"})
15 | student_schools = await Student.all().values(school="school__name")
16 | self.assertEqual(student_schools[0]["school"], school1.name)
17 | student_schools = await Student.all().values_list("school__name")
18 | self.assertEqual(student_schools[0][0], school1.name)
19 |
20 | await Student.create(name="Sang-Heon Jeon2", school=school1)
21 | school_with_filtered = (
22 | await School.all()
23 | .prefetch_related(Prefetch("students", queryset=Student.filter(name="Sang-Heon Jeon1")))
24 | .first()
25 | )
26 | school_without_filtered = await School.first().prefetch_related("students")
27 | self.assertEqual(len(school_with_filtered.students), 1)
28 | self.assertEqual(len(school_without_filtered.students), 2)
29 |
30 | student_direct_prefetch = await Student.first().prefetch_related("school")
31 | self.assertEqual(student_direct_prefetch.school.id, school1.id)
32 |
33 | school2 = await School.create(id=2048, name="School2")
34 | await Student.all().update(school=school2)
35 | student = await Student.first()
36 | self.assertEqual(student.school_id, school2.id)
37 |
38 | await Student.filter(id=student1.id).update(school=school1)
39 | schools = await School.all().order_by("students__name")
40 | self.assertEqual([school.name for school in schools], ["School1", "School2"])
41 | schools = await School.all().order_by("-students__name")
42 | self.assertEqual([school.name for school in schools], ["School2", "School1"])
43 |
44 | fetched_principal = await Principal.create(name="Sang-Heon Jeon3", school=school1)
45 | self.assertEqual(fetched_principal.name, "Sang-Heon Jeon3")
46 | fetched_school = await School.filter(name="School1").prefetch_related("principal").first()
47 | self.assertEqual(fetched_school.name, "School1")
48 |
--------------------------------------------------------------------------------
/tests/test_table_name.py:
--------------------------------------------------------------------------------
1 | from tortoise import Tortoise, fields
2 | from tortoise.contrib.test import SimpleTestCase
3 | from tortoise.models import Model
4 |
5 |
6 | def table_name_generator(model_cls: type[Model]):
7 | return f"test_{model_cls.__name__.lower()}"
8 |
9 |
10 | class Tournament(Model):
11 | id = fields.IntField(pk=True)
12 | name = fields.TextField()
13 | created_at = fields.DatetimeField(auto_now_add=True)
14 |
15 |
16 | class CustomTable(Model):
17 | id = fields.IntField(pk=True)
18 | name = fields.TextField()
19 |
20 | class Meta:
21 | table = "my_custom_table"
22 |
23 |
24 | class TestTableNameGenerator(SimpleTestCase):
25 | async def asyncSetUp(self):
26 | await super().asyncSetUp()
27 | await Tortoise.init(
28 | db_url="sqlite://:memory:",
29 | modules={"models": [__name__]},
30 | table_name_generator=table_name_generator,
31 | )
32 | await Tortoise.generate_schemas()
33 |
34 | async def test_glabal_name_generator(self):
35 | self.assertEqual(Tournament._meta.db_table, "test_tournament")
36 |
37 | async def test_custom_table_name_precedence(self):
38 | self.assertEqual(CustomTable._meta.db_table, "my_custom_table")
39 |
--------------------------------------------------------------------------------
/tests/test_unique_together.py:
--------------------------------------------------------------------------------
1 | from tests.testmodels import (
2 | Tournament,
3 | UniqueTogetherFields,
4 | UniqueTogetherFieldsWithFK,
5 | )
6 | from tortoise.contrib import test
7 | from tortoise.exceptions import IntegrityError
8 |
9 |
10 | class TestUniqueTogether(test.TestCase):
11 | async def test_unique_together(self):
12 | first_name = "first_name"
13 | last_name = "last_name"
14 |
15 | await UniqueTogetherFields.create(first_name=first_name, last_name=last_name)
16 |
17 | with self.assertRaises(IntegrityError):
18 | await UniqueTogetherFields.create(first_name=first_name, last_name=last_name)
19 |
20 | async def test_unique_together_with_foreign_keys(self):
21 | tournament_name = "tournament_name"
22 | text = "text"
23 |
24 | tournament = await Tournament.create(name=tournament_name)
25 |
26 | await UniqueTogetherFieldsWithFK.create(text=text, tournament=tournament)
27 |
28 | with self.assertRaises(IntegrityError):
29 | await UniqueTogetherFieldsWithFK.create(text=text, tournament=tournament)
30 |
--------------------------------------------------------------------------------
/tests/test_version.py:
--------------------------------------------------------------------------------
1 | import subprocess # nosec
2 | import sys
3 | from pathlib import Path
4 |
5 | from tortoise import __version__
6 |
7 | if sys.version_info >= (3, 11):
8 | import tomllib
9 | from contextlib import chdir
10 | else:
11 | import contextlib
12 | import os
13 |
14 | import tomli as tomllib
15 |
16 | class chdir(contextlib.AbstractContextManager): # Copied from source code of Python3.13
17 | """Non thread-safe context manager to change the current working directory."""
18 |
19 | def __init__(self, path) -> None:
20 | self.path = path
21 | self._old_cwd: list[str] = []
22 |
23 | def __enter__(self) -> None:
24 | self._old_cwd.append(os.getcwd())
25 | os.chdir(self.path)
26 |
27 | def __exit__(self, *excinfo) -> None:
28 | os.chdir(self._old_cwd.pop())
29 |
30 |
31 | def _read_version():
32 | text = Path("pyproject.toml").read_text()
33 | data = tomllib.loads(text)
34 | return data["project"]["version"]
35 |
36 |
37 | def test_version():
38 | assert _read_version() == __version__
39 |
40 |
41 | def test_added_by_poetry_v2(tmp_path: Path):
42 | tortoise_orm = Path(__file__).parent.resolve().parent
43 | with chdir(tmp_path):
44 | package = "foo"
45 | subprocess.run(["poetry", "new", package]) # nosec
46 | with chdir(package):
47 | r = subprocess.run(["poetry", "add", tortoise_orm]) # nosec
48 | assert r.returncode == 0
49 |
--------------------------------------------------------------------------------
/tests/testmodels_postgres.py:
--------------------------------------------------------------------------------
1 | from tortoise import Model, fields
2 | from tortoise.contrib.postgres.fields import ArrayField
3 |
4 |
5 | class ArrayFields(Model):
6 | id = fields.IntField(primary_key=True)
7 | array = ArrayField()
8 | array_null = ArrayField(null=True)
9 | array_str = ArrayField(element_type="varchar(1)", null=True)
10 | array_smallint = ArrayField(element_type="smallint", null=True)
11 |
--------------------------------------------------------------------------------
/tests/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tests/utils/__init__.py
--------------------------------------------------------------------------------
/tests/utils/test_run_async.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest import skipIf
3 |
4 | from tortoise import Tortoise, connections, run_async
5 | from tortoise.contrib.test import SimpleTestCase
6 |
7 |
8 | @skipIf(os.name == "nt", "stuck with Windows")
9 | class TestRunAsync(SimpleTestCase):
10 | def setUp(self):
11 | self.somevalue = 1
12 |
13 | def tearDown(self):
14 | run_async(self.asyncTearDown())
15 |
16 | async def init(self):
17 | await Tortoise.init(db_url="sqlite://:memory:", modules={"models": []})
18 | self.somevalue = 2
19 | self.assertNotEqual(connections._get_storage(), {})
20 |
21 | async def init_raise(self):
22 | await Tortoise.init(db_url="sqlite://:memory:", modules={"models": []})
23 | self.somevalue = 3
24 | self.assertNotEqual(connections._get_storage(), {})
25 | raise Exception("Some exception")
26 |
27 | def test_run_async(self):
28 | self.assertEqual(connections._get_storage(), {})
29 | self.assertEqual(self.somevalue, 1)
30 | run_async(self.init())
31 | self.assertEqual(connections._get_storage(), {})
32 | self.assertEqual(self.somevalue, 2)
33 |
34 | def test_run_async_raised(self):
35 | self.assertEqual(connections._get_storage(), {})
36 | self.assertEqual(self.somevalue, 1)
37 | with self.assertRaises(Exception):
38 | run_async(self.init_raise())
39 | self.assertEqual(connections._get_storage(), {})
40 | self.assertEqual(self.somevalue, 3)
41 |
--------------------------------------------------------------------------------
/tortoise/backends/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tortoise/backends/__init__.py
--------------------------------------------------------------------------------
/tortoise/backends/asyncpg/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import AsyncpgDBClient
2 |
3 | client_class = AsyncpgDBClient
4 |
--------------------------------------------------------------------------------
/tortoise/backends/asyncpg/executor.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import asyncpg
4 |
5 | from tortoise import Model
6 | from tortoise.backends.base_postgres.executor import BasePostgresExecutor
7 |
8 |
9 | class AsyncpgExecutor(BasePostgresExecutor):
10 | async def _process_insert_result(self, instance: Model, results: asyncpg.Record | None) -> None:
11 | return await super()._process_insert_result(instance, results)
12 |
--------------------------------------------------------------------------------
/tortoise/backends/asyncpg/schema_generator.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | from tortoise.backends.base_postgres.schema_generator import BasePostgresSchemaGenerator
6 |
7 | if TYPE_CHECKING: # pragma: nocoverage
8 | from tortoise.backends.asyncpg.client import AsyncpgDBClient
9 |
10 |
11 | class AsyncpgSchemaGenerator(BasePostgresSchemaGenerator):
12 | def __init__(self, client: AsyncpgDBClient) -> None:
13 | super().__init__(client)
14 |
--------------------------------------------------------------------------------
/tortoise/backends/base/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tortoise/backends/base/__init__.py
--------------------------------------------------------------------------------
/tortoise/backends/base_postgres/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tortoise/backends/base_postgres/__init__.py
--------------------------------------------------------------------------------
/tortoise/backends/mssql/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import MSSQLClient
2 |
3 | client_class = MSSQLClient
4 |
--------------------------------------------------------------------------------
/tortoise/backends/mssql/executor.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any
4 |
5 | from tortoise.backends.odbc.executor import ODBCExecutor
6 | from tortoise.exceptions import UnSupportedError
7 |
8 |
9 | class MSSQLExecutor(ODBCExecutor):
10 | async def execute_explain(self, sql: str) -> Any:
11 | raise UnSupportedError("MSSQL does not support explain")
12 |
--------------------------------------------------------------------------------
/tortoise/backends/mysql/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import MySQLClient
2 |
3 | client_class = MySQLClient
4 |
--------------------------------------------------------------------------------
/tortoise/backends/odbc/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tortoise/backends/odbc/__init__.py
--------------------------------------------------------------------------------
/tortoise/backends/odbc/executor.py:
--------------------------------------------------------------------------------
1 | from tortoise import Model
2 | from tortoise.backends.base.executor import BaseExecutor
3 | from tortoise.fields import BigIntField, IntField, SmallIntField
4 |
5 |
6 | class ODBCExecutor(BaseExecutor):
7 | async def _process_insert_result(self, instance: Model, results: int) -> None:
8 | pk_field_object = self.model._meta.pk
9 | if (
10 | isinstance(pk_field_object, (SmallIntField, IntField, BigIntField))
11 | and pk_field_object.generated
12 | ):
13 | instance.pk = results
14 |
--------------------------------------------------------------------------------
/tortoise/backends/oracle/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import OracleClient
2 |
3 | client_class = OracleClient
4 |
--------------------------------------------------------------------------------
/tortoise/backends/oracle/executor.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING, cast
4 |
5 | from tortoise import Model
6 | from tortoise.backends.odbc.executor import ODBCExecutor
7 |
8 | if TYPE_CHECKING:
9 | from .client import OracleClient # pylint: disable=W0611
10 |
11 |
12 | class OracleExecutor(ODBCExecutor):
13 | async def _process_insert_result(self, instance: Model, results: int) -> None:
14 | sql = "SELECT SEQUENCE_NAME FROM ALL_TAB_IDENTITY_COLS where TABLE_NAME = ? and OWNER = ?"
15 | db = cast("OracleClient", self.db)
16 | ret = await db.execute_query_dict(sql, values=[instance._meta.db_table, db.database])
17 | try:
18 | seq = ret[0]["SEQUENCE_NAME"]
19 | except IndexError:
20 | return
21 | sql = f"SELECT {seq}.CURRVAL FROM DUAL" # nosec:B608
22 | ret = await db.execute_query_dict(sql)
23 | await super()._process_insert_result(instance, ret[0]["CURRVAL"])
24 |
--------------------------------------------------------------------------------
/tortoise/backends/psycopg/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import PsycopgClient
2 |
3 | client_class = PsycopgClient
4 |
--------------------------------------------------------------------------------
/tortoise/backends/psycopg/executor.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pypika_tortoise import Parameter
4 |
5 | from tortoise import Model
6 | from tortoise.backends.base_postgres.executor import BasePostgresExecutor
7 |
8 |
9 | class PsycopgExecutor(BasePostgresExecutor):
10 | async def _process_insert_result(self, instance: Model, results: dict | tuple | None) -> None:
11 | if results:
12 | db_projection = instance._meta.fields_db_projection_reverse
13 |
14 | if isinstance(results, dict):
15 | for key, val in results.items():
16 | setattr(instance, db_projection[key], val)
17 | else:
18 | generated_fields = self.model._meta.generated_db_fields
19 |
20 | for key, val in zip(generated_fields, results):
21 | setattr(instance, db_projection[key], val)
22 |
23 | def parameter(self, pos: int) -> Parameter:
24 | return Parameter("%s")
25 |
--------------------------------------------------------------------------------
/tortoise/backends/psycopg/schema_generator.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING
4 |
5 | from tortoise.backends.base_postgres.schema_generator import BasePostgresSchemaGenerator
6 |
7 | if TYPE_CHECKING: # pragma: nocoverage
8 | from tortoise.backends.psycopg.client import PsycopgClient
9 |
10 |
11 | class PsycopgSchemaGenerator(BasePostgresSchemaGenerator):
12 | def __init__(self, client: PsycopgClient) -> None:
13 | super().__init__(client)
14 |
--------------------------------------------------------------------------------
/tortoise/backends/sqlite/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import SqliteClient, SqliteClientWithRegexpSupport
2 |
3 | client_class = SqliteClient
4 |
5 |
6 | def get_client_class(db_info: dict):
7 | if db_info.get("credentials", {}).get("install_regexp_functions"):
8 | return SqliteClientWithRegexpSupport
9 | else:
10 | return SqliteClient
11 |
--------------------------------------------------------------------------------
/tortoise/backends/sqlite/executor.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import sqlite3
3 | from decimal import Decimal
4 |
5 | from tortoise import Model
6 | from tortoise.backends.base.executor import BaseExecutor
7 | from tortoise.contrib.sqlite.regex import (
8 | insensitive_posix_sqlite_regexp,
9 | posix_sqlite_regexp,
10 | )
11 | from tortoise.fields import BigIntField, IntField, SmallIntField
12 | from tortoise.filters import insensitive_posix_regex, posix_regex
13 |
14 | # Conversion for the cases where it's hard to know the
15 | # related field, e.g. in raw queries, math or annotations.
16 | sqlite3.register_adapter(Decimal, str)
17 | sqlite3.register_adapter(datetime.date, lambda val: val.isoformat())
18 | sqlite3.register_adapter(datetime.datetime, lambda val: val.isoformat(" "))
19 |
20 |
21 | class SqliteExecutor(BaseExecutor):
22 | EXPLAIN_PREFIX = "EXPLAIN QUERY PLAN"
23 | DB_NATIVE = {bytes, str, int, float}
24 | FILTER_FUNC_OVERRIDE = {
25 | posix_regex: posix_sqlite_regexp,
26 | insensitive_posix_regex: insensitive_posix_sqlite_regexp,
27 | }
28 |
29 | async def _process_insert_result(self, instance: Model, results: int) -> None:
30 | pk_field_object = self.model._meta.pk
31 | if (
32 | isinstance(pk_field_object, (SmallIntField, IntField, BigIntField))
33 | and pk_field_object.generated
34 | ):
35 | instance.pk = results
36 |
37 | # SQLite can only generate a single ROWID
38 | # so if any other primary key, it won't generate what we want.
39 |
--------------------------------------------------------------------------------
/tortoise/backends/sqlite/schema_generator.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any
4 |
5 | from tortoise.backends.base.schema_generator import BaseSchemaGenerator
6 | from tortoise.converters import encoders
7 |
8 |
9 | class SqliteSchemaGenerator(BaseSchemaGenerator):
10 | DIALECT = "sqlite"
11 |
12 | @classmethod
13 | def _get_escape_translation_table(cls) -> list[str]:
14 | table = super()._get_escape_translation_table()
15 | table[ord('"')] = '"'
16 | table[ord("'")] = "'"
17 | table[ord("/")] = "\\/"
18 | return table
19 |
20 | def _table_comment_generator(self, table: str, comment: str) -> str:
21 | return f" /* {self._escape_comment(comment)} */"
22 |
23 | def _column_comment_generator(self, table: str, column: str, comment: str) -> str:
24 | return f" /* {self._escape_comment(comment)} */"
25 |
26 | def _column_default_generator(
27 | self,
28 | table: str,
29 | column: str,
30 | default: Any,
31 | auto_now_add: bool = False,
32 | auto_now: bool = False,
33 | ) -> str:
34 | default_str = " DEFAULT"
35 | default_str += " CURRENT_TIMESTAMP" if auto_now_add else f" {default}"
36 | return default_str
37 |
38 | def _escape_default_value(self, default: Any):
39 | return encoders.get(type(default))(default) # type: ignore
40 |
--------------------------------------------------------------------------------
/tortoise/contrib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tortoise/contrib/__init__.py
--------------------------------------------------------------------------------
/tortoise/contrib/mysql/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tortoise/contrib/mysql/__init__.py
--------------------------------------------------------------------------------
/tortoise/contrib/mysql/fields.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import TYPE_CHECKING, Any
4 | from uuid import UUID, uuid4
5 |
6 | from tortoise.fields import Field
7 | from tortoise.fields import UUIDField as UUIDFieldBase
8 |
9 | if TYPE_CHECKING: # pragma: nocoverage
10 | from tortoise.models import Model
11 |
12 |
13 | class GeometryField(Field):
14 | SQL_TYPE = "GEOMETRY"
15 |
16 |
17 | class UUIDField(UUIDFieldBase):
18 | """
19 | UUID Field
20 |
21 | This field can store uuid value, but with the option to add binary compression.
22 |
23 | If used as a primary key, it will auto-generate a UUID4 by default.
24 |
25 | ``binary_compression`` (bool):
26 | If True, the UUID will be stored in binary format.
27 | This will save 6 bytes per UUID in the database.
28 | Note: that this is a MySQL-only feature.
29 | See https://dev.mysql.com/blog-archive/mysql-8-0-uuid-support/ for more details.
30 | """
31 |
32 | SQL_TYPE = "CHAR(36)"
33 |
34 | def __init__(self, binary_compression: bool = True, **kwargs: Any) -> None:
35 | if (kwargs.get("primary_key") or kwargs.get("pk", False)) and "default" not in kwargs:
36 | kwargs["default"] = uuid4
37 | super().__init__(**kwargs)
38 |
39 | if binary_compression:
40 | self.SQL_TYPE = "BINARY(16)"
41 | self._binary_compression = binary_compression
42 |
43 | def to_db_value(self, value: Any, instance: type[Model] | Model) -> str | bytes | None: # type: ignore
44 | # Make sure that value is a UUIDv4
45 | # If not, raise an error
46 | # This is to prevent UUIDv1 or any other version from being stored in the database
47 | if self._binary_compression:
48 | if not isinstance(value, UUID):
49 | raise ValueError("UUIDField only accepts UUID values")
50 | return value.bytes
51 | return value and str(value)
52 |
53 | def to_python_value(self, value: Any) -> UUID | None:
54 | if value is None or isinstance(value, UUID):
55 | return value
56 | elif self._binary_compression and isinstance(value, bytes):
57 | return UUID(bytes=value)
58 | else:
59 | return UUID(value)
60 |
--------------------------------------------------------------------------------
/tortoise/contrib/mysql/functions.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pypika_tortoise.terms import Function
4 |
5 |
6 | class Rand(Function):
7 | """
8 | Generate random number, with optional seed.
9 |
10 | :samp:`Rand()`
11 | """
12 |
13 | def __init__(self, seed: int | None = None, alias=None) -> None:
14 | super().__init__("RAND", seed, alias=alias)
15 | self.args = [self.wrap_constant(seed)] if seed is not None else []
16 |
--------------------------------------------------------------------------------
/tortoise/contrib/mysql/indexes.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pypika_tortoise.terms import Term
4 |
5 | from tortoise.indexes import Index
6 |
7 |
8 | class FullTextIndex(Index):
9 | INDEX_TYPE = "FULLTEXT"
10 |
11 | def __init__(
12 | self,
13 | *expressions: Term,
14 | fields: tuple[str, ...] | None = None,
15 | name: str | None = None,
16 | parser_name: str | None = None,
17 | ) -> None:
18 | super().__init__(*expressions, fields=fields, name=name)
19 | if parser_name:
20 | self.extra = f" WITH PARSER {parser_name}"
21 |
22 |
23 | class SpatialIndex(Index):
24 | INDEX_TYPE = "SPATIAL"
25 |
--------------------------------------------------------------------------------
/tortoise/contrib/mysql/search.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from enum import Enum
4 | from typing import Any
5 |
6 | from pypika_tortoise import SqlContext
7 | from pypika_tortoise.enums import Comparator
8 | from pypika_tortoise.terms import BasicCriterion
9 | from pypika_tortoise.terms import Function as PypikaFunction
10 | from pypika_tortoise.terms import Term
11 |
12 |
13 | class Comp(Comparator):
14 | search = " "
15 |
16 |
17 | class Mode(Enum):
18 | NATURAL_LANGUAGE_MODE = "IN NATURAL LANGUAGE MODE"
19 | NATURAL_LANGUAGE_MODE_WITH_QUERY_EXPRESSION = "IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION"
20 | BOOL_MODE = "IN BOOLEAN MODE"
21 | WITH_QUERY_EXPRESSION = "WITH QUERY EXPANSION"
22 |
23 |
24 | class Match(PypikaFunction):
25 | def __init__(self, *columns: Term) -> None:
26 | super().__init__("MATCH", *columns)
27 |
28 |
29 | class Against(PypikaFunction):
30 | def __init__(self, expr: Term, mode: Mode | None = None) -> None:
31 | super().__init__("AGAINST", expr)
32 | self.mode = mode
33 |
34 | def get_special_params_sql(self, ctx: SqlContext) -> Any:
35 | if not self.mode:
36 | return ""
37 | return self.mode.value
38 |
39 |
40 | class SearchCriterion(BasicCriterion):
41 | """
42 | Only support for CharField, TextField with full search indexes.
43 | """
44 |
45 | def __init__(self, *columns: Term, expr: Term, mode: Mode | None = None) -> None:
46 | super().__init__(Comp.search, Match(*columns), Against(expr, mode))
47 |
--------------------------------------------------------------------------------
/tortoise/contrib/postgres/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tortoise/contrib/postgres/__init__.py
--------------------------------------------------------------------------------
/tortoise/contrib/postgres/array_functions.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | from pypika_tortoise.terms import BasicCriterion, Criterion, Function, Term
4 |
5 |
6 | class PostgresArrayOperators(str, Enum):
7 | CONTAINS = "@>"
8 | CONTAINED_BY = "<@"
9 | OVERLAP = "&&"
10 |
11 |
12 | # The value in the functions below is casted to the exact type of the field with value_encoder
13 | # to avoid issues with psycopg that tries to use the smallest possible type which can lead to errors,
14 | # e.g. {1,2} will be casted to smallint[] instead of integer[].
15 |
16 |
17 | def postgres_array_contains(field: Term, value: Term) -> Criterion:
18 | return BasicCriterion(PostgresArrayOperators.CONTAINS, field, value)
19 |
20 |
21 | def postgres_array_contained_by(field: Term, value: Term) -> Criterion:
22 | return BasicCriterion(PostgresArrayOperators.CONTAINED_BY, field, value)
23 |
24 |
25 | def postgres_array_overlap(field: Term, value: Term) -> Criterion:
26 | return BasicCriterion(PostgresArrayOperators.OVERLAP, field, value)
27 |
28 |
29 | def postgres_array_length(field: Term, value: int) -> Criterion:
30 | """Returns a criterion that checks if array length equals the given value"""
31 | return Function("array_length", field, 1).eq(value)
32 |
--------------------------------------------------------------------------------
/tortoise/contrib/postgres/fields.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any
4 |
5 | from tortoise.fields import Field
6 |
7 |
8 | class TSVectorField(Field):
9 | SQL_TYPE = "TSVECTOR"
10 |
11 |
12 | class ArrayField(Field, list): # type: ignore
13 | def __init__(self, element_type: str = "int", **kwargs: Any):
14 | super().__init__(**kwargs)
15 | self.element_type = element_type.upper()
16 |
17 | @property
18 | def SQL_TYPE(self) -> str: # type: ignore
19 | return f"{self.element_type}[]"
20 |
--------------------------------------------------------------------------------
/tortoise/contrib/postgres/functions.py:
--------------------------------------------------------------------------------
1 | from pypika_tortoise.terms import Function, Term
2 |
3 |
4 | class ToTsVector(Function):
5 | """
6 | to to_tsvector function
7 | """
8 |
9 | def __init__(self, field: Term) -> None:
10 | super().__init__("TO_TSVECTOR", field)
11 |
12 |
13 | class ToTsQuery(Function):
14 | """
15 | to_tsquery function
16 | """
17 |
18 | def __init__(self, field: Term) -> None:
19 | super().__init__("TO_TSQUERY", field)
20 |
21 |
22 | class PlainToTsQuery(Function):
23 | """
24 | plainto_tsquery function
25 | """
26 |
27 | def __init__(self, field: Term) -> None:
28 | super().__init__("PLAINTO_TSQUERY", field)
29 |
30 |
31 | class Random(Function):
32 | """
33 | Generate random number.
34 |
35 | :samp:`Random()`
36 | """
37 |
38 | def __init__(self, alias=None) -> None:
39 | super().__init__("RANDOM", alias=alias)
40 |
--------------------------------------------------------------------------------
/tortoise/contrib/postgres/indexes.py:
--------------------------------------------------------------------------------
1 | from tortoise.indexes import PartialIndex
2 |
3 |
4 | class PostgreSQLIndex(PartialIndex):
5 | pass
6 |
7 |
8 | class BloomIndex(PostgreSQLIndex):
9 | INDEX_TYPE = "BLOOM"
10 |
11 |
12 | class BrinIndex(PostgreSQLIndex):
13 | INDEX_TYPE = "BRIN"
14 |
15 |
16 | class GinIndex(PostgreSQLIndex):
17 | INDEX_TYPE = "GIN"
18 |
19 |
20 | class GistIndex(PostgreSQLIndex):
21 | INDEX_TYPE = "GIST"
22 |
23 |
24 | class HashIndex(PostgreSQLIndex):
25 | INDEX_TYPE = "HASH"
26 |
27 |
28 | class SpGistIndex(PostgreSQLIndex):
29 | INDEX_TYPE = "SPGIST"
30 |
--------------------------------------------------------------------------------
/tortoise/contrib/postgres/regex.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import enum
4 | from typing import cast
5 |
6 | from pypika_tortoise.enums import SqlTypes
7 | from pypika_tortoise.functions import Cast, Coalesce
8 | from pypika_tortoise.terms import BasicCriterion, Term
9 |
10 |
11 | class PostgresRegexMatching(enum.Enum):
12 | POSIX_REGEX = " ~ "
13 | IPOSIX_REGEX = " ~* "
14 |
15 |
16 | def postgres_posix_regex(field: Term, value: str):
17 | term = cast(Term, field.wrap_constant(value))
18 | return BasicCriterion(
19 | PostgresRegexMatching.POSIX_REGEX, Coalesce(Cast(field, SqlTypes.VARCHAR), ""), term
20 | )
21 |
22 |
23 | def postgres_insensitive_posix_regex(field: Term, value: str):
24 | term = cast(Term, field.wrap_constant(value))
25 | return BasicCriterion(
26 | PostgresRegexMatching.IPOSIX_REGEX, Coalesce(Cast(field, SqlTypes.VARCHAR), ""), term
27 | )
28 |
--------------------------------------------------------------------------------
/tortoise/contrib/postgres/search.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pypika_tortoise.enums import Comparator
4 | from pypika_tortoise.terms import BasicCriterion, Function, Term
5 |
6 | from tortoise.contrib.postgres.functions import ToTsQuery, ToTsVector
7 |
8 |
9 | class Comp(Comparator):
10 | search = " @@ "
11 |
12 |
13 | class SearchCriterion(BasicCriterion):
14 | def __init__(self, field: Term, expr: Term | Function) -> None:
15 | if isinstance(expr, Function):
16 | _expr = expr
17 | else:
18 | _expr = ToTsQuery(expr)
19 | super().__init__(Comp.search, ToTsVector(field), _expr)
20 |
--------------------------------------------------------------------------------
/tortoise/contrib/pydantic/__init__.py:
--------------------------------------------------------------------------------
1 | from tortoise.contrib.pydantic.base import PydanticListModel, PydanticModel
2 | from tortoise.contrib.pydantic.creator import (
3 | pydantic_model_creator,
4 | pydantic_queryset_creator,
5 | )
6 |
7 | __all__ = (
8 | "PydanticListModel",
9 | "PydanticModel",
10 | "pydantic_model_creator",
11 | "pydantic_queryset_creator",
12 | )
13 |
--------------------------------------------------------------------------------
/tortoise/contrib/pydantic/utils.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Callable
4 | from typing import TYPE_CHECKING, Any, get_type_hints
5 |
6 | if TYPE_CHECKING: # pragma: nocoverage
7 | from tortoise.models import Model
8 |
9 |
10 | def get_annotations(cls: type[Model], method: Callable | None = None) -> dict[str, Any]:
11 | """
12 | Get all annotations including base classes
13 | :param cls: The model class we need annotations from
14 | :param method: If specified, we try to get the annotations for the callable
15 | :return: The list of annotations
16 | """
17 | return get_type_hints(method or cls)
18 |
--------------------------------------------------------------------------------
/tortoise/contrib/sqlite/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tortoise/contrib/sqlite/__init__.py
--------------------------------------------------------------------------------
/tortoise/contrib/sqlite/functions.py:
--------------------------------------------------------------------------------
1 | from pypika_tortoise.terms import Function
2 |
3 |
4 | class Random(Function):
5 | """
6 | Generate random number.
7 |
8 | :samp:`Random()`
9 | """
10 |
11 | def __init__(self, alias=None) -> None:
12 | super().__init__("RANDOM", alias=alias)
13 |
--------------------------------------------------------------------------------
/tortoise/contrib/sqlite/regex.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import enum
4 | import re
5 | from typing import cast
6 |
7 | import aiosqlite
8 | from pypika_tortoise.enums import SqlTypes
9 | from pypika_tortoise.functions import Cast, Coalesce
10 | from pypika_tortoise.terms import BasicCriterion, Term
11 |
12 |
13 | class SQLiteRegexMatching(enum.Enum):
14 | POSIX_REGEX = " REGEXP "
15 | IPOSIX_REGEX = " MATCH "
16 |
17 |
18 | def posix_sqlite_regexp(field: Term, value: str):
19 | term = cast(Term, field.wrap_constant(value))
20 | return BasicCriterion(
21 | SQLiteRegexMatching.POSIX_REGEX, Coalesce(Cast(field, SqlTypes.VARCHAR), ""), term
22 | )
23 |
24 |
25 | def insensitive_posix_sqlite_regexp(field: Term, value: str):
26 | term = cast(Term, field.wrap_constant(value))
27 | return BasicCriterion(
28 | SQLiteRegexMatching.IPOSIX_REGEX, Coalesce(Cast(field, SqlTypes.VARCHAR), ""), term
29 | )
30 |
31 |
32 | async def install_regexp_functions(connection: aiosqlite.Connection):
33 | def regexp(expr, item):
34 | if not expr or not item:
35 | return False
36 | return re.search(expr, item) is not None
37 |
38 | def iregexp(expr, item):
39 | return re.search(expr, item, re.IGNORECASE) is not None
40 |
41 | await connection.create_function("regexp", 2, regexp)
42 | await connection.create_function("match", 2, iregexp)
43 |
--------------------------------------------------------------------------------
/tortoise/contrib/test/condition.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any
4 |
5 |
6 | class Condition:
7 | def __init__(self, value: Any) -> None:
8 | self.value = value
9 |
10 |
11 | class NotEQ(Condition):
12 | def __eq__(self, other: Any) -> bool:
13 | return self.value != other
14 |
15 | def __str__(self) -> str:
16 | return f""
17 |
18 |
19 | class In(Condition):
20 | def __init__(self, *args: Any) -> None:
21 | super().__init__(args)
22 |
23 | def __eq__(self, other: Any) -> bool:
24 | return other in self.value
25 |
26 | def __str__(self) -> str:
27 | return f""
28 |
29 |
30 | class NotIn(Condition):
31 | def __init__(self, *args: Any) -> None:
32 | super().__init__(args)
33 |
34 | def __eq__(self, other: Any) -> bool:
35 | return other not in self.value
36 |
37 | def __str__(self) -> str:
38 | return f""
39 |
--------------------------------------------------------------------------------
/tortoise/fields/__init__.py:
--------------------------------------------------------------------------------
1 | from tortoise.fields.base import (
2 | CASCADE,
3 | NO_ACTION,
4 | RESTRICT,
5 | SET_DEFAULT,
6 | SET_NULL,
7 | Field,
8 | OnDelete,
9 | )
10 | from tortoise.fields.data import (
11 | BigIntField,
12 | BinaryField,
13 | BooleanField,
14 | CharEnumField,
15 | CharField,
16 | DateField,
17 | DatetimeField,
18 | DecimalField,
19 | FloatField,
20 | IntEnumField,
21 | IntField,
22 | JSONField,
23 | SmallIntField,
24 | TextField,
25 | TimeDeltaField,
26 | TimeField,
27 | UUIDField,
28 | )
29 | from tortoise.fields.relational import (
30 | BackwardFKRelation,
31 | BackwardOneToOneRelation,
32 | ForeignKeyField,
33 | ForeignKeyNullableRelation,
34 | ForeignKeyRelation,
35 | ManyToManyField,
36 | ManyToManyRelation,
37 | OneToOneField,
38 | OneToOneNullableRelation,
39 | OneToOneRelation,
40 | ReverseRelation,
41 | )
42 |
43 | __all__ = [
44 | "CASCADE",
45 | "RESTRICT",
46 | "SET_DEFAULT",
47 | "SET_NULL",
48 | "NO_ACTION",
49 | "OnDelete",
50 | "Field",
51 | "BigIntField",
52 | "BinaryField",
53 | "BooleanField",
54 | "CharEnumField",
55 | "CharField",
56 | "DateField",
57 | "DatetimeField",
58 | "TimeField",
59 | "DecimalField",
60 | "FloatField",
61 | "IntEnumField",
62 | "IntField",
63 | "JSONField",
64 | "SmallIntField",
65 | "SmallIntField",
66 | "TextField",
67 | "TimeDeltaField",
68 | "UUIDField",
69 | "BackwardFKRelation",
70 | "BackwardOneToOneRelation",
71 | "ForeignKeyField",
72 | "ForeignKeyNullableRelation",
73 | "ForeignKeyRelation",
74 | "ManyToManyField",
75 | "ManyToManyRelation",
76 | "OneToOneField",
77 | "OneToOneNullableRelation",
78 | "OneToOneRelation",
79 | "ReverseRelation",
80 | ]
81 |
--------------------------------------------------------------------------------
/tortoise/log.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | logger = logging.getLogger("tortoise")
4 | db_client_logger = logging.getLogger("tortoise.db_client")
5 |
--------------------------------------------------------------------------------
/tortoise/manager.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any
4 |
5 | from tortoise.queryset import QuerySet
6 |
7 |
8 | class Manager:
9 | """
10 | A Manager is the interface through which database query operations are provided to tortoise models.
11 |
12 | There is one default Manager for every tortoise model.
13 | """
14 |
15 | def __init__(self, model=None) -> None:
16 | self._model = model
17 |
18 | def get_queryset(self) -> QuerySet:
19 | return QuerySet(self._model)
20 |
21 | def __getattr__(self, item: str) -> Any:
22 | return getattr(self.get_queryset(), item)
23 |
--------------------------------------------------------------------------------
/tortoise/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tortoise/tortoise-orm/65a61a29107e796c240f014fb7dc3baed3a7e601/tortoise/py.typed
--------------------------------------------------------------------------------
/tortoise/router.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Callable
4 | from typing import TYPE_CHECKING, Any
5 |
6 | from tortoise.connection import connections
7 | from tortoise.exceptions import ConfigurationError
8 |
9 | if TYPE_CHECKING:
10 | from tortoise import BaseDBAsyncClient, Model
11 |
12 |
13 | class ConnectionRouter:
14 | def __init__(self) -> None:
15 | self._routers: list[type] = None # type: ignore
16 |
17 | def init_routers(self, routers: list[Callable]) -> None:
18 | self._routers = [r() for r in routers]
19 |
20 | def _router_func(self, model: type[Model], action: str) -> Any:
21 | for r in self._routers:
22 | try:
23 | method = getattr(r, action)
24 | except AttributeError:
25 | # If the router doesn't have a method, skip to the next one.
26 | pass
27 | else:
28 | chosen_db = method(model)
29 | if chosen_db:
30 | return chosen_db
31 |
32 | def _db_route(self, model: type[Model], action: str) -> BaseDBAsyncClient | None:
33 | try:
34 | return connections.get(self._router_func(model, action))
35 | except ConfigurationError:
36 | return None
37 |
38 | def db_for_read(self, model: type[Model]) -> BaseDBAsyncClient | None:
39 | if not self._routers:
40 | return None
41 |
42 | return self._db_route(model, "db_for_read")
43 |
44 | def db_for_write(self, model: type[Model]) -> BaseDBAsyncClient | None:
45 | if not self._routers:
46 | return None
47 |
48 | return self._db_route(model, "db_for_write")
49 |
50 |
51 | router = ConnectionRouter()
52 |
--------------------------------------------------------------------------------
/tortoise/signals.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Callable
4 | from enum import Enum
5 | from typing import TypeVar
6 |
7 | T = TypeVar("T")
8 | FuncType = Callable[[T], T]
9 | Signals = Enum("Signals", ["pre_save", "post_save", "pre_delete", "post_delete"])
10 |
11 |
12 | def post_save(*senders) -> FuncType:
13 | """
14 | Register given models post_save signal.
15 |
16 | :param senders: Model class
17 | """
18 |
19 | def decorator(f: T) -> T:
20 | for sender in senders:
21 | sender.register_listener(Signals.post_save, f)
22 | return f
23 |
24 | return decorator
25 |
26 |
27 | def pre_save(*senders) -> FuncType:
28 | """
29 | Register given models pre_save signal.
30 |
31 | :param senders: Model class
32 | """
33 |
34 | def decorator(f: T) -> T:
35 | for sender in senders:
36 | sender.register_listener(Signals.pre_save, f)
37 | return f
38 |
39 | return decorator
40 |
41 |
42 | def pre_delete(*senders) -> FuncType:
43 | """
44 | Register given models pre_delete signal.
45 |
46 | :param senders: Model class
47 | """
48 |
49 | def decorator(f: T) -> T:
50 | for sender in senders:
51 | sender.register_listener(Signals.pre_delete, f)
52 | return f
53 |
54 | return decorator
55 |
56 |
57 | def post_delete(*senders) -> FuncType:
58 | """
59 | Register given models post_delete signal.
60 |
61 | :param senders: Model class
62 | """
63 |
64 | def decorator(f: T) -> T:
65 | for sender in senders:
66 | sender.register_listener(Signals.post_delete, f)
67 | return f
68 |
69 | return decorator
70 |
--------------------------------------------------------------------------------
/tortoise/transactions.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from collections.abc import Callable
4 | from functools import wraps
5 | from typing import TYPE_CHECKING, TypeVar, cast
6 |
7 | from tortoise import connections
8 | from tortoise.exceptions import ParamsError
9 |
10 | if TYPE_CHECKING: # pragma: nocoverage
11 | from tortoise.backends.base.client import BaseDBAsyncClient, TransactionContext
12 |
13 | T = TypeVar("T")
14 | FuncType = Callable[..., T]
15 | F = TypeVar("F", bound=FuncType)
16 |
17 |
18 | def _get_connection(connection_name: str | None) -> BaseDBAsyncClient:
19 | if connection_name:
20 | connection = connections.get(connection_name)
21 | elif len(connections.db_config) == 1:
22 | connection_name = next(iter(connections.db_config.keys()))
23 | connection = connections.get(connection_name)
24 | else:
25 | raise ParamsError(
26 | "You are running with multiple databases, so you should specify"
27 | f" connection_name: {list(connections.db_config)}"
28 | )
29 | return connection
30 |
31 |
32 | def in_transaction(connection_name: str | None = None) -> TransactionContext:
33 | """
34 | Transaction context manager.
35 |
36 | You can run your code inside ``async with in_transaction():`` statement to run it
37 | into one transaction. If error occurs transaction will rollback.
38 |
39 | :param connection_name: name of connection to run with, optional if you have only
40 | one db connection
41 | """
42 | connection = _get_connection(connection_name)
43 | return connection._in_transaction()
44 |
45 |
46 | def atomic(connection_name: str | None = None) -> Callable[[F], F]:
47 | """
48 | Transaction decorator.
49 |
50 | You can wrap your function with this decorator to run it into one transaction.
51 | If error occurs transaction will rollback.
52 |
53 | :param connection_name: name of connection to run with, optional if you have only
54 | one db connection
55 | """
56 |
57 | def wrapper(func: F) -> F:
58 | @wraps(func)
59 | async def wrapped(*args, **kwargs) -> T:
60 | async with in_transaction(connection_name):
61 | return await func(*args, **kwargs)
62 |
63 | return cast(F, wrapped)
64 |
65 | return wrapper
66 |
--------------------------------------------------------------------------------
/tortoise/utils.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from collections.abc import Iterable
5 | from typing import TYPE_CHECKING, Any
6 |
7 | from tortoise.log import logger
8 |
9 | if sys.version_info >= (3, 12):
10 | from itertools import batched
11 | else:
12 | from itertools import islice
13 |
14 | def batched(iterable: Iterable[Any], n: int) -> Iterable[tuple[Any]]:
15 | it = iter(iterable)
16 | while batch := tuple(islice(it, n)):
17 | yield batch
18 |
19 |
20 | if TYPE_CHECKING: # pragma: nocoverage
21 | from tortoise.backends.base.client import BaseDBAsyncClient
22 |
23 |
24 | def get_schema_sql(client: BaseDBAsyncClient, safe: bool) -> str:
25 | """
26 | Generates the SQL schema for the given client.
27 |
28 | :param client: The DB client to generate Schema SQL for
29 | :param safe: When set to true, creates the table only when it does not already exist.
30 | """
31 | generator = client.schema_generator(client)
32 | return generator.get_create_schema_sql(safe)
33 |
34 |
35 | async def generate_schema_for_client(client: BaseDBAsyncClient, safe: bool) -> None:
36 | """
37 | Generates and applies the SQL schema directly to the given client.
38 |
39 | :param client: The DB client to generate Schema SQL for
40 | :param safe: When set to true, creates the table only when it does not already exist.
41 | """
42 | generator = client.schema_generator(client)
43 | schema = get_schema_sql(client, safe)
44 | logger.debug("Creating schema: %s", schema)
45 | if schema: # pragma: nobranch
46 | await generator.generate_from_string(schema)
47 |
48 |
49 | def chunk(instances: Iterable[Any], batch_size: int | None = None) -> Iterable[Iterable[Any]]:
50 | """
51 | Generate iterable chunk by batch_size
52 | # noqa: DAR301
53 | """
54 | if not batch_size:
55 | yield instances
56 | else:
57 | yield from batched(instances, batch_size)
58 |
--------------------------------------------------------------------------------