├── .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 | --------------------------------------------------------------------------------