├── .codeclimate.yml ├── .codecov.yml ├── .coveragerc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── auto-merge-dependabot.yml │ ├── benchmark.yml │ ├── deploy-docs.yml │ ├── lint.yml │ ├── python-publish.yml │ ├── test-package.yml │ ├── test_docs.yml │ └── type-check.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE.md ├── Makefile ├── README.md ├── benchmarks ├── __init__.py ├── conftest.py ├── test_benchmark_aggregate.py ├── test_benchmark_bulk_create.py ├── test_benchmark_bulk_update.py ├── test_benchmark_create.py ├── test_benchmark_delete.py ├── test_benchmark_get.py ├── test_benchmark_init.py ├── test_benchmark_iterate.py ├── test_benchmark_save.py ├── test_benchmark_update.py └── test_benchmark_values.py ├── docs ├── contributing.md ├── fastapi │ ├── index.md │ ├── requests.md │ └── response.md ├── fields │ ├── common-parameters.md │ ├── encryption.md │ ├── field-types.md │ └── pydantic-fields.md ├── gen_ref_pages.py ├── index.md ├── install.md ├── migration.md ├── models │ ├── index.md │ ├── inheritance.md │ ├── internals.md │ ├── methods.md │ └── migrations.md ├── mypy.md ├── plugin.md ├── queries │ ├── aggregations.md │ ├── create.md │ ├── delete.md │ ├── filter-and-sort.md │ ├── index.md │ ├── joins-and-subqueries.md │ ├── pagination-and-rows-number.md │ ├── raw-data.md │ ├── read.md │ ├── select-columns.md │ └── update.md ├── relations │ ├── foreign-key.md │ ├── index.md │ ├── many-to-many.md │ ├── postponed-annotations.md │ └── queryset-proxy.md ├── releases.md ├── signals.md └── transactions.md ├── docs_src ├── __init__.py ├── aggregations │ ├── __init__.py │ └── docs001.py ├── fastapi │ ├── __init__.py │ ├── docs001.py │ └── mypy │ │ ├── __init__.py │ │ └── docs001.py ├── fields │ ├── __init__.py │ ├── docs001.py │ ├── docs002.py │ ├── docs003.py │ └── docs004.py ├── models │ ├── __init__.py │ ├── docs001.py │ ├── docs002.py │ ├── docs003.py │ ├── docs004.py │ ├── docs005.py │ ├── docs006.py │ ├── docs007.py │ ├── docs008.py │ ├── docs009.py │ ├── docs010.py │ ├── docs012.py │ ├── docs013.py │ ├── docs014.py │ ├── docs015.py │ ├── docs016.py │ ├── docs017.py │ └── docs018.py ├── queries │ ├── __init__.py │ ├── docs001.py │ ├── docs002.py │ ├── docs003.py │ ├── docs004.py │ ├── docs005.py │ ├── docs006.py │ ├── docs007.py │ ├── docs008.py │ └── docs009.py ├── relations │ ├── __init__.py │ ├── docs001.py │ ├── docs002.py │ ├── docs003.py │ └── docs004.py ├── select_columns │ ├── __init__.py │ └── docs001.py ├── signals │ ├── __init__.py │ └── docs002.py └── test_all_docs.py ├── examples ├── __init__.py ├── fastapi_quick_start.py ├── script_from_readme.py └── utils.py ├── mkdocs.yml ├── ormar ├── __init__.py ├── decorators │ ├── __init__.py │ └── signals.py ├── exceptions.py ├── fields │ ├── __init__.py │ ├── base.py │ ├── constraints.py │ ├── foreign_key.py │ ├── many_to_many.py │ ├── model_fields.py │ ├── parsers.py │ ├── referential_actions.py │ ├── sqlalchemy_encrypted.py │ ├── sqlalchemy_uuid.py │ └── through_field.py ├── models │ ├── __init__.py │ ├── descriptors │ │ ├── __init__.py │ │ └── descriptors.py │ ├── excludable.py │ ├── helpers │ │ ├── __init__.py │ │ ├── models.py │ │ ├── pydantic.py │ │ ├── related_names_validation.py │ │ ├── relations.py │ │ ├── sqlalchemy.py │ │ └── validation.py │ ├── metaclass.py │ ├── mixins │ │ ├── __init__.py │ │ ├── alias_mixin.py │ │ ├── excludable_mixin.py │ │ ├── merge_mixin.py │ │ ├── pydantic_mixin.py │ │ ├── relation_mixin.py │ │ └── save_mixin.py │ ├── model.py │ ├── model_row.py │ ├── modelproxy.py │ ├── newbasemodel.py │ ├── ormar_config.py │ ├── quick_access_views.py │ ├── traversible.py │ └── utils.py ├── protocols │ ├── __init__.py │ ├── queryset_protocol.py │ └── relation_protocol.py ├── py.typed ├── queryset │ ├── __init__.py │ ├── actions │ │ ├── __init__.py │ │ ├── filter_action.py │ │ ├── order_action.py │ │ ├── query_action.py │ │ └── select_action.py │ ├── clause.py │ ├── field_accessor.py │ ├── join.py │ ├── queries │ │ ├── __init__.py │ │ ├── filter_query.py │ │ ├── limit_query.py │ │ ├── offset_query.py │ │ ├── order_query.py │ │ ├── prefetch_query.py │ │ └── query.py │ ├── queryset.py │ ├── reverse_alias_resolver.py │ └── utils.py ├── relations │ ├── __init__.py │ ├── alias_manager.py │ ├── querysetproxy.py │ ├── relation.py │ ├── relation_manager.py │ ├── relation_proxy.py │ └── utils.py ├── signals │ ├── __init__.py │ └── signal.py └── warnings.py ├── poetry.lock ├── pyproject.toml ├── scripts ├── docker-compose.yml ├── test.sh └── test_docs.sh └── tests ├── __init__.py ├── lifespan.py ├── settings.py ├── test_deferred ├── __init__.py ├── test_forward_cross_refs.py ├── test_forward_refs.py ├── test_more_same_table_joins.py └── test_same_table_joins.py ├── test_encryption ├── __init__.py └── test_encrypted_columns.py ├── test_exclude_include_dict ├── __init__.py ├── test_complex_relation_tree_performance.py ├── test_dumping_model_to_dict.py ├── test_excludable_items.py ├── test_excluding_fields_in_fastapi.py ├── test_excluding_fields_with_default.py ├── test_excluding_nested_models_lists.py ├── test_excluding_subset_of_columns.py └── test_pydantic_dict_params.py ├── test_fastapi ├── __init__.py ├── test_binary_fields.py ├── test_docs_with_multiple_relations_to_one.py ├── test_enum_schema.py ├── test_excludes_with_get_pydantic.py ├── test_excluding_fields.py ├── test_extra_ignore_parameter.py ├── test_fastapi_docs.py ├── test_fastapi_usage.py ├── test_inheritance_concrete_fastapi.py ├── test_inheritance_mixins_fastapi.py ├── test_json_field_fastapi.py ├── test_m2m_forwardref.py ├── test_more_reallife_fastapi.py ├── test_nested_saving.py ├── test_recursion_error.py ├── test_relations_with_nested_defaults.py ├── test_schema_not_allowed_params.py ├── test_skip_reverse_models.py └── test_wekref_exclusion.py ├── test_hashes ├── __init__.py └── test_many_to_many.py ├── test_inheritance_and_pydantic_generation ├── __init__.py ├── test_excluding_parent_fields_inheritance.py ├── test_geting_pydantic_models.py ├── test_inheritance_concrete.py ├── test_inheritance_mixins.py ├── test_inheritance_of_property_fields.py ├── test_inheritance_with_default.py ├── test_inherited_class_is_not_abstract_by_default.py ├── test_nested_models_pydantic.py ├── test_pydantic_fields_order.py ├── test_validators_are_inherited.py └── test_validators_in_generated_pydantic.py ├── test_meta_constraints ├── __init__.py ├── test_check_constraints.py ├── test_index_constraints.py └── test_unique_constraints.py ├── test_model_definition ├── __init__.py ├── pks_and_fks │ ├── __init__.py │ ├── test_non_integer_pkey.py │ ├── test_saving_string_pks.py │ └── test_uuid_fks.py ├── test_aliases.py ├── test_columns.py ├── test_create_uses_init_for_consistency.py ├── test_dates_with_timezone.py ├── test_equality_and_hash.py ├── test_extra_ignore_parameter.py ├── test_field_quoting.py ├── test_fields_access.py ├── test_foreign_key_value_used_for_related_model.py ├── test_iterate.py ├── test_model_construct.py ├── test_model_definition.py ├── test_models.py ├── test_models_are_pickable.py ├── test_overwriting_pydantic_field_type.py ├── test_overwriting_sql_nullable.py ├── test_pk_field_is_always_not_null.py ├── test_properties.py ├── test_pydantic_fields.py ├── test_pydantic_only_fields.py ├── test_pydantic_private_attributes.py ├── test_save_status.py ├── test_saving_nullable_fields.py ├── test_server_default.py ├── test_setting_comments_in_db.py └── test_through_model_relation_setup_on_clone.py ├── test_model_methods ├── __init__.py ├── test_excludes_in_load_all.py ├── test_load_all.py ├── test_populate_default_values.py ├── test_save_related.py ├── test_save_related_from_dict.py ├── test_save_related_pk_only.py ├── test_save_related_uuid.py ├── test_update.py └── test_upsert.py ├── test_ordering ├── __init__.py ├── test_default_model_order.py ├── test_default_relation_order.py ├── test_default_through_relation_order.py └── test_proper_order_of_sorting_apply.py ├── test_queries ├── __init__.py ├── test_adding_related.py ├── test_aggr_functions.py ├── test_deep_relations_select_all.py ├── test_filter_groups.py ├── test_indirect_relations_to_self.py ├── test_isnull_filter.py ├── test_nested_reverse_relations.py ├── test_non_relation_fields_not_merged.py ├── test_or_filters.py ├── test_order_by.py ├── test_pagination.py ├── test_queryproxy_on_m2m_models.py ├── test_queryset_level_methods.py ├── test_quoting_table_names_in_on_join_clause.py ├── test_reserved_sql_keywords_escaped.py ├── test_reverse_fk_queryset.py ├── test_selecting_subset_of_columns.py └── test_values_and_values_list.py ├── test_relations ├── __init__.py ├── test_cascades.py ├── test_customizing_through_model_relation_names.py ├── test_database_fk_creation.py ├── test_foreign_keys.py ├── test_m2m_through_fields.py ├── test_many_to_many.py ├── test_postgress_select_related_with_limit.py ├── test_prefetch_related.py ├── test_prefetch_related_multiple_models_relation.py ├── test_prefetch_related_with_same_models.py ├── test_python_style_relations.py ├── test_relations_default_exception.py ├── test_replacing_models_with_copy.py ├── test_reverse_relation_preserves_validator.py ├── test_saving_related.py ├── test_select_related_with_limit.py ├── test_select_related_with_m2m_and_pk_name_set.py ├── test_selecting_proper_table_prefix.py ├── test_skipping_reverse.py ├── test_through_relations_fail.py └── test_weakref_checking.py ├── test_signals ├── __init__.py ├── test_signals.py └── test_signals_for_relations.py ├── test_types.py └── test_utils ├── __init__.py ├── test_models_helpers.py └── test_queryset_utils.py /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | checks: 3 | method-complexity: 4 | config: 5 | threshold: 8 6 | argument-count: 7 | config: 8 | threshold: 6 9 | method-count: 10 | config: 11 | threshold: 25 12 | method-length: 13 | config: 14 | threshold: 35 15 | file-lines: 16 | config: 17 | threshold: 500 18 | engines: 19 | bandit: 20 | enabled: true 21 | checks: 22 | assert_used: 23 | enabled: false 24 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: "80...100" 5 | 6 | status: 7 | project: yes 8 | patch: yes 9 | changes: yes 10 | 11 | comment: 12 | layout: "reach, diff, flags, files" 13 | behavior: default 14 | require_changes: false # if true: only post the comment if coverage changes 15 | require_base: no # [yes :: must have a base report to post] 16 | require_head: yes # [yes :: must have a head report to post] 17 | branches: # branch names that can post comment 18 | - "master" -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = ormar, tests 3 | omit = ./tests/test.db, *py.typed* 4 | data_file = .coverage 5 | 6 | [report] 7 | omit = ./tests/test.db, *py.typed* 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: collerek -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | (Note: this should be a complete and concise piece of code that allows reproduction of an issue) 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Versions (please complete the following information):** 29 | - Database backend used (mysql/sqlite/postgress) 30 | - Python version 31 | - `ormar` version 32 | - `pydantic` version 33 | - if applicable `fastapi` version 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: I have a question ❓ 3 | url: https://github.com/collerek/ormar/discussions 4 | about: If you have any question about the usage of ormar, please open a discussion first. 5 | - name: I want a new feature 🆕 6 | url: https://github.com/collerek/ormar/discussions 7 | about: If you would like to request or make a change/enhancement that is not trivial, please open a discussion first. 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic set up 2 | # https://help.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#package-ecosystem 3 | 4 | version: 2 5 | updates: 6 | 7 | - package-ecosystem: "pip" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | # Based on https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request 2 | name: Dependabot auto-approve and auto-merge 3 | on: pull_request_target 4 | 5 | permissions: 6 | pull-requests: write 7 | contents: write 8 | 9 | jobs: 10 | autoapprove: 11 | name: Auto Approve a PR by dependabot 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Auto approve 15 | uses: hmarr/auto-approve-action@v4.0.0 16 | if: github.actor == 'dependabot[bot]' 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | dependabot: 20 | runs-on: ubuntu-latest 21 | if: ${{ github.actor == 'dependabot[bot]' }} 22 | steps: 23 | - name: Dependabot metadata 24 | id: metadata 25 | uses: dependabot/fetch-metadata@v2.4.0 26 | with: 27 | github-token: "${{ secrets.GITHUB_TOKEN }}" 28 | 29 | - name: Enable auto-merge for Dependabot PRs 30 | # Automatically merge semver-patch and semver-minor PRs 31 | # or black dependency upgrades 32 | if: "${{ steps.metadata.outputs.update-type == 33 | 'version-update:semver-minor' || 34 | steps.metadata.outputs.update-type == 35 | 'version-update:semver-patch' || 36 | steps.metadata.outputs.dependency-names == 37 | 'black' }}" 38 | 39 | # https://cli.github.com/manual/gh_pr_merge 40 | run: gh pr merge --auto --squash "$PR_URL" 41 | env: 42 | PR_URL: ${{github.event.pull_request.html_url}} 43 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 44 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: codspeed-benchmarks 2 | 3 | on: 4 | push: 5 | branches: [ master, pydantic_v2 ] 6 | pull_request: 7 | branches: [ master, pydantic_v2 ] 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 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: "3.11" 20 | 21 | - name: Install Poetry 22 | uses: snok/install-poetry@v1.4 23 | with: 24 | version: 1.8.4 25 | virtualenvs-create: false 26 | 27 | - name: Poetry details 28 | run: | 29 | poetry --version 30 | poetry config --list 31 | 32 | - name: Install dependencies 33 | run: poetry install --extras "all" 34 | 35 | - name: Run benchmarks 36 | uses: CodSpeedHQ/action@v3 37 | with: 38 | token: ${{ secrets.CODSPEED_TOKEN }} 39 | run: poetry run pytest benchmarks/ --codspeed -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build Documentation using MkDocs 2 | on: 3 | push: 4 | # Pattern matched against refs/tags 5 | tags: 6 | - '**' 7 | jobs: 8 | build: 9 | name: Build and Deploy Documentation 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Master 13 | uses: actions/checkout@v4 14 | - name: Set up Python 3.8 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: '3.8' 18 | - name: Install Poetry 19 | uses: snok/install-poetry@v1.4 20 | with: 21 | version: 1.8.4 22 | virtualenvs-create: false 23 | - name: Install dependencies 24 | run: | 25 | poetry install --extras "all" 26 | env: 27 | POETRY_VIRTUALENVS_CREATE: false 28 | - name: Set env 29 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 30 | - name: Test 31 | run: | 32 | echo $RELEASE_VERSION 33 | echo ${{ env.RELEASE_VERSION }} 34 | - name: Deploy 35 | run: | 36 | mike deploy --push --update-aliases ${{ env.RELEASE_VERSION }} latest 37 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: lint 5 | 6 | on: 7 | push: 8 | branches-ignore: 9 | - 'gh-pages' 10 | pull_request: 11 | branches: [ master, pydantic_v2 ] 12 | 13 | jobs: 14 | lint: 15 | name: "Python ${{ matrix.python-version }}" 16 | runs-on: ubuntu-latest 17 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'collerek/ormar' 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: 3.11 25 | 26 | - name: Install Poetry 27 | uses: snok/install-poetry@v1.4 28 | with: 29 | version: 1.8.4 30 | virtualenvs-create: false 31 | 32 | - name: Poetry details 33 | run: | 34 | poetry --version 35 | poetry config --list 36 | 37 | - name: Install dependencies 38 | run: poetry install --extras "all" --no-root 39 | 40 | - name: Format 41 | run: make fmt 42 | 43 | - name: Lint 44 | run: make lint 45 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | jobs: 14 | deploy: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: '3.x' 24 | 25 | - name: Install Poetry 26 | uses: snok/install-poetry@v1.4 27 | with: 28 | version: 1.8.4 29 | virtualenvs-create: true 30 | virtualenvs-in-project: true 31 | 32 | - name: Build and publish 33 | run: | 34 | poetry build -vvv 35 | poetry publish -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} 36 | -------------------------------------------------------------------------------- /.github/workflows/test_docs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: test_docs 5 | 6 | on: 7 | push: 8 | branches-ignore: 9 | - 'gh-pages' 10 | pull_request: 11 | branches: [ master, pydantic_v2 ] 12 | 13 | jobs: 14 | tests_docs: 15 | name: "Python ${{ matrix.python-version }}" 16 | runs-on: ubuntu-latest 17 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'collerek/ormar' 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: 3.11 24 | - name: Install Poetry 25 | uses: snok/install-poetry@v1.4 26 | with: 27 | version: 1.8.4 28 | virtualenvs-create: false 29 | - name: Install dependencies 30 | run: | 31 | poetry install --extras "all" 32 | env: 33 | POETRY_VIRTUALENVS_CREATE: false 34 | - name: Test docs 35 | run: bash scripts/test_docs.sh 36 | -------------------------------------------------------------------------------- /.github/workflows/type-check.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: type_check 5 | 6 | on: 7 | push: 8 | branches-ignore: 9 | - 'gh-pages' 10 | pull_request: 11 | branches: [ master, pydantic_v2 ] 12 | 13 | jobs: 14 | lint: 15 | name: "Python ${{ matrix.python-version }}" 16 | runs-on: ubuntu-latest 17 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'collerek/ormar' 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: 3.11 25 | 26 | - name: Install Poetry 27 | uses: snok/install-poetry@v1.4 28 | with: 29 | version: 1.8.4 30 | virtualenvs-create: false 31 | 32 | - name: Poetry details 33 | run: | 34 | poetry --version 35 | poetry config --list 36 | 37 | - name: Install dependencies 38 | run: poetry install --extras "all" --no-root 39 | 40 | - name: Type check 41 | run: make type_check 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | p38venv 2 | alembic 3 | alembic.ini 4 | build 5 | .idea 6 | .pytest_cache 7 | .mypy_cache 8 | *.coverage 9 | *.pyc 10 | *.log 11 | test.db 12 | .vscode/ 13 | dist 14 | /ormar.egg-info/ 15 | site 16 | profile.py 17 | *.db 18 | *.db-journal 19 | *coverage.xml 20 | .benchmarks/ 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: pre-commit-local 5 | name: format 6 | entry: make pre-commit 7 | language: python 8 | pass_filenames: false 9 | 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Radosław Drążkiewicz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test_all: test_pg test_mysql test_sqlite 2 | 3 | test_pg: export DATABASE_URL=postgresql://username:password@localhost:5432/testsuite 4 | test_pg: 5 | docker-compose -f scripts/docker-compose.yml up -d postgres 6 | bash scripts/test.sh -svv 7 | docker-compose -f scripts/docker-compose.yml stop postgres 8 | 9 | test_mysql: export DATABASE_URL=mysql://username:password@127.0.0.1:3306/testsuite 10 | test_mysql: 11 | docker-compose -f "scripts/docker-compose.yml" up -d mysql 12 | bash scripts/test.sh -svv 13 | docker-compose -f scripts/docker-compose.yml stop mysql 14 | 15 | test_sqlite: 16 | bash scripts/test.sh -svv 17 | 18 | test_docs: 19 | bash scripts/test_docs.sh -svv 20 | 21 | test: 22 | pytest -svv tests/ 23 | 24 | coverage: 25 | pytest --cov=ormar --cov=tests --cov-fail-under=100 --cov-report=term-missing tests 26 | 27 | type_check: 28 | mkdir -p .mypy_cache && poetry run python -m mypy ormar tests --ignore-missing-imports --install-types --non-interactive 29 | 30 | lint: 31 | poetry run python -m ruff check . --fix 32 | 33 | fmt: 34 | poetry run python -m black . 35 | 36 | pre-commit: fmt lint type_check -------------------------------------------------------------------------------- /benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/benchmarks/__init__.py -------------------------------------------------------------------------------- /benchmarks/test_benchmark_aggregate.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | from benchmarks.conftest import Author 6 | 7 | pytestmark = pytest.mark.asyncio 8 | 9 | 10 | @pytest.mark.parametrize("num_models", [250, 500, 1000]) 11 | async def test_count(aio_benchmark, num_models: int, authors_in_db: List[Author]): 12 | @aio_benchmark 13 | async def count(): 14 | return await Author.objects.count() 15 | 16 | c = count() 17 | assert c == len(authors_in_db) 18 | 19 | 20 | @pytest.mark.parametrize("num_models", [250, 500, 1000]) 21 | async def test_avg(aio_benchmark, num_models: int, authors_in_db: List[Author]): 22 | @aio_benchmark 23 | async def avg(): 24 | return await Author.objects.avg("score") 25 | 26 | average = avg() 27 | assert 0 <= average <= 100 28 | 29 | 30 | @pytest.mark.parametrize("num_models", [250, 500, 1000]) 31 | async def test_sum(aio_benchmark, num_models: int, authors_in_db: List[Author]): 32 | @aio_benchmark 33 | async def sum_(): 34 | return await Author.objects.sum("score") 35 | 36 | s = sum_() 37 | assert 0 <= s <= 100 * num_models 38 | 39 | 40 | @pytest.mark.parametrize("num_models", [250, 500, 1000]) 41 | async def test_min(aio_benchmark, num_models: int, authors_in_db: List[Author]): 42 | @aio_benchmark 43 | async def min_(): 44 | return await Author.objects.min("score") 45 | 46 | m = min_() 47 | assert 0 <= m <= 100 48 | 49 | 50 | @pytest.mark.parametrize("num_models", [250, 500, 1000]) 51 | async def test_max(aio_benchmark, num_models: int, authors_in_db: List[Author]): 52 | @aio_benchmark 53 | async def max_(): 54 | return await Author.objects.max("score") 55 | 56 | m = max_() 57 | assert 0 <= m <= 100 58 | -------------------------------------------------------------------------------- /benchmarks/test_benchmark_bulk_create.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | import pytest 5 | 6 | from benchmarks.conftest import Author 7 | 8 | pytestmark = pytest.mark.asyncio 9 | 10 | 11 | @pytest.mark.parametrize("num_models", [10, 20, 40]) 12 | async def test_making_and_inserting_models_in_bulk(aio_benchmark, num_models: int): 13 | @aio_benchmark 14 | async def make_and_insert(num_models: int): 15 | authors = [ 16 | Author( 17 | name="".join(random.sample(string.ascii_letters, 5)), 18 | score=int(random.random() * 100), 19 | ) 20 | for i in range(0, num_models) 21 | ] 22 | assert len(authors) == num_models 23 | 24 | await Author.objects.bulk_create(authors) 25 | 26 | make_and_insert(num_models) 27 | -------------------------------------------------------------------------------- /benchmarks/test_benchmark_bulk_update.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | from typing import List 4 | 5 | import pytest 6 | 7 | from benchmarks.conftest import Author 8 | 9 | pytestmark = pytest.mark.asyncio 10 | 11 | 12 | @pytest.mark.parametrize("num_models", [10, 20, 40]) 13 | async def test_updating_models_in_bulk( 14 | aio_benchmark, num_models: int, authors_in_db: List[Author] 15 | ): 16 | starting_first_name = authors_in_db[0].name 17 | 18 | @aio_benchmark 19 | async def update(authors: List[Author]): 20 | await Author.objects.bulk_update(authors) 21 | 22 | for author in authors_in_db: 23 | author.name = "".join(random.sample(string.ascii_letters, 5)) 24 | 25 | update(authors_in_db) 26 | author = await Author.objects.get(id=authors_in_db[0].id) 27 | assert author.name != starting_first_name 28 | -------------------------------------------------------------------------------- /benchmarks/test_benchmark_delete.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | from benchmarks.conftest import Author 6 | 7 | pytestmark = pytest.mark.asyncio 8 | 9 | 10 | @pytest.mark.parametrize("num_models", [250, 500, 1000]) 11 | async def test_deleting_all( 12 | aio_benchmark, num_models: int, authors_in_db: List[Author] 13 | ): 14 | @aio_benchmark 15 | async def delete_all(): 16 | await Author.objects.delete(each=True) 17 | 18 | delete_all() 19 | 20 | num = await Author.objects.count() 21 | assert num == 0 22 | 23 | 24 | @pytest.mark.parametrize("num_models", [10, 20, 40]) 25 | async def test_deleting_individually( 26 | aio_benchmark, num_models: int, authors_in_db: List[Author] 27 | ): 28 | @aio_benchmark 29 | async def delete_one_by_one(authors: List[Author]): 30 | for author in authors: 31 | await Author.objects.filter(id=author.id).delete() 32 | 33 | delete_one_by_one(authors_in_db) 34 | 35 | num = await Author.objects.count() 36 | assert num == 0 37 | -------------------------------------------------------------------------------- /benchmarks/test_benchmark_init.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | import pytest 5 | 6 | from benchmarks.conftest import Author, Book, Publisher 7 | 8 | pytestmark = pytest.mark.asyncio 9 | 10 | 11 | @pytest.mark.parametrize("num_models", [250, 500, 1000]) 12 | async def test_initializing_models(aio_benchmark, num_models: int): 13 | @aio_benchmark 14 | async def initialize_models(num_models: int): 15 | authors = [ 16 | Author( 17 | name="".join(random.sample(string.ascii_letters, 5)), 18 | score=int(random.random() * 100), 19 | ) 20 | for i in range(0, num_models) 21 | ] 22 | assert len(authors) == num_models 23 | 24 | _ = initialize_models(num_models) 25 | 26 | 27 | @pytest.mark.parametrize("num_models", [10, 20, 40]) 28 | async def test_initializing_models_with_related_models(aio_benchmark, num_models: int): 29 | @aio_benchmark 30 | async def initialize_models_with_related_models( 31 | author: Author, publisher: Publisher, num_models: int 32 | ): 33 | _ = [ 34 | Book( 35 | author=author, 36 | publisher=publisher, 37 | title="".join(random.sample(string.ascii_letters, 5)), 38 | year=random.randint(0, 2000), 39 | ) 40 | for i in range(0, num_models) 41 | ] 42 | 43 | author = await Author(name="Author", score=10).save() 44 | publisher = await Publisher(name="Publisher", prestige=random.randint(0, 10)).save() 45 | 46 | _ = initialize_models_with_related_models( 47 | author=author, publisher=publisher, num_models=num_models 48 | ) 49 | -------------------------------------------------------------------------------- /benchmarks/test_benchmark_iterate.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | from benchmarks.conftest import Author 6 | 7 | pytestmark = pytest.mark.asyncio 8 | 9 | 10 | @pytest.mark.parametrize("num_models", [250, 500, 1000]) 11 | async def test_iterate(aio_benchmark, num_models: int, authors_in_db: List[Author]): 12 | @aio_benchmark 13 | async def iterate_over_all(authors: List[Author]): 14 | authors = [] 15 | async for author in Author.objects.iterate(): 16 | authors.append(author) 17 | return authors 18 | 19 | authors = iterate_over_all(authors_in_db) 20 | for idx, author in enumerate(authors_in_db): 21 | assert authors[idx].id == author.id 22 | -------------------------------------------------------------------------------- /benchmarks/test_benchmark_save.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | import pytest 5 | 6 | from benchmarks.conftest import Author, Book, Publisher 7 | 8 | pytestmark = pytest.mark.asyncio 9 | 10 | 11 | @pytest.mark.parametrize("num_models", [10, 20, 40]) 12 | async def test_saving_models_individually(aio_benchmark, num_models: int): 13 | @aio_benchmark 14 | async def make_and_insert(num_models: int): 15 | authors = [ 16 | Author( 17 | name="".join(random.sample(string.ascii_letters, 5)), 18 | score=int(random.random() * 100), 19 | ) 20 | for i in range(0, num_models) 21 | ] 22 | assert len(authors) == num_models 23 | 24 | ids = [] 25 | for author in authors: 26 | a = await author.save() 27 | ids.append(a) 28 | return ids 29 | 30 | ids = make_and_insert(num_models) 31 | for id in ids: 32 | assert id is not None 33 | 34 | 35 | @pytest.mark.parametrize("num_models", [10, 20, 40]) 36 | async def test_saving_models_individually_with_related_models( 37 | aio_benchmark, num_models: int, author: Author, publisher: Publisher 38 | ): 39 | @aio_benchmark 40 | async def making_and_inserting_related_models_one_by_one( 41 | author: Author, publisher: Publisher, num_models: int 42 | ): 43 | books = [ 44 | Book( 45 | author=author, 46 | publisher=publisher, 47 | title="".join(random.sample(string.ascii_letters, 5)), 48 | year=random.randint(0, 2000), 49 | ) 50 | for i in range(0, num_models) 51 | ] 52 | 53 | ids = [] 54 | for book in books: 55 | await book.save() 56 | ids.append(book.id) 57 | 58 | return ids 59 | 60 | ids = making_and_inserting_related_models_one_by_one( 61 | author=author, publisher=publisher, num_models=num_models 62 | ) 63 | 64 | for id in ids: 65 | assert id is not None 66 | -------------------------------------------------------------------------------- /benchmarks/test_benchmark_update.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | from typing import List 4 | 5 | import pytest 6 | 7 | from benchmarks.conftest import Author 8 | 9 | pytestmark = pytest.mark.asyncio 10 | 11 | 12 | @pytest.mark.parametrize("num_models", [10, 20, 40]) 13 | async def test_updating_models_individually( 14 | aio_benchmark, num_models: int, authors_in_db: List[Author] 15 | ): 16 | starting_first_name = authors_in_db[0].name 17 | 18 | @aio_benchmark 19 | async def update(authors: List[Author]): 20 | for author in authors: 21 | _ = await author.update( 22 | name="".join(random.sample(string.ascii_letters, 5)) 23 | ) 24 | 25 | update(authors_in_db) 26 | author = await Author.objects.get(id=authors_in_db[0].id) 27 | assert author.name != starting_first_name 28 | -------------------------------------------------------------------------------- /benchmarks/test_benchmark_values.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | from benchmarks.conftest import Author 6 | 7 | pytestmark = pytest.mark.asyncio 8 | 9 | 10 | @pytest.mark.parametrize("num_models", [250, 500, 1000]) 11 | async def test_values(aio_benchmark, num_models: int, authors_in_db: List[Author]): 12 | @aio_benchmark 13 | async def get_all_values(authors: List[Author]): 14 | return await Author.objects.values() 15 | 16 | authors_list = get_all_values(authors_in_db) 17 | for idx, author in enumerate(authors_in_db): 18 | assert authors_list[idx]["id"] == author.id 19 | 20 | 21 | @pytest.mark.parametrize("num_models", [250, 500, 1000]) 22 | async def test_values_list(aio_benchmark, num_models: int, authors_in_db: List[Author]): 23 | @aio_benchmark 24 | async def get_all_values_list(authors: List[Author]): 25 | return await Author.objects.values_list() 26 | 27 | authors_list = get_all_values_list(authors_in_db) 28 | for idx, author in enumerate(authors_in_db): 29 | assert authors_list[idx][0] == author.id 30 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | All contributions to *ormar* are welcomed! 2 | 3 | ## Issues 4 | 5 | To make it as simple as possible for us to help you, please include the following: 6 | 7 | * OS 8 | * python version 9 | * ormar version 10 | * database backend (mysql, sqlite or postgresql) 11 | 12 | Please try to always include the above unless you're unable to install *ormar* or **know** it's not relevant 13 | to your question or feature request. 14 | 15 | ## Pull Requests 16 | 17 | It should be quite straight forward to get started and create a Pull Request. 18 | 19 | !!! note 20 | Unless your change is trivial (typo, docs tweak etc.), please create an issue to discuss the change before 21 | creating a pull request. 22 | 23 | To make contributing as easy and fast as possible, you'll want to run tests and linting locally. 24 | 25 | You'll need to have **python 3.6.2**, **3.7**, or **3.8**, **poetry**, and **git** installed. 26 | 27 | ```bash 28 | # 1. clone your fork and cd into the repo directory 29 | git clone git@github.com:/ormar.git 30 | cd ormar 31 | 32 | # 2. Install ormar, dependencies and test dependencies 33 | poetry install -E dev 34 | 35 | # 3. Checkout a new branch and make your changes 36 | git checkout -b my-new-feature-branch 37 | # make your changes... 38 | 39 | # 4. Formatting and linting 40 | # ormar uses black for formatting, flake8 for linting and mypy for type hints check 41 | # run all of the following as all those calls will be run on travis after every push 42 | black ormar tests 43 | flake8 ormar 44 | mypy ormar tests 45 | 46 | # 5. Run tests 47 | # on localhost all tests are run against sglite backend 48 | # rest of the backends will be checked after push 49 | pytest -svv --cov=ormar --cov=tests --cov-fail-under=100 --cov-report=term-missing 50 | 51 | # 6. Build documentation 52 | mkdocs build 53 | # if you have changed the documentation make sure it builds successfully 54 | # you can also use `mkdocs serve` to serve the documentation at localhost:8000 55 | 56 | # ... commit, push, and create your pull request 57 | ``` 58 | 59 | !!!tip 60 | For more information on how and why ormar works the way it works 61 | please see the [API documentation][API documentation] 62 | 63 | [API documentation]: ./api/index.md -------------------------------------------------------------------------------- /docs/gen_ref_pages.py: -------------------------------------------------------------------------------- 1 | """Generate the code reference pages and navigation.""" 2 | 3 | from pathlib import Path 4 | 5 | import mkdocs_gen_files 6 | 7 | nav = mkdocs_gen_files.Nav() 8 | 9 | for path in sorted(Path("ormar").rglob("*.py")): 10 | module_path = path.relative_to(".").with_suffix("") 11 | doc_path = path.relative_to("ormar").with_suffix(".md") 12 | full_doc_path = Path("api", doc_path) 13 | 14 | parts = tuple(module_path.parts) 15 | 16 | if parts[-1] == "__init__": 17 | parts = parts[:-1] 18 | doc_path = doc_path.with_name("index.md") 19 | full_doc_path = full_doc_path.with_name("index.md") 20 | elif parts[-1] == "__main__": 21 | continue 22 | 23 | nav[parts] = str(doc_path) 24 | 25 | with mkdocs_gen_files.open(full_doc_path, "w") as fd: 26 | ident = ".".join(parts) 27 | fd.write(f"::: {ident}") 28 | 29 | mkdocs_gen_files.set_edit_path(full_doc_path, path) 30 | 31 | with mkdocs_gen_files.open("api/SUMMARY.md", "w") as nav_file: 32 | nav_file.writelines(nav.build_literate_nav()) 33 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | Installation is as simple as: 4 | 5 | ```py 6 | pip install ormar 7 | ``` 8 | 9 | ### Dependencies 10 | 11 | Ormar uses `databases` for connectivity issues, `pydantic` for validation and `sqlalchemy-core` for queries. 12 | 13 | All three should install along the installation of ormar if not present at your system before. 14 | 15 | * databases 16 | * pydantic 17 | * sqlalchemy 18 | 19 | The required versions are pinned in the pyproject.toml file. 20 | 21 | ## Optional dependencies 22 | 23 | *ormar* has three optional dependencies based on database backend you use: 24 | 25 | ### Database backend 26 | 27 | #### Postgresql 28 | 29 | ```py 30 | pip install ormar[postgresql] 31 | ``` 32 | Will install also `asyncpg` and `psycopg2`. 33 | 34 | #### Mysql 35 | 36 | ```py 37 | pip install ormar[mysql] 38 | ``` 39 | 40 | Will install also `aiomysql` and `pymysql`. 41 | 42 | #### Sqlite 43 | 44 | ```py 45 | pip install ormar[sqlite] 46 | ``` 47 | 48 | Will install also `aiosqlite`. 49 | 50 | ### Orjson 51 | 52 | ```py 53 | pip install ormar[orjson] 54 | ``` 55 | 56 | Will install also `orjson` that is much faster than builtin json parser. 57 | 58 | ### Crypto 59 | 60 | ```py 61 | pip install ormar[crypto] 62 | ``` 63 | 64 | Will install also `cryptography` that is required to work with encrypted columns. 65 | 66 | ### Manual installation of dependencies 67 | 68 | Of course, you can also install these requirements manually with `pip install asyncpg` etc. 69 | -------------------------------------------------------------------------------- /docs/models/internals.md: -------------------------------------------------------------------------------- 1 | # Internals 2 | 3 | Apart from special parameters defined in the `Model` during definition (tablename, metadata etc.) the `Model` provides you with useful internals. 4 | 5 | ## Pydantic Model 6 | 7 | All `Model` classes inherit from `pydantic.BaseModel` so you can access all normal attributes of pydantic models. 8 | 9 | For example to list pydantic model fields you can: 10 | 11 | ```Python hl_lines="20" 12 | --8<-- "../docs_src/models/docs003.py" 13 | ``` 14 | 15 | !!!tip 16 | Note how the primary key `id` field is optional as `Integer` primary key by default has `autoincrement` set to `True`. 17 | 18 | !!!info 19 | For more options visit official [pydantic][pydantic] documentation. 20 | 21 | ## Sqlalchemy Table 22 | 23 | To access auto created sqlalchemy table you can use `Model.ormar_config.table` parameter 24 | 25 | For example to list table columns you can: 26 | 27 | ```Python hl_lines="24" 28 | --8<-- "../docs_src/models/docs004.py" 29 | ``` 30 | 31 | !!!tip 32 | You can access table primary key name by `Course.ormar_config.pkname` 33 | 34 | !!!info 35 | For more options visit official [sqlalchemy-metadata][sqlalchemy-metadata] documentation. 36 | 37 | ## Fields Definition 38 | 39 | To access ormar `Fields` you can use `Model.ormar_config.model_fields` parameter 40 | 41 | For example to list table model fields you can: 42 | 43 | ```Python hl_lines="22" 44 | --8<-- "../docs_src/models/docs005.py" 45 | ``` 46 | 47 | !!!info 48 | Note that fields stored on a model are `classes` not `instances`. 49 | 50 | So if you print just model fields you will get: 51 | 52 | `{'id': , ` 53 | 54 | `'name': , ` 55 | 56 | `'completed': }` 57 | 58 | 59 | [fields]: ./fields.md 60 | [relations]: ./relations/index.md 61 | [queries]: ./queries.md 62 | [pydantic]: https://pydantic-docs.helpmanual.io/ 63 | [sqlalchemy-core]: https://docs.sqlalchemy.org/en/latest/core/ 64 | [sqlalchemy-metadata]: https://docs.sqlalchemy.org/en/13/core/metadata.html 65 | [databases]: https://github.com/encode/databases 66 | [sqlalchemy connection string]: https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls 67 | [sqlalchemy table creation]: https://docs.sqlalchemy.org/en/13/core/metadata.html#creating-and-dropping-database-tables 68 | [alembic]: https://alembic.sqlalchemy.org/en/latest/tutorial.html 69 | [save status]: ../models/#model-save-status 70 | [Internals]: #internals 71 | -------------------------------------------------------------------------------- /docs/mypy.md: -------------------------------------------------------------------------------- 1 | To provide better errors check you should use mypy with pydantic [plugin][plugin] 2 | 3 | Please use notation introduced in version 0.4.0. 4 | 5 | ```Python hl_lines="15-17" 6 | --8<-- "../docs_src/models/docs012.py" 7 | ``` 8 | 9 | Note that above example is not using the type hints, so further operations with mypy might fail, depending on the context. 10 | 11 | Preferred notation should look liked this: 12 | 13 | ```Python hl_lines="15-17" 14 | --8<-- "../docs_src/models/docs001.py" 15 | ``` 16 | 17 | 18 | 19 | 20 | [plugin]: https://pydantic-docs.helpmanual.io/mypy_plugin/ -------------------------------------------------------------------------------- /docs/plugin.md: -------------------------------------------------------------------------------- 1 | While `ormar` will work with any IDE there is a PyCharm `pydantic` plugin that enhances the user experience for this IDE. 2 | 3 | Plugin is available on the JetBrains Plugins Repository for PyCharm: [plugin page][plugin page]. 4 | 5 | You can install the plugin for free from the plugin marketplace 6 | (PyCharm's Preferences -> Plugin -> Marketplace -> search "pydantic"). 7 | 8 | !!!note 9 | For plugin to work properly you need to provide valid type hints for model fields. 10 | 11 | !!!info 12 | Plugin supports type hints, argument inspection and more but mainly only for __init__ methods 13 | 14 | More information can be found on the 15 | [official plugin page](https://plugins.jetbrains.com/plugin/12861-pydantic) 16 | and [github repository](https://github.com/koxudaxi/pydantic-pycharm-plugin). 17 | 18 | [plugin page]: https://plugins.jetbrains.com/plugin/12861-pydantic -------------------------------------------------------------------------------- /docs/transactions.md: -------------------------------------------------------------------------------- 1 | # Transactions 2 | 3 | Database transactions are supported thanks to `encode/databases` which is used to issue async queries. 4 | 5 | ## Basic usage 6 | 7 | To use transactions use `database.transaction` as async context manager: 8 | 9 | ```python 10 | async with database.transaction(): 11 | # everything called here will be one transaction 12 | await Model1().save() 13 | await Model2().save() 14 | ... 15 | ``` 16 | 17 | !!!note 18 | Note that it has to be the same `database` that the one used in Model's `ormar_config` object. 19 | 20 | To avoid passing `database` instance around in your code you can extract the instance from each `Model`. 21 | Database provided during declaration of `ormar.Model` is available through `ormar_config.database` and can 22 | be reached from both class and instance. 23 | 24 | ```python 25 | import databases 26 | import sqlalchemy 27 | import ormar 28 | 29 | 30 | base_ormar_config = OrmarConfig( 31 | metadata=sqlalchemy.MetaData(), 32 | database = databases.Database("sqlite:///"), 33 | ) 34 | 35 | 36 | class Author(ormar.Model): 37 | ormar_config = base_ormar_config.copy() 38 | 39 | id: int = ormar.Integer(primary_key=True) 40 | name: str = ormar.String(max_length=255) 41 | 42 | # database is accessible from class 43 | database = Author.ormar_config.database 44 | 45 | # as well as from instance 46 | author = Author(name="Stephen King") 47 | database = author.ormar_config.database 48 | ``` 49 | 50 | You can also use `.transaction()` as a function decorator on any async function: 51 | 52 | ```python 53 | @database.transaction() 54 | async def create_users(request): 55 | ... 56 | ``` 57 | 58 | Transaction blocks are managed as task-local state. Nested transactions 59 | are fully supported, and are implemented using database savepoints. 60 | 61 | ## Manual commits/ rollbacks 62 | 63 | For a lower-level transaction API you can trigger it manually 64 | 65 | ```python 66 | transaction = await database.transaction() 67 | try: 68 | await transaction.start() 69 | ... 70 | except: 71 | await transaction.rollback() 72 | else: 73 | await transaction.commit() 74 | ``` 75 | 76 | 77 | ## Testing 78 | 79 | Transactions can also be useful during testing when you can apply force rollback 80 | and you do not have to clean the data after each test. 81 | 82 | ```python 83 | @pytest.mark.asyncio 84 | async def sample_test(): 85 | async with database: 86 | async with database.transaction(force_rollback=True): 87 | # your test code here 88 | ... 89 | ``` 90 | -------------------------------------------------------------------------------- /docs_src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/docs_src/__init__.py -------------------------------------------------------------------------------- /docs_src/aggregations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/docs_src/aggregations/__init__.py -------------------------------------------------------------------------------- /docs_src/aggregations/docs001.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | from tests.settings import DATABASE_URL 7 | 8 | database = databases.Database(DATABASE_URL) 9 | metadata = sqlalchemy.MetaData() 10 | 11 | 12 | base_ormar_config = ormar.OrmarConfig( 13 | metadata=metadata, 14 | database=database, 15 | ) 16 | 17 | 18 | class Author(ormar.Model): 19 | ormar_config = base_ormar_config.copy(tablename="authors", order_by=["-name"]) 20 | 21 | id: int = ormar.Integer(primary_key=True) 22 | name: str = ormar.String(max_length=100) 23 | 24 | 25 | class Book(ormar.Model): 26 | 27 | ormar_config = base_ormar_config.copy( 28 | tablename="books", order_by=["year", "-ranking"] 29 | ) 30 | 31 | id: int = ormar.Integer(primary_key=True) 32 | author: Optional[Author] = ormar.ForeignKey(Author) 33 | title: str = ormar.String(max_length=100) 34 | year: int = ormar.Integer(nullable=True) 35 | ranking: int = ormar.Integer(nullable=True) 36 | -------------------------------------------------------------------------------- /docs_src/fastapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/docs_src/fastapi/__init__.py -------------------------------------------------------------------------------- /docs_src/fastapi/docs001.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | import ormar 4 | from fastapi import FastAPI 5 | from tests.lifespan import lifespan 6 | from tests.settings import create_config 7 | 8 | base_ormar_config = create_config() 9 | app = FastAPI(lifespan=lifespan(base_ormar_config)) 10 | 11 | 12 | class Category(ormar.Model): 13 | ormar_config = base_ormar_config.copy(tablename="categories") 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | name: str = ormar.String(max_length=100) 17 | 18 | 19 | class Item(ormar.Model): 20 | ormar_config = base_ormar_config.copy(tablename="items") 21 | 22 | id: int = ormar.Integer(primary_key=True) 23 | name: str = ormar.String(max_length=100) 24 | category: Optional[Category] = ormar.ForeignKey(Category, nullable=True) 25 | 26 | 27 | @app.get("/items/", response_model=List[Item]) 28 | async def get_items(): 29 | items = await Item.objects.select_related("category").all() 30 | return items 31 | 32 | 33 | @app.post("/items/", response_model=Item) 34 | async def create_item(item: Item): 35 | await item.save() 36 | return item 37 | 38 | 39 | @app.post("/categories/", response_model=Category) 40 | async def create_category(category: Category): 41 | await category.save() 42 | return category 43 | 44 | 45 | @app.put("/items/{item_id}") 46 | async def get_item(item_id: int, item: Item): 47 | item_db = await Item.objects.get(pk=item_id) 48 | return await item_db.update(**item.model_dump()) 49 | 50 | 51 | @app.delete("/items/{item_id}") 52 | async def delete_item(item_id: int, item: Item = None): 53 | if item: 54 | return {"deleted_rows": await item.delete()} 55 | item_db = await Item.objects.get(pk=item_id) 56 | return {"deleted_rows": await item_db.delete()} 57 | -------------------------------------------------------------------------------- /docs_src/fastapi/mypy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/docs_src/fastapi/mypy/__init__.py -------------------------------------------------------------------------------- /docs_src/fastapi/mypy/docs001.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import sqlalchemy 4 | 5 | database = databases.Database("sqlite:///db.sqlite") 6 | metadata = sqlalchemy.MetaData() 7 | 8 | 9 | class Course(ormar.Model): 10 | ormar_config = ormar.OrmarConfig( 11 | database=database, 12 | metadata=metadata, 13 | ) 14 | 15 | id = ormar.Integer(primary_key=True) 16 | name = ormar.String(max_length=100) 17 | completed = ormar.Boolean(default=False) 18 | -------------------------------------------------------------------------------- /docs_src/fields/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/docs_src/fields/__init__.py -------------------------------------------------------------------------------- /docs_src/fields/docs001.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Optional 3 | 4 | import databases 5 | import ormar 6 | import sqlalchemy 7 | from examples import create_drop_database 8 | 9 | DATABASE_URL = "sqlite:///test.db" 10 | 11 | ormar_base_config = ormar.OrmarConfig( 12 | database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() 13 | ) 14 | 15 | 16 | class Department(ormar.Model): 17 | ormar_config = ormar_base_config.copy(tablename="departments") 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | name: str = ormar.String(max_length=100) 21 | 22 | 23 | class Course(ormar.Model): 24 | ormar_config = ormar_base_config.copy() 25 | 26 | id: int = ormar.Integer(primary_key=True) 27 | name: str = ormar.String(max_length=100) 28 | completed: bool = ormar.Boolean(default=False) 29 | department: Optional[Department] = ormar.ForeignKey(Department) 30 | 31 | 32 | @create_drop_database(base_config=ormar_base_config) 33 | async def verify(): 34 | department = await Department(name="Science").save() 35 | course = Course(name="Math", completed=False, department=department) 36 | print(department.courses[0]) 37 | # Will produce: 38 | # Course(id=None, 39 | # name='Math', 40 | # completed=False, 41 | # department=Department(id=None, name='Science')) 42 | await course.save() 43 | 44 | 45 | asyncio.run(verify()) 46 | -------------------------------------------------------------------------------- /docs_src/fields/docs002.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | 7 | DATABASE_URL = "sqlite:///test.db" 8 | 9 | ormar_base_config = ormar.OrmarConfig( 10 | database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() 11 | ) 12 | 13 | 14 | class Department(ormar.Model): 15 | ormar_config = ormar_base_config.copy() 16 | 17 | id: int = ormar.Integer(primary_key=True) 18 | name: str = ormar.String(max_length=100) 19 | 20 | 21 | class Course(ormar.Model): 22 | ormar_config = ormar_base_config.copy() 23 | 24 | id: int = ormar.Integer(primary_key=True) 25 | name: str = ormar.String(max_length=100) 26 | completed: bool = ormar.Boolean(default=False) 27 | department: Optional[Department] = ormar.ForeignKey( 28 | Department, related_name="my_courses" 29 | ) 30 | 31 | 32 | department = Department(name="Science") 33 | course = Course(name="Math", completed=False, department=department) 34 | 35 | print(department.my_courses[0]) 36 | # Will produce: 37 | # Course(id=None, 38 | # name='Math', 39 | # completed=False, 40 | # department=Department(id=None, name='Science')) 41 | -------------------------------------------------------------------------------- /docs_src/fields/docs003.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | 7 | database = databases.Database("sqlite:///db.sqlite") 8 | metadata = sqlalchemy.MetaData() 9 | 10 | 11 | class Department(ormar.Model): 12 | ormar_config = ormar.OrmarConfig( 13 | database=database, 14 | metadata=metadata, 15 | ) 16 | 17 | id: int = ormar.Integer(primary_key=True) 18 | name: str = ormar.String(max_length=100) 19 | 20 | 21 | class Course(ormar.Model): 22 | ormar_config = ormar.OrmarConfig( 23 | database=database, 24 | metadata=metadata, 25 | ) 26 | 27 | id: int = ormar.Integer(primary_key=True) 28 | name: str = ormar.String(max_length=100) 29 | completed: bool = ormar.Boolean(default=False) 30 | department: Optional[Department] = ormar.ForeignKey(Department) 31 | -------------------------------------------------------------------------------- /docs_src/fields/docs004.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | from sqlalchemy import func, text 7 | 8 | database = databases.Database("sqlite:///test.db") 9 | metadata = sqlalchemy.MetaData() 10 | 11 | 12 | class Product(ormar.Model): 13 | 14 | ormar_config = ormar.OrmarConfig( 15 | database=database, metadata=metadata, tablename="product" 16 | ) 17 | 18 | id: int = ormar.Integer(primary_key=True) 19 | name: str = ormar.String(max_length=100) 20 | company: str = ormar.String(max_length=200, server_default="Acme") 21 | sort_order: int = ormar.Integer(server_default=text("10")) 22 | created: datetime = ormar.DateTime(server_default=func.now()) 23 | -------------------------------------------------------------------------------- /docs_src/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/docs_src/models/__init__.py -------------------------------------------------------------------------------- /docs_src/models/docs001.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import sqlalchemy 4 | 5 | database = databases.Database("sqlite:///db.sqlite") 6 | metadata = sqlalchemy.MetaData() 7 | 8 | 9 | class Course(ormar.Model): 10 | ormar_config = ormar.OrmarConfig( 11 | database=database, 12 | metadata=metadata, 13 | ) 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | name: str = ormar.String(max_length=100) 17 | completed: bool = ormar.Boolean(default=False) 18 | -------------------------------------------------------------------------------- /docs_src/models/docs002.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import sqlalchemy 4 | 5 | database = databases.Database("sqlite:///db.sqlite") 6 | metadata = sqlalchemy.MetaData() 7 | 8 | 9 | class Course(ormar.Model): 10 | 11 | ormar_config = ormar.OrmarConfig( 12 | database=database, 13 | metadata=metadata, 14 | # if you omit this parameter it will be created automatically 15 | # as class.__name__.lower()+'s' -> "courses" in this example 16 | tablename="my_courses", 17 | ) 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | name: str = ormar.String(max_length=100) 21 | completed: bool = ormar.Boolean(default=False) 22 | -------------------------------------------------------------------------------- /docs_src/models/docs003.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import sqlalchemy 4 | 5 | database = databases.Database("sqlite:///db.sqlite") 6 | metadata = sqlalchemy.MetaData() 7 | 8 | 9 | class Course(ormar.Model): 10 | ormar_config = ormar.OrmarConfig( 11 | database=database, 12 | metadata=metadata, 13 | ) 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | name: str = ormar.String(max_length=100) 17 | completed: bool = ormar.Boolean(default=False) 18 | 19 | 20 | print(Course.model_fields) 21 | """ 22 | Will produce: 23 | {'id': Field(name='id', 24 | type=Optional[int], 25 | required=False, 26 | default=None), 27 | 'name': Field(name='name', 28 | type=Optional[str], 29 | required=False, 30 | default=None), 31 | 'completed': Field(name='completed', 32 | type=bool, 33 | required=False, 34 | default=False)} 35 | """ 36 | -------------------------------------------------------------------------------- /docs_src/models/docs004.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import sqlalchemy 4 | 5 | DATABASE_URL = "sqlite:///test.db" 6 | 7 | ormar_base_config = ormar.OrmarConfig( 8 | database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() 9 | ) 10 | 11 | 12 | class Course(ormar.Model): 13 | ormar_config = ormar.OrmarConfig( 14 | tablename="courses", 15 | database=databases.Database(DATABASE_URL), 16 | metadata=sqlalchemy.MetaData(), 17 | ) 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | name: str = ormar.String(max_length=100) 21 | completed: bool = ormar.Boolean(default=False) 22 | 23 | 24 | print(Course.ormar_config.table.columns) 25 | """ 26 | Will produce: 27 | ImmutableColumnCollection(courses.id, courses.name, courses.completed) 28 | """ 29 | -------------------------------------------------------------------------------- /docs_src/models/docs006.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import sqlalchemy 4 | 5 | database = databases.Database("sqlite:///db.sqlite") 6 | metadata = sqlalchemy.MetaData() 7 | 8 | 9 | class Course(ormar.Model): 10 | ormar_config = ormar.OrmarConfig( 11 | database=database, 12 | metadata=metadata, 13 | # define your constraints in OrmarConfig of the model 14 | # it's a list that can contain multiple constraints 15 | # hera a combination of name and column will have to be unique in db 16 | constraints=[ormar.UniqueColumns("name", "completed")], 17 | ) 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | name: str = ormar.String(max_length=100) 21 | completed: bool = ormar.Boolean(default=False) 22 | -------------------------------------------------------------------------------- /docs_src/models/docs007.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | from examples import create_drop_database 7 | 8 | DATABASE_URL = "sqlite:///test.db" 9 | 10 | ormar_base_config = ormar.OrmarConfig( 11 | database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() 12 | ) 13 | 14 | 15 | class Course(ormar.Model): 16 | ormar_config = ormar_base_config.copy(tablename="courses") 17 | 18 | id: int = ormar.Integer(primary_key=True) 19 | name: str = ormar.String(max_length=100) 20 | completed: bool = ormar.Boolean(default=False) 21 | 22 | 23 | @create_drop_database(base_config=ormar_base_config) 24 | async def run_query(): 25 | course = Course(name="Painting for dummies", completed=False) 26 | await course.save() 27 | 28 | await Course.objects.create(name="Painting for dummies", completed=False) 29 | 30 | 31 | asyncio.run(run_query()) 32 | -------------------------------------------------------------------------------- /docs_src/models/docs008.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import sqlalchemy 4 | 5 | DATABASE_URl = "sqlite:///test.db" 6 | 7 | database = databases.Database(DATABASE_URl, force_rollback=True) 8 | metadata = sqlalchemy.MetaData() 9 | 10 | 11 | class Child(ormar.Model): 12 | ormar_config = ormar.OrmarConfig( 13 | database=database, 14 | metadata=metadata, 15 | tablename="children", 16 | ) 17 | 18 | id: int = ormar.Integer(name="child_id", primary_key=True) 19 | first_name: str = ormar.String(name="fname", max_length=100) 20 | last_name: str = ormar.String(name="lname", max_length=100) 21 | born_year: int = ormar.Integer(name="year_born", nullable=True) 22 | -------------------------------------------------------------------------------- /docs_src/models/docs009.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | 7 | database = databases.Database("sqlite:///test.db", force_rollback=True) 8 | metadata = sqlalchemy.MetaData() 9 | 10 | 11 | class Artist(ormar.Model): 12 | ormar_config = ormar.OrmarConfig( 13 | database=database, 14 | metadata=metadata, 15 | tablename="artists", 16 | ) 17 | 18 | id: int = ormar.Integer(name="artist_id", primary_key=True) 19 | first_name: str = ormar.String(name="fname", max_length=100) 20 | last_name: str = ormar.String(name="lname", max_length=100) 21 | born_year: int = ormar.Integer(name="year") 22 | 23 | 24 | class Album(ormar.Model): 25 | 26 | ormar_config = ormar.OrmarConfig( 27 | database=database, 28 | metadata=metadata, 29 | tablename="music_albums", 30 | ) 31 | 32 | id: int = ormar.Integer(name="album_id", primary_key=True) 33 | name: str = ormar.String(name="album_name", max_length=100) 34 | artist: Optional[Artist] = ormar.ForeignKey(Artist, name="artist_id") 35 | -------------------------------------------------------------------------------- /docs_src/models/docs010.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import sqlalchemy 4 | 5 | DATABASE_URl = "sqlite:///test.db" 6 | 7 | database = databases.Database(DATABASE_URl, force_rollback=True) 8 | metadata = sqlalchemy.MetaData() 9 | 10 | 11 | class Child(ormar.Model): 12 | ormar_config = ormar.OrmarConfig( 13 | database=database, 14 | metadata=metadata, 15 | tablename="children", 16 | ) 17 | 18 | id: int = ormar.Integer(name="child_id", primary_key=True) 19 | first_name: str = ormar.String(name="fname", max_length=100) 20 | last_name: str = ormar.String(name="lname", max_length=100) 21 | born_year: int = ormar.Integer(name="year_born", nullable=True) 22 | 23 | 24 | class ArtistChildren(ormar.Model): 25 | ormar_config = ormar.OrmarConfig( 26 | database=database, 27 | metadata=metadata, 28 | tablename="children_x_artists", 29 | ) 30 | 31 | 32 | class Artist(ormar.Model): 33 | ormar_config = ormar.OrmarConfig( 34 | database=database, 35 | metadata=metadata, 36 | tablename="artists", 37 | ) 38 | 39 | id: int = ormar.Integer(name="artist_id", primary_key=True) 40 | first_name: str = ormar.String(name="fname", max_length=100) 41 | last_name: str = ormar.String(name="lname", max_length=100) 42 | born_year: int = ormar.Integer(name="year") 43 | children = ormar.ManyToMany(Child, through=ArtistChildren) 44 | -------------------------------------------------------------------------------- /docs_src/models/docs012.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import sqlalchemy 4 | 5 | database = databases.Database("sqlite:///db.sqlite") 6 | metadata = sqlalchemy.MetaData() 7 | 8 | 9 | class Course(ormar.Model): 10 | ormar_config = ormar.OrmarConfig( 11 | database=database, 12 | metadata=metadata, 13 | ) 14 | 15 | id = ormar.Integer(primary_key=True) 16 | name = ormar.String(max_length=100) 17 | completed = ormar.Boolean(default=False) 18 | -------------------------------------------------------------------------------- /docs_src/models/docs013.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | 7 | DATABASE_URL = "sqlite:///test.db" 8 | 9 | ormar_base_config = ormar.OrmarConfig( 10 | database=databases.Database(DATABASE_URL), 11 | metadata=sqlalchemy.MetaData(), 12 | ) 13 | 14 | 15 | class Artist(ormar.Model): 16 | # note that tablename is optional 17 | # if not provided ormar will user class.__name__.lower()+'s' 18 | # -> artists in this example 19 | ormar_config = ormar_base_config.copy() 20 | 21 | id: int = ormar.Integer(primary_key=True) 22 | first_name: str = ormar.String(max_length=100) 23 | last_name: str = ormar.String(max_length=100) 24 | born_year: int = ormar.Integer(name="year") 25 | 26 | 27 | class Album(ormar.Model): 28 | ormar_config = ormar_base_config.copy() 29 | 30 | id: int = ormar.Integer(primary_key=True) 31 | name: str = ormar.String(max_length=100) 32 | artist: Optional[Artist] = ormar.ForeignKey(Artist) 33 | -------------------------------------------------------------------------------- /docs_src/models/docs014.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import pydantic 4 | import sqlalchemy 5 | 6 | database = databases.Database("sqlite:///db.sqlite") 7 | metadata = sqlalchemy.MetaData() 8 | 9 | 10 | class Course(ormar.Model): 11 | ormar_config = ormar.OrmarConfig( 12 | database=database, 13 | metadata=metadata, 14 | ) 15 | 16 | id: int = ormar.Integer(primary_key=True) 17 | name: str = ormar.String(max_length=100) 18 | completed: bool = ormar.Boolean(default=False) 19 | non_db_field: str = pydantic.Field(max_length=100) 20 | -------------------------------------------------------------------------------- /docs_src/models/docs015.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import sqlalchemy 4 | 5 | database = databases.Database("sqlite:///db.sqlite") 6 | metadata = sqlalchemy.MetaData() 7 | 8 | 9 | class Course(ormar.Model): 10 | ormar_config = ormar.OrmarConfig( 11 | database=database, 12 | metadata=metadata, 13 | ) 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | name: str = ormar.String(max_length=100) 17 | completed: bool = ormar.Boolean(default=False) 18 | 19 | @property 20 | def prefixed_name(self): 21 | return "custom_prefix__" + self.name 22 | -------------------------------------------------------------------------------- /docs_src/models/docs016.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import pydantic 4 | import sqlalchemy 5 | 6 | database = databases.Database("sqlite:///db.sqlite") 7 | metadata = sqlalchemy.MetaData() 8 | 9 | 10 | class Course(ormar.Model): 11 | ormar_config = ormar.OrmarConfig( 12 | database=database, 13 | metadata=metadata, 14 | ) 15 | 16 | model_config = pydantic.ConfigDict(frozen=True) 17 | 18 | id: int = ormar.Integer(primary_key=True) 19 | name: str = ormar.String(max_length=100) 20 | completed: bool = ormar.Boolean(default=False) 21 | -------------------------------------------------------------------------------- /docs_src/models/docs017.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import sqlalchemy 4 | 5 | database = databases.Database("sqlite:///db.sqlite") 6 | metadata = sqlalchemy.MetaData() 7 | 8 | 9 | class Course(ormar.Model): 10 | ormar_config = ormar.OrmarConfig( 11 | database=database, 12 | metadata=metadata, 13 | # define your constraints in OrmarConfig of the model 14 | # it's a list that can contain multiple constraints 15 | # hera a combination of name and column will have a compound index in the db 16 | constraints=[ormar.IndexColumns("name", "completed")], 17 | ) 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | name: str = ormar.String(max_length=100) 21 | completed: bool = ormar.Boolean(default=False) 22 | -------------------------------------------------------------------------------- /docs_src/models/docs018.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | 7 | database = databases.Database("sqlite:///db.sqlite") 8 | metadata = sqlalchemy.MetaData() 9 | 10 | 11 | class Course(ormar.Model): 12 | ormar_config = ormar.OrmarConfig( 13 | database=database, 14 | metadata=metadata, 15 | # define your constraints in OrmarConfig of the model 16 | # it's a list that can contain multiple constraints 17 | # hera a combination of name and column will have a level check in the db 18 | constraints=[ 19 | ormar.CheckColumns("start_time < end_time", name="date_check"), 20 | ], 21 | ) 22 | 23 | id: int = ormar.Integer(primary_key=True) 24 | name: str = ormar.String(max_length=100) 25 | start_date: datetime.date = ormar.Date() 26 | end_date: datetime.date = ormar.Date() 27 | -------------------------------------------------------------------------------- /docs_src/queries/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/docs_src/queries/__init__.py -------------------------------------------------------------------------------- /docs_src/queries/docs001.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | 7 | DATABASE_URL = "sqlite:///test.db" 8 | 9 | ormar_base_config = ormar.OrmarConfig( 10 | database=databases.Database(DATABASE_URL), 11 | metadata=sqlalchemy.MetaData(), 12 | ) 13 | 14 | 15 | class Album(ormar.Model): 16 | ormar_config = ormar_base_config.copy(tablename="album") 17 | 18 | id: int = ormar.Integer(primary_key=True) 19 | name: str = ormar.String(max_length=100) 20 | 21 | 22 | class Track(ormar.Model): 23 | ormar_config = ormar_base_config.copy(tablename="track") 24 | 25 | id: int = ormar.Integer(primary_key=True) 26 | album: Optional[Album] = ormar.ForeignKey(Album) 27 | title: str = ormar.String(max_length=100) 28 | position: int = ormar.Integer() 29 | -------------------------------------------------------------------------------- /docs_src/queries/docs002.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | from examples import create_drop_database 7 | 8 | DATABASE_URL = "sqlite:///test.db" 9 | 10 | ormar_base_config = ormar.OrmarConfig( 11 | database=databases.Database(DATABASE_URL), 12 | metadata=sqlalchemy.MetaData(), 13 | ) 14 | 15 | 16 | class Book(ormar.Model): 17 | ormar_config = ormar_base_config.copy( 18 | tablename="books", 19 | ) 20 | 21 | id: int = ormar.Integer(primary_key=True) 22 | title: str = ormar.String(max_length=200) 23 | author: str = ormar.String(max_length=100) 24 | genre: str = ormar.String( 25 | max_length=100, 26 | default="Fiction", 27 | ) 28 | 29 | 30 | @create_drop_database(base_config=ormar_base_config) 31 | async def run_query(): 32 | await Book.objects.create( 33 | title="Tom Sawyer", author="Twain, Mark", genre="Adventure" 34 | ) 35 | await Book.objects.create( 36 | title="War and Peace", author="Tolstoy, Leo", genre="Fiction" 37 | ) 38 | await Book.objects.create( 39 | title="Anna Karenina", author="Tolstoy, Leo", genre="Fiction" 40 | ) 41 | 42 | await Book.objects.update(each=True, genre="Fiction") 43 | all_books = await Book.objects.filter(genre="Fiction").all() 44 | assert len(all_books) == 3 45 | 46 | 47 | asyncio.run(run_query()) 48 | -------------------------------------------------------------------------------- /docs_src/queries/docs003.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | from examples import create_drop_database 7 | 8 | DATABASE_URL = "sqlite:///test.db" 9 | 10 | ormar_base_config = ormar.OrmarConfig( 11 | database=databases.Database(DATABASE_URL), 12 | metadata=sqlalchemy.MetaData(), 13 | ) 14 | 15 | 16 | class Book(ormar.Model): 17 | ormar_config = ormar_base_config.copy() 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | title: str = ormar.String(max_length=200) 21 | author: str = ormar.String(max_length=100) 22 | genre: str = ormar.String( 23 | max_length=100, 24 | default="Fiction", 25 | ) 26 | 27 | 28 | @create_drop_database(base_config=ormar_base_config) 29 | async def run_query(): 30 | await Book.objects.create( 31 | title="Tom Sawyer", author="Twain, Mark", genre="Adventure" 32 | ) 33 | await Book.objects.create( 34 | title="War and Peace", author="Tolstoy, Leo", genre="Fiction" 35 | ) 36 | await Book.objects.create( 37 | title="Anna Karenina", author="Tolstoy, Leo", genre="Fiction" 38 | ) 39 | 40 | # if not exist the instance will be persisted in db 41 | vol2 = await Book.objects.update_or_create( 42 | title="Volume II", author="Anonymous", genre="Fiction" 43 | ) 44 | assert await Book.objects.count() == 4 45 | 46 | # if pk or pkname passed in kwargs (like id here) the object will be updated 47 | assert await Book.objects.update_or_create(id=vol2.id, genre="Historic") 48 | assert await Book.objects.count() == 4 49 | 50 | 51 | asyncio.run(run_query()) 52 | -------------------------------------------------------------------------------- /docs_src/queries/docs004.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | from examples import create_drop_database 7 | 8 | DATABASE_URL = "sqlite:///test.db" 9 | 10 | ormar_base_config = ormar.OrmarConfig( 11 | database=databases.Database(DATABASE_URL), 12 | metadata=sqlalchemy.MetaData(), 13 | ) 14 | 15 | 16 | class ToDo(ormar.Model): 17 | ormar_config = ormar_base_config.copy(tablename="todos") 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | text: str = ormar.String(max_length=500) 21 | completed = ormar.Boolean(default=False) 22 | 23 | 24 | @create_drop_database(base_config=ormar_base_config) 25 | async def run_query(): 26 | # create multiple instances at once with bulk_create 27 | await ToDo.objects.bulk_create( 28 | [ 29 | ToDo(text="Buy the groceries."), 30 | ToDo(text="Call Mum.", completed=True), 31 | ToDo(text="Send invoices.", completed=True), 32 | ] 33 | ) 34 | 35 | todoes = await ToDo.objects.all() 36 | assert len(todoes) == 3 37 | 38 | 39 | asyncio.run(run_query()) 40 | -------------------------------------------------------------------------------- /docs_src/queries/docs005.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | from examples import create_drop_database 7 | 8 | DATABASE_URL = "sqlite:///test.db" 9 | 10 | ormar_base_config = ormar.OrmarConfig( 11 | database=databases.Database(DATABASE_URL), 12 | metadata=sqlalchemy.MetaData(), 13 | ) 14 | 15 | 16 | class Book(ormar.Model): 17 | ormar_config = ormar_base_config.copy(tablename="books") 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | title: str = ormar.String(max_length=200) 21 | author: str = ormar.String(max_length=100) 22 | genre: str = ormar.String( 23 | max_length=100, 24 | default="Fiction", 25 | ) 26 | 27 | 28 | @create_drop_database(base_config=ormar_base_config) 29 | async def run_query(): 30 | await Book.objects.create( 31 | title="Tom Sawyer", author="Twain, Mark", genre="Adventure" 32 | ) 33 | await Book.objects.create( 34 | title="War and Peace in Space", author="Tolstoy, Leo", genre="Fantasy" 35 | ) 36 | await Book.objects.create( 37 | title="Anna Karenina", author="Tolstoy, Leo", genre="Fiction" 38 | ) 39 | 40 | # delete accepts kwargs that will be used in filter 41 | # acting in same way as queryset.filter(**kwargs).delete() 42 | await Book.objects.delete(genre="Fantasy") # delete all fantasy books 43 | all_books = await Book.objects.all() 44 | assert len(all_books) == 2 45 | 46 | 47 | asyncio.run(run_query()) 48 | -------------------------------------------------------------------------------- /docs_src/queries/docs006.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | from examples import create_drop_database 7 | 8 | DATABASE_URL = "sqlite:///test.db" 9 | 10 | ormar_base_config = ormar.OrmarConfig( 11 | database=databases.Database(DATABASE_URL), 12 | metadata=sqlalchemy.MetaData(), 13 | ) 14 | 15 | 16 | class Company(ormar.Model): 17 | ormar_config = ormar_base_config.copy(tablename="companies") 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | name: str = ormar.String(max_length=100) 21 | founded: int = ormar.Integer(nullable=True) 22 | 23 | 24 | class Car(ormar.Model): 25 | ormar_config = ormar_base_config.copy(tablename="cars") 26 | 27 | id: int = ormar.Integer(primary_key=True) 28 | manufacturer = ormar.ForeignKey(Company) 29 | name: str = ormar.String(max_length=100) 30 | year: int = ormar.Integer(nullable=True) 31 | gearbox_type: str = ormar.String(max_length=20, nullable=True) 32 | gears: int = ormar.Integer(nullable=True) 33 | aircon_type: str = ormar.String(max_length=20, nullable=True) 34 | 35 | 36 | @create_drop_database(base_config=ormar_base_config) 37 | async def run_query(): 38 | # build some sample data 39 | toyota = await Company.objects.create(name="Toyota", founded=1937) 40 | await Car.objects.create( 41 | manufacturer=toyota, 42 | name="Corolla", 43 | year=2020, 44 | gearbox_type="Manual", 45 | gears=5, 46 | aircon_type="Manual", 47 | ) 48 | await Car.objects.create( 49 | manufacturer=toyota, 50 | name="Yaris", 51 | year=2019, 52 | gearbox_type="Manual", 53 | gears=5, 54 | aircon_type="Manual", 55 | ) 56 | await Car.objects.create( 57 | manufacturer=toyota, 58 | name="Supreme", 59 | year=2020, 60 | gearbox_type="Auto", 61 | gears=6, 62 | aircon_type="Auto", 63 | ) 64 | 65 | 66 | asyncio.run(run_query()) 67 | -------------------------------------------------------------------------------- /docs_src/queries/docs007.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | from examples import create_drop_database 7 | 8 | DATABASE_URL = "sqlite:///test.db" 9 | 10 | ormar_base_config = ormar.OrmarConfig( 11 | database=databases.Database(DATABASE_URL), 12 | metadata=sqlalchemy.MetaData(), 13 | ) 14 | 15 | 16 | class Owner(ormar.Model): 17 | ormar_config = ormar_base_config.copy(tablename="owners") 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | name: str = ormar.String(max_length=100) 21 | 22 | 23 | class Toy(ormar.Model): 24 | ormar_config = ormar_base_config.copy(tablename="toys") 25 | 26 | id: int = ormar.Integer(primary_key=True) 27 | name: str = ormar.String(max_length=100) 28 | owner: Owner = ormar.ForeignKey(Owner) 29 | 30 | 31 | @create_drop_database(base_config=ormar_base_config) 32 | async def run_query(): 33 | # build some sample data 34 | aphrodite = await Owner.objects.create(name="Aphrodite") 35 | hermes = await Owner.objects.create(name="Hermes") 36 | zeus = await Owner.objects.create(name="Zeus") 37 | 38 | await Toy.objects.create(name="Toy 4", owner=zeus) 39 | await Toy.objects.create(name="Toy 5", owner=hermes) 40 | await Toy.objects.create(name="Toy 2", owner=aphrodite) 41 | await Toy.objects.create(name="Toy 1", owner=zeus) 42 | await Toy.objects.create(name="Toy 3", owner=aphrodite) 43 | await Toy.objects.create(name="Toy 6", owner=hermes) 44 | 45 | 46 | asyncio.run(run_query()) 47 | -------------------------------------------------------------------------------- /docs_src/queries/docs009.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | from examples import create_drop_database 7 | 8 | DATABASE_URL = "sqlite:///test.db" 9 | 10 | ormar_base_config = ormar.OrmarConfig( 11 | database=databases.Database(DATABASE_URL), 12 | metadata=sqlalchemy.MetaData(), 13 | ) 14 | 15 | 16 | class Company(ormar.Model): 17 | ormar_config = ormar_base_config.copy(tablename="companies") 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | name: str = ormar.String(max_length=100) 21 | founded: int = ormar.Integer(nullable=True) 22 | 23 | 24 | class Car(ormar.Model): 25 | ormar_config = ormar_base_config.copy(tablename="cars") 26 | 27 | id: int = ormar.Integer(primary_key=True) 28 | manufacturer = ormar.ForeignKey(Company) 29 | name: str = ormar.String(max_length=100) 30 | year: int = ormar.Integer(nullable=True) 31 | gearbox_type: str = ormar.String(max_length=20, nullable=True) 32 | gears: int = ormar.Integer(nullable=True) 33 | aircon_type: str = ormar.String(max_length=20, nullable=True) 34 | 35 | 36 | @create_drop_database(base_config=ormar_base_config) 37 | async def run_query(): 38 | # 1. like in example above 39 | await Car.objects.select_related("manufacturer").fields( 40 | ["id", "name", "manufacturer__name"] 41 | ).all() 42 | 43 | # 2. to mark a field as required use ellipsis 44 | await Car.objects.select_related("manufacturer").fields( 45 | {"id": ..., "name": ..., "manufacturer": {"name": ...}} 46 | ).all() 47 | 48 | # 3. to include whole nested model use ellipsis 49 | await Car.objects.select_related("manufacturer").fields( 50 | {"id": ..., "name": ..., "manufacturer": ...} 51 | ).all() 52 | 53 | # 4. to specify fields at last nesting level you can also use set 54 | # - equivalent to 2. above 55 | await Car.objects.select_related("manufacturer").fields( 56 | {"id": ..., "name": ..., "manufacturer": {"name"}} 57 | ).all() 58 | 59 | # 5. of course set can have multiple fields 60 | await Car.objects.select_related("manufacturer").fields( 61 | {"id": ..., "name": ..., "manufacturer": {"name", "founded"}} 62 | ).all() 63 | 64 | # 6. you can include all nested fields, 65 | # but it will be equivalent of 3. above which is shorter 66 | await Car.objects.select_related("manufacturer").fields( 67 | {"id": ..., "name": ..., "manufacturer": {"id", "name", "founded"}} 68 | ).all() 69 | 70 | 71 | asyncio.run(run_query()) 72 | -------------------------------------------------------------------------------- /docs_src/relations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/docs_src/relations/__init__.py -------------------------------------------------------------------------------- /docs_src/relations/docs001.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional, Union 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | 7 | database = databases.Database("sqlite:///db.sqlite") 8 | metadata = sqlalchemy.MetaData() 9 | 10 | 11 | class Department(ormar.Model): 12 | ormar_config = ormar.OrmarConfig( 13 | database=database, 14 | metadata=metadata, 15 | ) 16 | 17 | id: int = ormar.Integer(primary_key=True) 18 | name: str = ormar.String(max_length=100) 19 | 20 | 21 | class Course(ormar.Model): 22 | ormar_config = ormar.OrmarConfig( 23 | database=database, 24 | metadata=metadata, 25 | ) 26 | 27 | id: int = ormar.Integer(primary_key=True) 28 | name: str = ormar.String(max_length=100) 29 | completed: bool = ormar.Boolean(default=False) 30 | department: Optional[Union[Department, Dict]] = ormar.ForeignKey(Department) 31 | 32 | 33 | department = Department(name="Science") 34 | 35 | # set up a relation with actual Model instance 36 | course = Course(name="Math", completed=False, department=department) 37 | 38 | # set up relation with only related model pk value 39 | course2 = Course(name="Math II", completed=False, department=department.pk) 40 | 41 | # set up a relation with dictionary corresponding to related model 42 | course3 = Course(name="Math III", completed=False, department=department.model_dump()) 43 | 44 | # explicitly set up None 45 | course4 = Course(name="Math III", completed=False, department=None) 46 | -------------------------------------------------------------------------------- /docs_src/relations/docs002.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | 7 | DATABASE_URL = "sqlite:///test.db" 8 | 9 | ormar_base_config = ormar.OrmarConfig( 10 | database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() 11 | ) 12 | 13 | 14 | class Author(ormar.Model): 15 | ormar_config = ormar_base_config.copy(tablename="authors") 16 | 17 | id: int = ormar.Integer(primary_key=True) 18 | first_name: str = ormar.String(max_length=80) 19 | last_name: str = ormar.String(max_length=80) 20 | 21 | 22 | class Category(ormar.Model): 23 | ormar_config = ormar_base_config.copy(tablename="categories") 24 | 25 | id: int = ormar.Integer(primary_key=True) 26 | name: str = ormar.String(max_length=40) 27 | 28 | 29 | class Post(ormar.Model): 30 | ormar_config = ormar_base_config.copy(tablename="posts") 31 | 32 | id: int = ormar.Integer(primary_key=True) 33 | title: str = ormar.String(max_length=200) 34 | categories: Optional[List[Category]] = ormar.ManyToMany(Category) 35 | author: Optional[Author] = ormar.ForeignKey(Author) 36 | -------------------------------------------------------------------------------- /docs_src/relations/docs003.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional, Union 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | 7 | DATABASE_URL = "sqlite:///test.db" 8 | 9 | ormar_base_config = ormar.OrmarConfig( 10 | database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() 11 | ) 12 | 13 | 14 | class Department(ormar.Model): 15 | ormar_config = ormar_base_config.copy() 16 | 17 | id: int = ormar.Integer(primary_key=True) 18 | name: str = ormar.String(max_length=100) 19 | 20 | 21 | class Course(ormar.Model): 22 | ormar_config = ormar_base_config.copy() 23 | 24 | id: int = ormar.Integer(primary_key=True) 25 | name: str = ormar.String(max_length=100) 26 | department: Optional[Union[Department, Dict]] = ormar.ForeignKey(Department) 27 | -------------------------------------------------------------------------------- /docs_src/relations/docs004.py: -------------------------------------------------------------------------------- 1 | import databases 2 | import ormar 3 | import sqlalchemy 4 | 5 | DATABASE_URL = "sqlite:///test.db" 6 | 7 | ormar_base_config = ormar.OrmarConfig( 8 | database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() 9 | ) 10 | 11 | 12 | class Category(ormar.Model): 13 | ormar_config = ormar_base_config.copy(tablename="categories") 14 | 15 | id = ormar.Integer(primary_key=True) 16 | name = ormar.String(max_length=40) 17 | 18 | 19 | class PostCategory(ormar.Model): 20 | ormar_config = ormar_base_config.copy(tablename="posts_x_categories") 21 | 22 | id: int = ormar.Integer(primary_key=True) 23 | sort_order: int = ormar.Integer(nullable=True) 24 | param_name: str = ormar.String(default="Name", max_length=200) 25 | 26 | 27 | class Post(ormar.Model): 28 | ormar_config = ormar_base_config.copy() 29 | 30 | id: int = ormar.Integer(primary_key=True) 31 | title: str = ormar.String(max_length=200) 32 | categories = ormar.ManyToMany(Category, through=PostCategory) 33 | -------------------------------------------------------------------------------- /docs_src/select_columns/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/docs_src/select_columns/__init__.py -------------------------------------------------------------------------------- /docs_src/select_columns/docs001.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | from examples import create_drop_database 7 | from tests.settings import DATABASE_URL 8 | 9 | base_ormar_config = ormar.OrmarConfig( 10 | database=databases.Database(DATABASE_URL, force_rollback=True), 11 | metadata=sqlalchemy.MetaData(), 12 | ) 13 | 14 | 15 | class Company(ormar.Model): 16 | ormar_config = base_ormar_config.copy(tablename="companies") 17 | 18 | id: int = ormar.Integer(primary_key=True) 19 | name: str = ormar.String(max_length=100) 20 | founded: int = ormar.Integer(nullable=True) 21 | 22 | 23 | class Car(ormar.Model): 24 | ormar_config = base_ormar_config.copy() 25 | 26 | id: int = ormar.Integer(primary_key=True) 27 | manufacturer = ormar.ForeignKey(Company) 28 | name: str = ormar.String(max_length=100) 29 | year: int = ormar.Integer(nullable=True) 30 | gearbox_type: str = ormar.String(max_length=20, nullable=True) 31 | gears: int = ormar.Integer(nullable=True) 32 | aircon_type: str = ormar.String(max_length=20, nullable=True) 33 | 34 | 35 | @create_drop_database(base_config=base_ormar_config) 36 | async def sample_data(): 37 | # build some sample data 38 | toyota = await Company.objects.create(name="Toyota", founded=1937) 39 | await Car.objects.create( 40 | manufacturer=toyota, 41 | name="Corolla", 42 | year=2020, 43 | gearbox_type="Manual", 44 | gears=5, 45 | aircon_type="Manual", 46 | ) 47 | await Car.objects.create( 48 | manufacturer=toyota, 49 | name="Yaris", 50 | year=2019, 51 | gearbox_type="Manual", 52 | gears=5, 53 | aircon_type="Manual", 54 | ) 55 | await Car.objects.create( 56 | manufacturer=toyota, 57 | name="Supreme", 58 | year=2020, 59 | gearbox_type="Auto", 60 | gears=6, 61 | aircon_type="Auto", 62 | ) 63 | 64 | 65 | asyncio.run(sample_data()) 66 | -------------------------------------------------------------------------------- /docs_src/signals/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/docs_src/signals/__init__.py -------------------------------------------------------------------------------- /docs_src/signals/docs002.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | from examples import create_drop_database 7 | from ormar import pre_update 8 | 9 | DATABASE_URL = "sqlite:///test.db" 10 | 11 | ormar_base_config = ormar.OrmarConfig( 12 | database=databases.Database(DATABASE_URL), 13 | metadata=sqlalchemy.MetaData(), 14 | ) 15 | 16 | 17 | class Album(ormar.Model): 18 | ormar_config = ormar_base_config.copy( 19 | tablename="albums", 20 | ) 21 | 22 | id: int = ormar.Integer(primary_key=True) 23 | name: str = ormar.String(max_length=100) 24 | is_best_seller: bool = ormar.Boolean(default=False) 25 | play_count: int = ormar.Integer(default=0) 26 | 27 | 28 | @pre_update(Album) 29 | async def before_update(sender, instance, **kwargs): 30 | if instance.play_count > 50 and not instance.is_best_seller: 31 | instance.is_best_seller = True 32 | 33 | 34 | @create_drop_database(base_config=ormar_base_config) 35 | async def run_query(): 36 | # here album.play_count ans is_best_seller get default values 37 | album = await Album.objects.create(name="Venice") 38 | assert not album.is_best_seller 39 | assert album.play_count == 0 40 | 41 | album.play_count = 30 42 | # here a trigger is called but play_count is too low 43 | await album.update() 44 | assert not album.is_best_seller 45 | 46 | album.play_count = 60 47 | await album.update() 48 | assert album.is_best_seller 49 | 50 | 51 | asyncio.run(run_query()) 52 | -------------------------------------------------------------------------------- /docs_src/test_all_docs.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | filepaths = [] 8 | path = Path(__file__).parent 9 | for p in path.rglob("*"): 10 | print(p.name) 11 | for p in path.rglob("*"): 12 | if p.name.endswith(".py") and not p.name == "__init__.py" and p != Path(__file__): 13 | filepath_ = str(p.resolve()) 14 | filepaths.append(filepath_) 15 | 16 | 17 | @pytest.mark.parametrize("filepath", filepaths) 18 | def test_all_docs(filepath: str): 19 | result = subprocess.run([sys.executable, filepath]) 20 | assert result.returncode == 0 21 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import create_drop_database 2 | 3 | __all__ = ["create_drop_database"] 4 | -------------------------------------------------------------------------------- /examples/fastapi_quick_start.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from typing import List, Optional 3 | 4 | import databases 5 | import ormar 6 | import sqlalchemy 7 | import uvicorn 8 | from fastapi import FastAPI 9 | 10 | DATABASE_URL = "sqlite:///test.db" 11 | 12 | ormar_base_config = ormar.OrmarConfig( 13 | database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData() 14 | ) 15 | 16 | 17 | @asynccontextmanager 18 | async def lifespan(app: FastAPI): 19 | database_ = app.state.database 20 | if not database_.is_connected: 21 | await database_.connect() 22 | yield 23 | database_ = app.state.database 24 | if database_.is_connected: 25 | await database_.disconnect() 26 | 27 | 28 | app = FastAPI(lifespan=lifespan) 29 | metadata = sqlalchemy.MetaData() 30 | database = databases.Database("sqlite:///test.db") 31 | app.state.database = database 32 | 33 | 34 | class Category(ormar.Model): 35 | ormar_config = ormar_base_config.copy(tablename="categories") 36 | 37 | id: int = ormar.Integer(primary_key=True) 38 | name: str = ormar.String(max_length=100) 39 | 40 | 41 | class Item(ormar.Model): 42 | ormar_config = ormar_base_config.copy(tablename="items") 43 | 44 | id: int = ormar.Integer(primary_key=True) 45 | name: str = ormar.String(max_length=100) 46 | category: Optional[Category] = ormar.ForeignKey(Category, nullable=True) 47 | 48 | 49 | @app.get("/items/", response_model=List[Item]) 50 | async def get_items(): 51 | items = await Item.objects.select_related("category").all() 52 | return items 53 | 54 | 55 | @app.post("/items/", response_model=Item) 56 | async def create_item(item: Item): 57 | await item.save() 58 | return item 59 | 60 | 61 | @app.post("/categories/", response_model=Category) 62 | async def create_category(category: Category): 63 | await category.save() 64 | return category 65 | 66 | 67 | @app.put("/items/{item_id}") 68 | async def get_item(item_id: int, item: Item): 69 | item_db = await Item.objects.get(pk=item_id) 70 | return await item_db.update(**item.model_dump()) 71 | 72 | 73 | @app.delete("/items/{item_id}") 74 | async def delete_item(item_id: int, item: Item = None): 75 | if item: 76 | return {"deleted_rows": await item.delete()} 77 | item_db = await Item.objects.get(pk=item_id) 78 | return {"deleted_rows": await item_db.delete()} 79 | 80 | 81 | if __name__ == "__main__": 82 | # to play with API run the script and visit http://127.0.0.1:8000/docs 83 | uvicorn.run(app, host="127.0.0.1", port=8000) 84 | -------------------------------------------------------------------------------- /examples/utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | import ormar 4 | import sqlalchemy 5 | 6 | 7 | def create_drop_database(base_config: ormar.OrmarConfig) -> None: 8 | # create all tables in the database before execution 9 | # and drop them after, note that in production you should use migrations 10 | def wrapper(func): 11 | @functools.wraps(func) 12 | async def wrapped(*args): 13 | engine = sqlalchemy.create_engine(str(base_config.database.url)) 14 | base_config.metadata.drop_all(engine) 15 | base_config.metadata.create_all(engine) 16 | await func(*args) 17 | base_config.metadata.drop_all(engine) 18 | 19 | return wrapped 20 | 21 | return wrapper 22 | -------------------------------------------------------------------------------- /ormar/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module with all decorators that are exposed for users. 3 | 4 | Currently only: 5 | 6 | * predefined signals decorators (pre/post + save/update/delete) 7 | 8 | """ 9 | 10 | from ormar.decorators.signals import ( 11 | post_bulk_update, 12 | post_delete, 13 | post_relation_add, 14 | post_relation_remove, 15 | post_save, 16 | post_update, 17 | pre_delete, 18 | pre_relation_add, 19 | pre_relation_remove, 20 | pre_save, 21 | pre_update, 22 | ) 23 | 24 | __all__ = [ 25 | "post_bulk_update", 26 | "post_delete", 27 | "post_save", 28 | "post_update", 29 | "pre_delete", 30 | "pre_save", 31 | "pre_update", 32 | "post_relation_remove", 33 | "post_relation_add", 34 | "pre_relation_remove", 35 | "pre_relation_add", 36 | ] 37 | -------------------------------------------------------------------------------- /ormar/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Gathers all exceptions thrown by ormar. 3 | """ 4 | 5 | 6 | class AsyncOrmException(Exception): 7 | """ 8 | Base ormar Exception 9 | """ 10 | 11 | pass 12 | 13 | 14 | class ModelDefinitionError(AsyncOrmException): 15 | """ 16 | Raised for errors related to the model definition itself: 17 | 18 | * defining a Field without required parameters 19 | * defining a model with more than one primary_key 20 | * defining a model without primary_key 21 | """ 22 | 23 | pass 24 | 25 | 26 | class ModelError(AsyncOrmException): 27 | """ 28 | Raised for initialization of model with non-existing field keyword. 29 | """ 30 | 31 | pass 32 | 33 | 34 | class NoMatch(AsyncOrmException): 35 | """ 36 | Raised for database queries that has no matching result (empty result). 37 | """ 38 | 39 | pass 40 | 41 | 42 | class MultipleMatches(AsyncOrmException): 43 | """ 44 | Raised for database queries that should return one row (i.e. get, first etc.) 45 | but has multiple matching results in response. 46 | """ 47 | 48 | pass 49 | 50 | 51 | class QueryDefinitionError(AsyncOrmException): 52 | """ 53 | Raised for errors in query definition: 54 | 55 | * using contains or icontains filter with instance of the Model 56 | * using Queryset.update() without filter and setting each flag to True 57 | * using Queryset.delete() without filter and setting each flag to True 58 | """ 59 | 60 | pass 61 | 62 | 63 | class RelationshipInstanceError(AsyncOrmException): 64 | pass 65 | 66 | 67 | class ModelPersistenceError(AsyncOrmException): 68 | """ 69 | Raised for update of models without primary_key set (cannot retrieve from db) 70 | or for saving a model with relation to unsaved model (cannot extract fk value). 71 | """ 72 | 73 | pass 74 | 75 | 76 | class SignalDefinitionError(AsyncOrmException): 77 | """ 78 | Raised when non callable receiver is passed as signal callback. 79 | """ 80 | 81 | pass 82 | 83 | 84 | class ModelListEmptyError(AsyncOrmException): 85 | """ 86 | Raised for objects is empty when bulk_update 87 | """ 88 | 89 | pass 90 | -------------------------------------------------------------------------------- /ormar/fields/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module with classes and constructors for ormar Fields. 3 | Base Fields types (like String, Integer etc.) 4 | as well as relation Fields (ForeignKey, ManyToMany). 5 | Also a definition for custom CHAR based sqlalchemy UUID field 6 | """ 7 | 8 | from ormar.fields.base import BaseField 9 | from ormar.fields.constraints import CheckColumns, IndexColumns, UniqueColumns 10 | from ormar.fields.foreign_key import ForeignKey, ForeignKeyField 11 | from ormar.fields.many_to_many import ManyToMany, ManyToManyField 12 | from ormar.fields.model_fields import ( 13 | JSON, 14 | UUID, 15 | BigInteger, 16 | Boolean, 17 | Date, 18 | DateTime, 19 | Decimal, 20 | Enum, 21 | Float, 22 | Integer, 23 | LargeBinary, 24 | SmallInteger, 25 | String, 26 | Text, 27 | Time, 28 | ) 29 | from ormar.fields.parsers import DECODERS_MAP, ENCODERS_MAP, SQL_ENCODERS_MAP 30 | from ormar.fields.referential_actions import ReferentialAction 31 | from ormar.fields.sqlalchemy_encrypted import EncryptBackend, EncryptBackends 32 | from ormar.fields.through_field import Through, ThroughField 33 | 34 | __all__ = [ 35 | "Decimal", 36 | "BigInteger", 37 | "SmallInteger", 38 | "Boolean", 39 | "Date", 40 | "DateTime", 41 | "String", 42 | "JSON", 43 | "Integer", 44 | "Text", 45 | "Float", 46 | "Time", 47 | "UUID", 48 | "Enum", 49 | "ForeignKey", 50 | "ManyToMany", 51 | "ManyToManyField", 52 | "BaseField", 53 | "ForeignKeyField", 54 | "ThroughField", 55 | "Through", 56 | "EncryptBackends", 57 | "EncryptBackend", 58 | "DECODERS_MAP", 59 | "ENCODERS_MAP", 60 | "SQL_ENCODERS_MAP", 61 | "LargeBinary", 62 | "UniqueColumns", 63 | "IndexColumns", 64 | "CheckColumns", 65 | "ReferentialAction", 66 | ] 67 | -------------------------------------------------------------------------------- /ormar/fields/constraints.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | from sqlalchemy import CheckConstraint, Index, UniqueConstraint 4 | 5 | 6 | class UniqueColumns(UniqueConstraint): 7 | """ 8 | Subclass of sqlalchemy.UniqueConstraint. 9 | Used to avoid importing anything from sqlalchemy by user. 10 | """ 11 | 12 | 13 | class IndexColumns(Index): 14 | def __init__(self, *args: Any, name: Optional[str] = None, **kw: Any) -> None: 15 | if not name: 16 | name = "TEMPORARY_NAME" 17 | super().__init__(name, *args, **kw) 18 | 19 | """ 20 | Subclass of sqlalchemy.Index. 21 | Used to avoid importing anything from sqlalchemy by user. 22 | """ 23 | 24 | 25 | class CheckColumns(CheckConstraint): 26 | """ 27 | Subclass of sqlalchemy.CheckConstraint. 28 | Used to avoid importing anything from sqlalchemy by user. 29 | 30 | Note that some databases do not actively support check constraints such as MySQL. 31 | """ 32 | -------------------------------------------------------------------------------- /ormar/fields/referential_actions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Gathers all referential actions by ormar. 3 | """ 4 | 5 | from enum import Enum 6 | 7 | 8 | class ReferentialAction(Enum): 9 | """ 10 | Because the database management system(DBMS) enforces referential constraints, 11 | it must ensure data integrity 12 | if rows in a referenced table are to be deleted (or updated). 13 | 14 | If dependent rows in referencing tables still exist, 15 | those references have to be considered. 16 | 17 | SQL specifies 5 different referential actions 18 | that shall take place in such occurrences. 19 | """ 20 | 21 | CASCADE: str = "CASCADE" 22 | RESTRICT: str = "RESTRICT" 23 | SET_NULL: str = "SET NULL" 24 | SET_DEFAULT: str = "SET DEFAULT" 25 | DO_NOTHING: str = "NO ACTION" 26 | -------------------------------------------------------------------------------- /ormar/fields/sqlalchemy_uuid.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Any, Optional 3 | 4 | from sqlalchemy import CHAR 5 | from sqlalchemy.engine import Dialect 6 | from sqlalchemy.types import TypeDecorator 7 | 8 | 9 | class UUID(TypeDecorator): 10 | """ 11 | Platform-independent GUID type. 12 | Uses CHAR(36) if in a string mode, otherwise uses CHAR(32), to store UUID. 13 | 14 | For details for different methods check documentation of parent class. 15 | """ 16 | 17 | impl = CHAR 18 | 19 | def __init__(self, *args: Any, uuid_format: str = "hex", **kwargs: Any) -> None: 20 | super().__init__(*args, **kwargs) 21 | self.uuid_format = uuid_format 22 | 23 | def __repr__(self) -> str: # pragma: nocover 24 | if self.uuid_format == "string": 25 | return "CHAR(36)" 26 | return "CHAR(32)" 27 | 28 | def load_dialect_impl(self, dialect: Dialect) -> Any: 29 | return ( 30 | dialect.type_descriptor(CHAR(36)) 31 | if self.uuid_format == "string" 32 | else dialect.type_descriptor(CHAR(32)) 33 | ) 34 | 35 | def process_bind_param(self, value: uuid.UUID, dialect: Dialect) -> Optional[str]: 36 | if value is None: 37 | return value 38 | return str(value) if self.uuid_format == "string" else "%.32x" % value.int 39 | 40 | def process_result_value( 41 | self, value: Optional[str], dialect: Dialect 42 | ) -> Optional[uuid.UUID]: 43 | if value is None: 44 | return value 45 | if not isinstance(value, uuid.UUID): 46 | return uuid.UUID(value) 47 | return value # pragma: nocover 48 | -------------------------------------------------------------------------------- /ormar/fields/through_field.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import TYPE_CHECKING, Any, Optional, Type, Union 3 | 4 | from ormar.fields.base import BaseField 5 | from ormar.fields.foreign_key import ForeignKeyField 6 | 7 | if TYPE_CHECKING: # pragma no cover 8 | from pydantic.typing import ForwardRef 9 | 10 | from ormar import Model 11 | 12 | if sys.version_info < (3, 7): 13 | ToType = Type[Model] 14 | else: 15 | ToType = Union[Type[Model], ForwardRef] 16 | 17 | 18 | def Through( # noqa CFQ002 19 | to: "ToType", 20 | *, 21 | name: Optional[str] = None, 22 | related_name: Optional[str] = None, 23 | **kwargs: Any 24 | ) -> Any: 25 | """ 26 | Despite a name it's a function that returns constructed ThroughField. 27 | It's a special field populated only for m2m relations. 28 | Accepts number of relation setting parameters as well as all BaseField ones. 29 | 30 | :param to: target related ormar Model 31 | :type to: Model class 32 | :param name: name of the database field - later called alias 33 | :type name: str 34 | :param related_name: name of reversed FK relation populated for you on to model 35 | :type related_name: str 36 | It is for reversed FK and auto generated FK on through model in Many2Many relations. 37 | :param kwargs: all other args to be populated by BaseField 38 | :type kwargs: Any 39 | :return: ormar ForeignKeyField with relation to selected model 40 | :rtype: ForeignKeyField 41 | """ 42 | nullable = kwargs.pop("nullable", False) 43 | owner = kwargs.pop("owner", None) 44 | namespace = dict( 45 | __type__=to, 46 | to=to, 47 | through=None, 48 | alias=name, 49 | name=kwargs.pop("real_name", None), 50 | related_name=related_name, 51 | virtual=True, 52 | owner=owner, 53 | nullable=nullable, 54 | unique=False, 55 | column_type=None, 56 | primary_key=False, 57 | index=False, 58 | default=None, 59 | server_default=None, 60 | is_relation=True, 61 | is_through=True, 62 | ) 63 | 64 | Field = type("Through", (ThroughField, BaseField), {}) 65 | return Field(**namespace) 66 | 67 | 68 | class ThroughField(ForeignKeyField): 69 | """ 70 | Field class used to access ManyToMany model through model. 71 | """ 72 | -------------------------------------------------------------------------------- /ormar/models/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Definition of Model, it's parents NewBaseModel and mixins used by models. 3 | Also defines a Metaclass that handles all constructions and relations registration, 4 | ass well as vast number of helper functions for pydantic, sqlalchemy and relations. 5 | """ 6 | 7 | from ormar.models.newbasemodel import NewBaseModel # noqa I100 8 | from ormar.models.model_row import ModelRow # noqa I100 9 | from ormar.models.model import Model, T # noqa I100 10 | from ormar.models.excludable import ExcludableItems # noqa I100 11 | from ormar.models.utils import Extra # noqa I100 12 | from ormar.models.ormar_config import OrmarConfig # noqa I100 13 | 14 | __all__ = [ 15 | "NewBaseModel", 16 | "Model", 17 | "ModelRow", 18 | "ExcludableItems", 19 | "T", 20 | "Extra", 21 | "OrmarConfig", 22 | ] 23 | -------------------------------------------------------------------------------- /ormar/models/descriptors/__init__.py: -------------------------------------------------------------------------------- 1 | from ormar.models.descriptors.descriptors import ( 2 | BytesDescriptor, 3 | JsonDescriptor, 4 | PkDescriptor, 5 | PydanticDescriptor, 6 | RelationDescriptor, 7 | ) 8 | 9 | __all__ = [ 10 | "PydanticDescriptor", 11 | "RelationDescriptor", 12 | "PkDescriptor", 13 | "JsonDescriptor", 14 | "BytesDescriptor", 15 | ] 16 | -------------------------------------------------------------------------------- /ormar/models/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from ormar.models.helpers.models import ( 2 | check_required_config_parameters, 3 | config_field_not_set, 4 | extract_annotations_and_default_vals, 5 | populate_default_options_values, 6 | ) 7 | from ormar.models.helpers.pydantic import ( 8 | get_potential_fields, 9 | get_pydantic_base_orm_config, 10 | merge_or_generate_pydantic_config, 11 | remove_excluded_parent_fields, 12 | ) 13 | from ormar.models.helpers.relations import ( 14 | alias_manager, 15 | expand_reverse_relationships, 16 | register_relation_in_alias_manager, 17 | ) 18 | from ormar.models.helpers.sqlalchemy import ( 19 | populate_config_sqlalchemy_table_if_required, 20 | populate_config_tablename_columns_and_pk, 21 | sqlalchemy_columns_from_model_fields, 22 | ) 23 | from ormar.models.helpers.validation import modify_schema_example 24 | 25 | __all__ = [ 26 | "expand_reverse_relationships", 27 | "extract_annotations_and_default_vals", 28 | "populate_config_tablename_columns_and_pk", 29 | "populate_config_sqlalchemy_table_if_required", 30 | "populate_default_options_values", 31 | "alias_manager", 32 | "register_relation_in_alias_manager", 33 | "get_potential_fields", 34 | "get_pydantic_base_orm_config", 35 | "merge_or_generate_pydantic_config", 36 | "check_required_config_parameters", 37 | "sqlalchemy_columns_from_model_fields", 38 | "config_field_not_set", 39 | "remove_excluded_parent_fields", 40 | "modify_schema_example", 41 | ] 42 | -------------------------------------------------------------------------------- /ormar/models/helpers/related_names_validation.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Dict, ForwardRef, List, Optional, Type 2 | 3 | import ormar # noqa: I100 4 | 5 | if TYPE_CHECKING: # pragma no cover 6 | from ormar import Model 7 | 8 | 9 | def validate_related_names_in_relations( # noqa CCR001 10 | model_fields: Dict, new_model: Type["Model"] 11 | ) -> None: 12 | """ 13 | Performs a validation of relation_names in relation fields. 14 | If multiple fields are leading to the same related model 15 | only one can have empty related_name param 16 | (populated by default as model.name.lower()+'s'). 17 | Also related_names have to be unique for given related model. 18 | 19 | :raises ModelDefinitionError: if validation of related_names fail 20 | :param model_fields: dictionary of declared ormar model fields 21 | :type model_fields: Dict[str, ormar.Field] 22 | :param new_model: 23 | :type new_model: Model class 24 | """ 25 | already_registered: Dict[str, List[Optional[str]]] = dict() 26 | for field in model_fields.values(): 27 | if field.is_relation: 28 | to_name = ( 29 | field.to.get_name() 30 | if not field.to.__class__ == ForwardRef 31 | else str(field.to) 32 | ) 33 | previous_related_names = already_registered.setdefault(to_name, []) 34 | if field.related_name in previous_related_names: 35 | raise ormar.ModelDefinitionError( 36 | f"Multiple fields declared on {new_model.get_name(lower=False)} " 37 | f"model leading to {field.to.get_name(lower=False)} model without " 38 | f"related_name property set. \nThere can be only one relation with " 39 | f"default/empty name: '{new_model.get_name() + 's'}'" 40 | f"\nTip: provide different related_name for FK and/or M2M fields" 41 | ) 42 | previous_related_names.append(field.related_name) 43 | -------------------------------------------------------------------------------- /ormar/models/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Package contains functionalities divided by features. 3 | All mixins are combined into ModelTableProxy which is one of the parents of Model. 4 | The split into mixins was done to ease the maintainability of the proxy class, as 5 | it became quite complicated over time. 6 | """ 7 | 8 | from ormar.models.mixins.alias_mixin import AliasMixin 9 | from ormar.models.mixins.excludable_mixin import ExcludableMixin 10 | from ormar.models.mixins.merge_mixin import MergeModelMixin 11 | from ormar.models.mixins.pydantic_mixin import PydanticMixin 12 | from ormar.models.mixins.save_mixin import SavePrepareMixin 13 | 14 | __all__ = [ 15 | "MergeModelMixin", 16 | "AliasMixin", 17 | "SavePrepareMixin", 18 | "ExcludableMixin", 19 | "PydanticMixin", 20 | ] 21 | -------------------------------------------------------------------------------- /ormar/models/modelproxy.py: -------------------------------------------------------------------------------- 1 | from ormar.models.mixins import ( 2 | ExcludableMixin, 3 | MergeModelMixin, 4 | PydanticMixin, 5 | SavePrepareMixin, 6 | ) 7 | 8 | 9 | class ModelTableProxy( 10 | MergeModelMixin, 11 | SavePrepareMixin, 12 | ExcludableMixin, 13 | PydanticMixin, 14 | ): 15 | """ 16 | Used to combine all mixins with different set of functionalities. 17 | One of the bases of the ormar Model class. 18 | """ 19 | -------------------------------------------------------------------------------- /ormar/models/quick_access_views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains set of fields/methods etc names that are used to bypass the checks in 3 | NewBaseModel __getattribute__ calls to speed the calls. 4 | """ 5 | 6 | quick_access_set = { 7 | "Config", 8 | "model_config", 9 | "model_fields", 10 | "__cached_hash__", 11 | "__class__", 12 | "__config__", 13 | "__custom_root_type__", 14 | "__dict__", 15 | "model_fields", 16 | "__fields_set__", 17 | "__json_encoder__", 18 | "__pk_only__", 19 | "__post_root_validators__", 20 | "__pre_root_validators__", 21 | "__private_attributes__", 22 | "__same__", 23 | "_calculate_keys", 24 | "_convert_json", 25 | "_extract_db_related_names", 26 | "_extract_model_db_fields", 27 | "_extract_nested_models", 28 | "_extract_nested_models_from_list", 29 | "_extract_own_model_fields", 30 | "_extract_related_model_instead_of_field", 31 | "_get_not_excluded_fields", 32 | "_get_value", 33 | "_init_private_attributes", 34 | "_is_conversion_to_json_needed", 35 | "_iter", 36 | "_iterate_related_models", 37 | "_orm", 38 | "_orm_id", 39 | "_orm_saved", 40 | "_related_names", 41 | "_skip_ellipsis", 42 | "_update_and_follow", 43 | "_update_excluded_with_related_not_required", 44 | "_verify_model_can_be_initialized", 45 | "copy", 46 | "delete", 47 | "dict", 48 | "extract_related_names", 49 | "extract_related_fields", 50 | "extract_through_names", 51 | "update_from_dict", 52 | "get_child", 53 | "get_column_alias", 54 | "get_column_name_from_alias", 55 | "get_filtered_names_to_extract", 56 | "get_name", 57 | "get_properties", 58 | "get_related_field_name", 59 | "get_relation_model_id", 60 | "json", 61 | "keys", 62 | "load", 63 | "load_all", 64 | "pk_column", 65 | "pk_type", 66 | "populate_default_values", 67 | "prepare_model_to_save", 68 | "remove", 69 | "resolve_relation_field", 70 | "resolve_relation_name", 71 | "save", 72 | "save_related", 73 | "saved", 74 | "set_save_status", 75 | "signals", 76 | "translate_aliases_to_columns", 77 | "translate_columns_to_aliases", 78 | "update", 79 | "upsert", 80 | } 81 | -------------------------------------------------------------------------------- /ormar/models/utils.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Extra(str, Enum): 5 | ignore = "ignore" 6 | forbid = "forbid" 7 | -------------------------------------------------------------------------------- /ormar/protocols/__init__.py: -------------------------------------------------------------------------------- 1 | from ormar.protocols.queryset_protocol import QuerySetProtocol 2 | from ormar.protocols.relation_protocol import RelationProtocol 3 | 4 | __all__ = ["QuerySetProtocol", "RelationProtocol"] 5 | -------------------------------------------------------------------------------- /ormar/protocols/queryset_protocol.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Set, Tuple, Union 2 | 3 | try: 4 | from typing import Protocol 5 | except ImportError: # pragma: nocover 6 | from typing_extensions import Protocol # type: ignore 7 | 8 | if TYPE_CHECKING: # noqa: C901; #pragma nocover 9 | from ormar import Model 10 | from ormar.relations.querysetproxy import QuerysetProxy 11 | 12 | 13 | class QuerySetProtocol(Protocol): # pragma: nocover 14 | def filter(self, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001 15 | ... 16 | 17 | def exclude(self, **kwargs: Any) -> "QuerysetProxy": # noqa: A003, A001 18 | ... 19 | 20 | def select_related(self, related: Union[List, str]) -> "QuerysetProxy": ... 21 | 22 | def prefetch_related(self, related: Union[List, str]) -> "QuerysetProxy": ... 23 | 24 | async def exists(self) -> bool: ... 25 | 26 | async def count(self, distinct: bool = True) -> int: ... 27 | 28 | async def clear(self) -> int: ... 29 | 30 | def limit(self, limit_count: int) -> "QuerysetProxy": ... 31 | 32 | def offset(self, offset: int) -> "QuerysetProxy": ... 33 | 34 | async def first(self, **kwargs: Any) -> "Model": ... 35 | 36 | async def get(self, **kwargs: Any) -> "Model": ... 37 | 38 | async def all( # noqa: A003, A001 39 | self, **kwargs: Any 40 | ) -> Sequence[Optional["Model"]]: ... 41 | 42 | async def create(self, **kwargs: Any) -> "Model": ... 43 | 44 | async def update(self, each: bool = False, **kwargs: Any) -> int: ... 45 | 46 | async def get_or_create( 47 | self, 48 | _defaults: Optional[Dict[str, Any]] = None, 49 | **kwargs: Any, 50 | ) -> Tuple["Model", bool]: ... 51 | 52 | async def update_or_create(self, **kwargs: Any) -> "Model": ... 53 | 54 | def fields(self, columns: Union[List, str, Set, Dict]) -> "QuerysetProxy": ... 55 | 56 | def exclude_fields( 57 | self, columns: Union[List, str, Set, Dict] 58 | ) -> "QuerysetProxy": ... 59 | 60 | def order_by(self, columns: Union[List, str]) -> "QuerysetProxy": ... 61 | -------------------------------------------------------------------------------- /ormar/protocols/relation_protocol.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Type, Union 2 | 3 | try: 4 | from typing import Protocol 5 | except ImportError: # pragma: nocover 6 | from typing_extensions import Protocol # type: ignore 7 | 8 | if TYPE_CHECKING: # pragma: nocover 9 | from ormar import Model 10 | 11 | 12 | class RelationProtocol(Protocol): # pragma: nocover 13 | def add(self, child: "Model") -> None: ... 14 | 15 | def remove(self, child: Union["Model", Type["Model"]]) -> None: ... 16 | -------------------------------------------------------------------------------- /ormar/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/ormar/py.typed -------------------------------------------------------------------------------- /ormar/queryset/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains QuerySet and different Query classes to allow for constructing of sql queries. 3 | """ 4 | 5 | from ormar.queryset.actions import FilterAction, OrderAction, SelectAction 6 | from ormar.queryset.clause import and_, or_ 7 | from ormar.queryset.field_accessor import FieldAccessor 8 | from ormar.queryset.queries import FilterQuery, LimitQuery, OffsetQuery, OrderQuery 9 | from ormar.queryset.queryset import QuerySet 10 | 11 | __all__ = [ 12 | "QuerySet", 13 | "FilterQuery", 14 | "LimitQuery", 15 | "OffsetQuery", 16 | "OrderQuery", 17 | "FilterAction", 18 | "OrderAction", 19 | "SelectAction", 20 | "and_", 21 | "or_", 22 | "FieldAccessor", 23 | ] 24 | -------------------------------------------------------------------------------- /ormar/queryset/actions/__init__.py: -------------------------------------------------------------------------------- 1 | from ormar.queryset.actions.filter_action import FilterAction 2 | from ormar.queryset.actions.order_action import OrderAction 3 | from ormar.queryset.actions.select_action import SelectAction 4 | 5 | __all__ = ["FilterAction", "OrderAction", "SelectAction"] 6 | -------------------------------------------------------------------------------- /ormar/queryset/actions/select_action.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | from typing import TYPE_CHECKING, Any, Callable, Optional, Type 3 | 4 | import sqlalchemy 5 | 6 | from ormar.queryset.actions.query_action import QueryAction # noqa: I202 7 | 8 | if TYPE_CHECKING: # pragma: no cover 9 | from ormar import Model 10 | 11 | 12 | class SelectAction(QueryAction): 13 | """ 14 | Order Actions is populated by queryset when order_by() is called. 15 | 16 | All required params are extracted but kept raw until actual filter clause value 17 | is required -> then the action is converted into text() clause. 18 | 19 | Extracted in order to easily change table prefixes on complex relations. 20 | """ 21 | 22 | def __init__( 23 | self, select_str: str, model_cls: Type["Model"], alias: Optional[str] = None 24 | ) -> None: 25 | super().__init__(query_str=select_str, model_cls=model_cls) 26 | if alias: # pragma: no cover 27 | self.table_prefix = alias 28 | 29 | def _split_value_into_parts(self, order_str: str) -> None: 30 | parts = order_str.split("__") 31 | self.field_name = parts[-1] 32 | self.related_parts = parts[:-1] 33 | 34 | @property 35 | def is_numeric(self) -> bool: 36 | return self.get_target_field_type() in [int, float, decimal.Decimal] 37 | 38 | def get_target_field_type(self) -> Any: 39 | return self.target_model.ormar_config.model_fields[self.field_name].__type__ 40 | 41 | def get_text_clause(self) -> sqlalchemy.sql.expression.TextClause: 42 | alias = f"{self.table_prefix}_" if self.table_prefix else "" 43 | return sqlalchemy.text(f"{alias}{self.field_name}") 44 | 45 | def apply_func( 46 | self, func: Callable, use_label: bool = True 47 | ) -> sqlalchemy.sql.expression.TextClause: 48 | result = func(self.get_text_clause()) 49 | if use_label: 50 | rel_prefix = f"{self.related_str}__" if self.related_str else "" 51 | result = result.label(f"{rel_prefix}{self.field_name}") 52 | return result 53 | -------------------------------------------------------------------------------- /ormar/queryset/queries/__init__.py: -------------------------------------------------------------------------------- 1 | from ormar.queryset.queries.filter_query import FilterQuery 2 | from ormar.queryset.queries.limit_query import LimitQuery 3 | from ormar.queryset.queries.offset_query import OffsetQuery 4 | from ormar.queryset.queries.order_query import OrderQuery 5 | from ormar.queryset.queries.prefetch_query import PrefetchQuery 6 | from ormar.queryset.queries.query import Query 7 | 8 | __all__ = [ 9 | "FilterQuery", 10 | "LimitQuery", 11 | "OffsetQuery", 12 | "OrderQuery", 13 | "PrefetchQuery", 14 | "Query", 15 | ] 16 | -------------------------------------------------------------------------------- /ormar/queryset/queries/filter_query.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import sqlalchemy 4 | 5 | from ormar.queryset.actions.filter_action import FilterAction 6 | 7 | 8 | class FilterQuery: 9 | """ 10 | Modifies the select query with given list of where/filter clauses. 11 | """ 12 | 13 | def __init__( 14 | self, filter_clauses: List[FilterAction], exclude: bool = False 15 | ) -> None: 16 | self.exclude = exclude 17 | self.filter_clauses = filter_clauses 18 | 19 | def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: 20 | """ 21 | Applies all filter clauses if set. 22 | 23 | :param expr: query to modify 24 | :type expr: sqlalchemy.sql.selectable.Select 25 | :return: modified query 26 | :rtype: sqlalchemy.sql.selectable.Select 27 | """ 28 | if self.filter_clauses: 29 | if len(self.filter_clauses) == 1: 30 | clause = self.filter_clauses[0].get_text_clause() 31 | else: 32 | clause = sqlalchemy.sql.and_( 33 | *[x.get_text_clause() for x in self.filter_clauses] 34 | ) 35 | clause = sqlalchemy.sql.not_(clause) if self.exclude else clause 36 | expr = expr.where(clause) 37 | return expr 38 | -------------------------------------------------------------------------------- /ormar/queryset/queries/limit_query.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import sqlalchemy 4 | 5 | 6 | class LimitQuery: 7 | """ 8 | Modifies the select query with limit clause. 9 | """ 10 | 11 | def __init__(self, limit_count: Optional[int]) -> None: 12 | self.limit_count = limit_count 13 | 14 | def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: 15 | """ 16 | Applies the limit clause. 17 | 18 | :param expr: query to modify 19 | :type expr: sqlalchemy.sql.selectable.Select 20 | :return: modified query 21 | :rtype: sqlalchemy.sql.selectable.Select 22 | """ 23 | 24 | if self.limit_count is not None: 25 | expr = expr.limit(self.limit_count) 26 | 27 | return expr 28 | -------------------------------------------------------------------------------- /ormar/queryset/queries/offset_query.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import sqlalchemy 4 | 5 | 6 | class OffsetQuery: 7 | """ 8 | Modifies the select query with offset if set 9 | """ 10 | 11 | def __init__(self, query_offset: Optional[int]) -> None: 12 | self.query_offset = query_offset 13 | 14 | def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: 15 | """ 16 | Applies the offset clause. 17 | 18 | :param expr: query to modify 19 | :type expr: sqlalchemy.sql.selectable.Select 20 | :return: modified query 21 | :rtype: sqlalchemy.sql.selectable.Select 22 | """ 23 | if self.query_offset: 24 | expr = expr.offset(self.query_offset) 25 | return expr 26 | -------------------------------------------------------------------------------- /ormar/queryset/queries/order_query.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | import sqlalchemy 4 | 5 | 6 | class OrderQuery: 7 | """ 8 | Modifies the select query with given list of order_by clauses. 9 | """ 10 | 11 | def __init__(self, sorted_orders: Dict) -> None: 12 | self.sorted_orders = sorted_orders 13 | 14 | def apply(self, expr: sqlalchemy.sql.select) -> sqlalchemy.sql.select: 15 | """ 16 | Applies all order_by clauses if set. 17 | 18 | :param expr: query to modify 19 | :type expr: sqlalchemy.sql.selectable.Select 20 | :return: modified query 21 | :rtype: sqlalchemy.sql.selectable.Select 22 | """ 23 | if self.sorted_orders: 24 | for order in list(self.sorted_orders.values()): 25 | if order is not None: 26 | expr = expr.order_by(order) 27 | return expr 28 | -------------------------------------------------------------------------------- /ormar/relations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Package handles relations on models, returning related models on calls and exposing 3 | QuerySetProxy for m2m and reverse relations. 4 | """ 5 | 6 | from ormar.relations.alias_manager import AliasManager 7 | from ormar.relations.relation import Relation, RelationType 8 | from ormar.relations.relation_manager import RelationsManager 9 | from ormar.relations.utils import get_relations_sides_and_names 10 | 11 | __all__ = [ 12 | "AliasManager", 13 | "Relation", 14 | "RelationsManager", 15 | "RelationType", 16 | "get_relations_sides_and_names", 17 | ] 18 | -------------------------------------------------------------------------------- /ormar/relations/utils.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Tuple 2 | from weakref import proxy 3 | 4 | from ormar.fields.foreign_key import ForeignKeyField 5 | 6 | if TYPE_CHECKING: # pragma no cover 7 | from ormar import Model 8 | 9 | 10 | def get_relations_sides_and_names( 11 | to_field: ForeignKeyField, parent: "Model", child: "Model" 12 | ) -> Tuple["Model", "Model", str, str]: 13 | """ 14 | Determines the names of child and parent relations names, as well as 15 | changes one of the sides of the relation into weakref.proxy to model. 16 | 17 | :param to_field: field with relation definition 18 | :type to_field: ForeignKeyField 19 | :param parent: parent model 20 | :type parent: Model 21 | :param child: child model 22 | :type child: Model 23 | :return: parent, child, child_name, to_name 24 | :rtype: Tuple["Model", "Model", str, str] 25 | """ 26 | to_name = to_field.name 27 | child_name = to_field.get_related_name() 28 | if to_field.virtual: 29 | child_name, to_name = to_name, child_name 30 | child, parent = parent, proxy(child) 31 | else: 32 | child = proxy(child) 33 | return parent, child, child_name, to_name 34 | -------------------------------------------------------------------------------- /ormar/signals/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Signals and SignalEmitter that gathers the signals on models OrmarConfig. 3 | Used to signal receivers functions about events, i.e. post_save, pre_delete etc. 4 | """ 5 | 6 | from ormar.signals.signal import Signal, SignalEmitter 7 | 8 | __all__ = ["Signal", "SignalEmitter"] 9 | -------------------------------------------------------------------------------- /ormar/warnings.py: -------------------------------------------------------------------------------- 1 | # Adopted from pydantic 2 | from typing import Optional, Tuple 3 | 4 | 5 | class OrmarDeprecationWarning(DeprecationWarning): 6 | """A Pydantic specific deprecation warning. 7 | 8 | This warning is raised when using deprecated functionality in Ormar. 9 | It provides information on when the deprecation was introduced and 10 | the expected version in which the corresponding functionality will be removed. 11 | 12 | Attributes: 13 | message: Description of the warning 14 | since: Ormar version in what the deprecation was introduced 15 | expected_removal: Ormar version in what the functionality will be removed 16 | """ 17 | 18 | message: str 19 | since: Tuple[int, int] 20 | expected_removal: Tuple[int, int] 21 | 22 | def __init__( 23 | self, 24 | message: str, 25 | *args: object, 26 | since: Tuple[int, int], 27 | expected_removal: Optional[Tuple[int, int]] = None, 28 | ) -> None: # pragma: no cover 29 | super().__init__(message, *args) 30 | self.message = message.rstrip(".") 31 | self.since = since 32 | self.expected_removal = ( 33 | expected_removal if expected_removal is not None else (since[0] + 1, 0) 34 | ) 35 | 36 | def __str__(self) -> str: # pragma: no cover 37 | message = ( 38 | f"{self.message}. Deprecated in Ormar V{self.since[0]}.{self.since[1]}" 39 | f" to be removed in V{self.expected_removal[0]}.{self.expected_removal[1]}." 40 | ) 41 | if self.since == (0, 20): 42 | message += " See Ormar V0.20 Migration Guide at https://collerek.github.io/ormar/migration/" 43 | return message 44 | 45 | 46 | class OrmarDeprecatedSince020(OrmarDeprecationWarning): 47 | """A specific `OrmarDeprecationWarning` subclass defining 48 | functionality deprecated since Ormar 0.20.""" 49 | 50 | def __init__(self, message: str, *args: object) -> None: # pragma: no cover 51 | super().__init__(message, *args, since=(0, 20), expected_removal=(0, 30)) 52 | -------------------------------------------------------------------------------- /scripts/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | services: 3 | postgres: 4 | image: postgres:10.8 5 | environment: 6 | POSTGRES_USER: username 7 | POSTGRES_PASSWORD: password 8 | POSTGRES_DB: testsuite 9 | ports: 10 | - 5432:5432 11 | 12 | mysql: 13 | image: mysql:5.7 14 | environment: 15 | MYSQL_USER: username 16 | MYSQL_PASSWORD: password 17 | MYSQL_ROOT_PASSWORD: password 18 | MYSQL_DATABASE: testsuite 19 | ports: 20 | - 3306:3306 -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | PACKAGE="ormar" 4 | 5 | PREFIX="" 6 | if [ -d 'venv' ] ; then 7 | PREFIX="venv/bin/" 8 | fi 9 | 10 | set -x 11 | 12 | PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov=${PACKAGE} --cov=tests --cov-report=xml --cov-fail-under=100 --cov-report=term-missing tests/ "${@}" 13 | -------------------------------------------------------------------------------- /scripts/test_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | PACKAGE="docs_src" 4 | 5 | PREFIX="" 6 | if [ -d 'venv' ] ; then 7 | PREFIX="venv/bin/" 8 | fi 9 | 10 | set -x 11 | 12 | PYTHONPATH=. ${PREFIX}pytest --ignore venv docs_src/ "${@}" 13 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/__init__.py -------------------------------------------------------------------------------- /tests/lifespan.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from typing import AsyncIterator 3 | 4 | import pytest 5 | import sqlalchemy 6 | from fastapi import FastAPI 7 | 8 | 9 | def lifespan(config): 10 | @asynccontextmanager 11 | async def do_lifespan(_: FastAPI) -> AsyncIterator[None]: 12 | if not config.database.is_connected: 13 | await config.database.connect() 14 | 15 | yield 16 | 17 | if config.database.is_connected: 18 | await config.database.disconnect() 19 | 20 | return do_lifespan 21 | 22 | 23 | def init_tests(config, scope="module"): 24 | @pytest.fixture(autouse=True, scope=scope) 25 | def create_database(): 26 | config.engine = sqlalchemy.create_engine(config.database.url._url) 27 | config.metadata.create_all(config.engine) 28 | 29 | yield 30 | 31 | config.metadata.drop_all(config.engine) 32 | 33 | return create_database 34 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import databases 4 | import ormar 5 | import sqlalchemy 6 | 7 | DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///test.db") 8 | database_url = databases.DatabaseURL(DATABASE_URL) 9 | if database_url.scheme == "postgresql+aiopg": # pragma no cover 10 | DATABASE_URL = str(database_url.replace(driver=None)) 11 | print("USED DB:", DATABASE_URL) 12 | 13 | 14 | def create_config(**args): 15 | database_ = databases.Database(DATABASE_URL, **args) 16 | metadata_ = sqlalchemy.MetaData() 17 | engine_ = sqlalchemy.create_engine(DATABASE_URL) 18 | 19 | return ormar.OrmarConfig( 20 | metadata=metadata_, 21 | database=database_, 22 | engine=engine_, 23 | ) 24 | -------------------------------------------------------------------------------- /tests/test_deferred/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_deferred/__init__.py -------------------------------------------------------------------------------- /tests/test_encryption/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_encryption/__init__.py -------------------------------------------------------------------------------- /tests/test_exclude_include_dict/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_exclude_include_dict/__init__.py -------------------------------------------------------------------------------- /tests/test_exclude_include_dict/test_excluding_nested_models_lists.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_exclude_include_dict/test_excluding_nested_models_lists.py -------------------------------------------------------------------------------- /tests/test_fastapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_fastapi/__init__.py -------------------------------------------------------------------------------- /tests/test_fastapi/test_binary_fields.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import uuid 3 | from enum import Enum 4 | from typing import List 5 | 6 | import ormar 7 | import pytest 8 | from asgi_lifespan import LifespanManager 9 | from fastapi import FastAPI 10 | from httpx import AsyncClient 11 | 12 | from tests.lifespan import init_tests, lifespan 13 | from tests.settings import create_config 14 | 15 | headers = {"content-type": "application/json"} 16 | base_ormar_config = create_config() 17 | app = FastAPI(lifespan=lifespan(base_ormar_config)) 18 | 19 | 20 | blob3 = b"\xc3\x83\x28" 21 | blob4 = b"\xf0\x28\x8c\x28" 22 | blob5 = b"\xee" 23 | blob6 = b"\xff" 24 | 25 | 26 | class BinaryEnum(Enum): 27 | blob3 = blob3 28 | blob4 = blob4 29 | blob5 = blob5 30 | blob6 = blob6 31 | 32 | 33 | class BinaryThing(ormar.Model): 34 | ormar_config = base_ormar_config.copy(tablename="things") 35 | 36 | id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4) 37 | name: str = ormar.Text(default="") 38 | bt: str = ormar.LargeBinary(represent_as_base64_str=True, max_length=100) 39 | 40 | 41 | create_test_database = init_tests(base_ormar_config) 42 | 43 | 44 | @app.get("/things", response_model=List[BinaryThing]) 45 | async def read_things(): 46 | return await BinaryThing.objects.order_by("name").all() 47 | 48 | 49 | @app.post("/things", response_model=BinaryThing) 50 | async def create_things(thing: BinaryThing): 51 | thing = await thing.save() 52 | return thing 53 | 54 | 55 | @pytest.mark.asyncio 56 | async def test_read_main(): 57 | client = AsyncClient(app=app, base_url="http://testserver") 58 | async with client as client, LifespanManager(app): 59 | response = await client.post( 60 | "/things", 61 | json={"bt": base64.b64encode(blob3).decode()}, 62 | headers=headers, 63 | ) 64 | assert response.status_code == 200 65 | response = await client.get("/things") 66 | assert response.json()[0]["bt"] == base64.b64encode(blob3).decode() 67 | thing = BinaryThing(**response.json()[0]) 68 | assert thing.__dict__["bt"] == blob3 69 | assert thing.bt == base64.b64encode(blob3).decode() 70 | 71 | 72 | def test_schema(): 73 | schema = BinaryThing.model_json_schema() 74 | assert schema["properties"]["bt"]["format"] == "base64" 75 | assert schema["example"]["bt"] == "string" 76 | -------------------------------------------------------------------------------- /tests/test_fastapi/test_docs_with_multiple_relations_to_one.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from uuid import UUID, uuid4 3 | 4 | import ormar 5 | import pytest 6 | from asgi_lifespan import LifespanManager 7 | from fastapi import FastAPI 8 | from httpx import AsyncClient 9 | 10 | from tests.lifespan import init_tests, lifespan 11 | from tests.settings import create_config 12 | 13 | base_ormar_config = create_config() 14 | app = FastAPI(lifespan=lifespan(base_ormar_config)) 15 | 16 | 17 | class CA(ormar.Model): 18 | ormar_config = base_ormar_config.copy(tablename="cas") 19 | 20 | id: UUID = ormar.UUID(primary_key=True, default=uuid4) 21 | ca_name: str = ormar.Text(default="") 22 | 23 | 24 | class CB1(ormar.Model): 25 | ormar_config = base_ormar_config.copy(tablename="cb1s") 26 | 27 | id: UUID = ormar.UUID(primary_key=True, default=uuid4) 28 | cb1_name: str = ormar.Text(default="") 29 | ca1: Optional[CA] = ormar.ForeignKey(CA, nullable=True) 30 | 31 | 32 | class CB2(ormar.Model): 33 | ormar_config = base_ormar_config.copy(tablename="cb2s") 34 | 35 | id: UUID = ormar.UUID(primary_key=True, default=uuid4) 36 | cb2_name: str = ormar.Text(default="") 37 | ca2: Optional[CA] = ormar.ForeignKey(CA, nullable=True) 38 | 39 | 40 | create_test_database = init_tests(base_ormar_config) 41 | 42 | 43 | @app.get("/ca", response_model=CA) 44 | async def get_ca(): # pragma: no cover 45 | return None 46 | 47 | 48 | @app.get("/cb1", response_model=CB1) 49 | async def get_cb1(): # pragma: no cover 50 | return None 51 | 52 | 53 | @app.get("/cb2", response_model=CB2) 54 | async def get_cb2(): # pragma: no cover 55 | return None 56 | 57 | 58 | @pytest.mark.asyncio 59 | async def test_all_endpoints(): 60 | client = AsyncClient(app=app, base_url="http://testserver") 61 | async with client as client, LifespanManager(app): 62 | response = await client.get("/openapi.json") 63 | assert response.status_code == 200, response.text 64 | schema = response.json() 65 | components = schema["components"]["schemas"] 66 | raw_names_w_o_modules = [x.split("__")[-1] for x in components.keys()] 67 | assert all(x in raw_names_w_o_modules for x in ["CA", "CB1", "CB2"]) 68 | pk_onlys = [x for x in list(raw_names_w_o_modules) if x.startswith("PkOnly")] 69 | assert len(pk_onlys) == 4 70 | -------------------------------------------------------------------------------- /tests/test_fastapi/test_enum_schema.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import ormar 4 | 5 | from tests.lifespan import init_tests 6 | from tests.settings import create_config 7 | 8 | base_ormar_config = create_config() 9 | 10 | 11 | class MyEnum(Enum): 12 | SMALL = 1 13 | BIG = 2 14 | 15 | 16 | class EnumExample(ormar.Model): 17 | ormar_config = base_ormar_config.copy(tablename="enum_example") 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | size: MyEnum = ormar.Enum(enum_class=MyEnum, default=MyEnum.SMALL) 21 | 22 | 23 | create_test_database = init_tests(base_ormar_config) 24 | 25 | 26 | def test_proper_schema(): 27 | schema = EnumExample.model_json_schema() 28 | assert {"MyEnum": {"title": "MyEnum", "enum": [1, 2], "type": "integer"}} == schema[ 29 | "$defs" 30 | ] 31 | -------------------------------------------------------------------------------- /tests/test_fastapi/test_extra_ignore_parameter.py: -------------------------------------------------------------------------------- 1 | import ormar 2 | import pytest 3 | from asgi_lifespan import LifespanManager 4 | from fastapi import FastAPI 5 | from httpx import AsyncClient 6 | from ormar import Extra 7 | 8 | from tests.lifespan import init_tests, lifespan 9 | from tests.settings import create_config 10 | 11 | base_ormar_config = create_config() 12 | app = FastAPI(lifespan=lifespan(base_ormar_config)) 13 | 14 | 15 | class Item(ormar.Model): 16 | ormar_config = base_ormar_config.copy(extra=Extra.ignore) 17 | 18 | id: int = ormar.Integer(primary_key=True) 19 | name: str = ormar.String(max_length=100) 20 | 21 | 22 | create_test_database = init_tests(base_ormar_config) 23 | 24 | 25 | @app.post("/item/", response_model=Item) 26 | async def create_item(item: Item): 27 | return await item.save() 28 | 29 | 30 | @pytest.mark.asyncio 31 | async def test_extra_parameters_in_request(): 32 | client = AsyncClient(app=app, base_url="http://testserver") 33 | async with client as client, LifespanManager(app): 34 | data = {"name": "Name", "extraname": "to ignore"} 35 | resp = await client.post("item/", json=data) 36 | assert resp.status_code == 200 37 | assert "name" in resp.json() 38 | assert resp.json().get("name") == "Name" 39 | -------------------------------------------------------------------------------- /tests/test_fastapi/test_fastapi_usage.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import ormar 4 | import pytest 5 | from asgi_lifespan import LifespanManager 6 | from fastapi import FastAPI 7 | from httpx import AsyncClient 8 | 9 | from tests.lifespan import init_tests, lifespan 10 | from tests.settings import create_config 11 | 12 | base_ormar_config = create_config() 13 | app = FastAPI(lifespan=lifespan(base_ormar_config)) 14 | 15 | 16 | class Category(ormar.Model): 17 | ormar_config = base_ormar_config.copy(tablename="categories") 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | name: str = ormar.String(max_length=100) 21 | 22 | 23 | class Item(ormar.Model): 24 | ormar_config = base_ormar_config.copy(tablename="items") 25 | 26 | id: int = ormar.Integer(primary_key=True) 27 | name: str = ormar.String(max_length=100) 28 | category: Optional[Category] = ormar.ForeignKey(Category, nullable=True) 29 | 30 | 31 | create_test_database = init_tests(base_ormar_config) 32 | 33 | 34 | @app.post("/items/", response_model=Item) 35 | async def create_item(item: Item): 36 | return item 37 | 38 | 39 | @pytest.mark.asyncio 40 | async def test_read_main(): 41 | client = AsyncClient(app=app, base_url="http://testserver") 42 | async with client as client, LifespanManager(app): 43 | response = await client.post( 44 | "/items/", json={"name": "test", "id": 1, "category": {"name": "test cat"}} 45 | ) 46 | assert response.status_code == 200 47 | assert response.json() == { 48 | "category": { 49 | "id": None, 50 | "name": "test cat", 51 | }, 52 | "id": 1, 53 | "name": "test", 54 | } 55 | item = Item(**response.json()) 56 | assert item.id == 1 57 | assert item.category.items[0].id == 1 58 | -------------------------------------------------------------------------------- /tests/test_fastapi/test_schema_not_allowed_params.py: -------------------------------------------------------------------------------- 1 | import ormar 2 | 3 | from tests.lifespan import init_tests 4 | from tests.settings import create_config 5 | 6 | base_ormar_config = create_config() 7 | 8 | 9 | class Author(ormar.Model): 10 | ormar_config = base_ormar_config.copy(tablename="authors") 11 | 12 | id: int = ormar.Integer(primary_key=True) 13 | name: str = ormar.String(max_length=100) 14 | contents: str = ormar.Text() 15 | 16 | 17 | create_test_database = init_tests(base_ormar_config) 18 | 19 | 20 | def test_schema_not_allowed(): 21 | schema = Author.model_json_schema() 22 | for field_schema in schema.get("properties").values(): 23 | for key in field_schema.keys(): 24 | assert "_" not in key, f"Found illegal field in openapi schema: {key}" 25 | -------------------------------------------------------------------------------- /tests/test_hashes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_hashes/__init__.py -------------------------------------------------------------------------------- /tests/test_inheritance_and_pydantic_generation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_inheritance_and_pydantic_generation/__init__.py -------------------------------------------------------------------------------- /tests/test_inheritance_and_pydantic_generation/test_inheritance_of_property_fields.py: -------------------------------------------------------------------------------- 1 | import ormar 2 | from pydantic import computed_field 3 | 4 | from tests.lifespan import init_tests 5 | from tests.settings import create_config 6 | 7 | base_ormar_config = create_config() 8 | 9 | 10 | class BaseFoo(ormar.Model): 11 | ormar_config = base_ormar_config.copy(abstract=True) 12 | 13 | name: str = ormar.String(max_length=100) 14 | 15 | @computed_field 16 | def prefixed_name(self) -> str: 17 | return "prefix_" + self.name 18 | 19 | 20 | class Foo(BaseFoo): 21 | ormar_config = base_ormar_config.copy() 22 | 23 | @computed_field 24 | def double_prefixed_name(self) -> str: 25 | return "prefix2_" + self.name 26 | 27 | id: int = ormar.Integer(primary_key=True) 28 | 29 | 30 | class Bar(BaseFoo): 31 | ormar_config = base_ormar_config.copy() 32 | 33 | @computed_field 34 | def prefixed_name(self) -> str: 35 | return "baz_" + self.name 36 | 37 | id: int = ormar.Integer(primary_key=True) 38 | 39 | 40 | create_test_database = init_tests(base_ormar_config) 41 | 42 | 43 | def test_property_fields_are_inherited(): 44 | foo = Foo(name="foo") 45 | assert foo.prefixed_name == "prefix_foo" 46 | assert foo.model_dump() == { 47 | "name": "foo", 48 | "id": None, 49 | "double_prefixed_name": "prefix2_foo", 50 | "prefixed_name": "prefix_foo", 51 | } 52 | 53 | bar = Bar(name="bar") 54 | assert bar.prefixed_name == "baz_bar" 55 | assert bar.model_dump() == {"name": "bar", "id": None, "prefixed_name": "baz_bar"} 56 | -------------------------------------------------------------------------------- /tests/test_inheritance_and_pydantic_generation/test_inheritance_with_default.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | 4 | import ormar 5 | import pytest 6 | 7 | from tests.lifespan import init_tests 8 | from tests.settings import create_config 9 | 10 | base_ormar_config = create_config() 11 | 12 | 13 | class BaseModel(ormar.Model): 14 | ormar_config = base_ormar_config.copy(abstract=True) 15 | 16 | id: uuid.UUID = ormar.UUID( 17 | primary_key=True, default=uuid.uuid4, uuid_format="string" 18 | ) 19 | created_at: datetime.datetime = ormar.DateTime(default=datetime.datetime.utcnow()) 20 | updated_at: datetime.datetime = ormar.DateTime(default=datetime.datetime.utcnow()) 21 | 22 | 23 | class Member(BaseModel): 24 | ormar_config = base_ormar_config.copy(tablename="members") 25 | 26 | first_name: str = ormar.String(max_length=50) 27 | last_name: str = ormar.String(max_length=50) 28 | 29 | 30 | create_test_database = init_tests(base_ormar_config) 31 | 32 | 33 | def test_model_structure(): 34 | assert "id" in BaseModel.model_fields 35 | assert "id" in BaseModel.ormar_config.model_fields 36 | assert BaseModel.ormar_config.model_fields["id"].has_default() 37 | assert BaseModel.model_fields["id"].default_factory is not None 38 | 39 | assert "id" in Member.model_fields 40 | assert "id" in Member.ormar_config.model_fields 41 | assert Member.ormar_config.model_fields["id"].has_default() 42 | assert Member.model_fields["id"].default_factory is not None 43 | 44 | 45 | @pytest.mark.asyncio 46 | async def test_fields_inherited_with_default(): 47 | async with base_ormar_config.database: 48 | await Member(first_name="foo", last_name="bar").save() 49 | await Member.objects.create(first_name="foo", last_name="bar") 50 | -------------------------------------------------------------------------------- /tests/test_inheritance_and_pydantic_generation/test_inherited_class_is_not_abstract_by_default.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import ormar 4 | import pytest 5 | 6 | from tests.lifespan import init_tests 7 | from tests.settings import create_config 8 | 9 | base_ormar_config = create_config() 10 | 11 | 12 | class TableBase(ormar.Model): 13 | ormar_config = base_ormar_config.copy(abstract=True) 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | created_by: str = ormar.String(max_length=20, default="test") 17 | created_at: datetime.datetime = ormar.DateTime( 18 | timezone=True, default=datetime.datetime.now 19 | ) 20 | last_modified_by: str = ormar.String(max_length=20, nullable=True) 21 | last_modified_at: datetime.datetime = ormar.DateTime(timezone=True, nullable=True) 22 | 23 | 24 | class NationBase(ormar.Model): 25 | ormar_config = base_ormar_config.copy(abstract=True) 26 | 27 | name: str = ormar.String(max_length=50) 28 | alpha2_code: str = ormar.String(max_length=2) 29 | region: str = ormar.String(max_length=30) 30 | subregion: str = ormar.String(max_length=30) 31 | 32 | 33 | class Nation(NationBase, TableBase): 34 | ormar_config = base_ormar_config.copy() 35 | 36 | 37 | create_test_database = init_tests(base_ormar_config) 38 | 39 | 40 | @pytest.mark.asyncio 41 | async def test_model_is_not_abstract_by_default(): 42 | async with base_ormar_config.database: 43 | sweden = await Nation( 44 | name="Sweden", alpha2_code="SE", region="Europe", subregion="Scandinavia" 45 | ).save() 46 | assert sweden.id is not None 47 | -------------------------------------------------------------------------------- /tests/test_inheritance_and_pydantic_generation/test_nested_models_pydantic.py: -------------------------------------------------------------------------------- 1 | import ormar 2 | 3 | from tests.lifespan import init_tests 4 | from tests.settings import create_config 5 | 6 | base_ormar_config = create_config() 7 | 8 | 9 | class Library(ormar.Model): 10 | ormar_config = base_ormar_config.copy() 11 | 12 | id: int = ormar.Integer(primary_key=True) 13 | name: str = ormar.String(max_length=100) 14 | 15 | 16 | class Package(ormar.Model): 17 | ormar_config = base_ormar_config.copy() 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | library: Library = ormar.ForeignKey(Library, related_name="packages") 21 | version: str = ormar.String(max_length=100) 22 | 23 | 24 | class Ticket(ormar.Model): 25 | ormar_config = base_ormar_config.copy() 26 | 27 | id: int = ormar.Integer(primary_key=True) 28 | number: int = ormar.Integer() 29 | status: str = ormar.String(max_length=100) 30 | 31 | 32 | class TicketPackage(ormar.Model): 33 | ormar_config = base_ormar_config.copy() 34 | 35 | id: int = ormar.Integer(primary_key=True) 36 | status: str = ormar.String(max_length=100) 37 | ticket: Ticket = ormar.ForeignKey(Ticket, related_name="packages") 38 | package: Package = ormar.ForeignKey(Package, related_name="tickets") 39 | 40 | 41 | create_test_database = init_tests(base_ormar_config) 42 | 43 | 44 | def test_have_proper_children(): 45 | TicketPackageOut = TicketPackage.get_pydantic(exclude={"ticket"}) 46 | assert "package" in TicketPackageOut.model_fields 47 | PydanticPackage = TicketPackageOut.__pydantic_core_schema__["schema"]["fields"][ 48 | "package" 49 | ]["schema"]["schema"]["schema"]["cls"] 50 | assert "library" in PydanticPackage.model_fields 51 | 52 | 53 | def test_casts_properly(): 54 | payload = { 55 | "id": 0, 56 | "status": "string", 57 | "ticket": {"id": 0, "number": 0, "status": "string"}, 58 | "package": { 59 | "version": "string", 60 | "id": 0, 61 | "library": {"id": 0, "name": "string"}, 62 | }, 63 | } 64 | test_package = TicketPackage(**payload) 65 | TicketPackageOut = TicketPackage.get_pydantic(exclude={"ticket"}) 66 | parsed = TicketPackageOut(**test_package.model_dump()).model_dump() 67 | assert "ticket" not in parsed 68 | assert "package" in parsed 69 | assert "library" in parsed.get("package") 70 | -------------------------------------------------------------------------------- /tests/test_inheritance_and_pydantic_generation/test_pydantic_fields_order.py: -------------------------------------------------------------------------------- 1 | import ormar 2 | 3 | from tests.lifespan import init_tests 4 | from tests.settings import create_config 5 | 6 | base_ormar_config = create_config() 7 | 8 | 9 | class NewTestModel(ormar.Model): 10 | ormar_config = base_ormar_config.copy() 11 | 12 | a: int = ormar.Integer(primary_key=True) 13 | b: str = ormar.String(max_length=1) 14 | c: str = ormar.String(max_length=1) 15 | d: str = ormar.String(max_length=1) 16 | e: str = ormar.String(max_length=1) 17 | f: str = ormar.String(max_length=1) 18 | 19 | 20 | create_test_database = init_tests(base_ormar_config) 21 | 22 | 23 | def test_model_field_order(): 24 | TestCreate = NewTestModel.get_pydantic(exclude={"a"}) 25 | assert list(TestCreate.model_fields.keys()) == ["b", "c", "d", "e", "f"] 26 | -------------------------------------------------------------------------------- /tests/test_inheritance_and_pydantic_generation/test_validators_are_inherited.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | import ormar 4 | import pytest 5 | from pydantic import ValidationError, field_validator 6 | 7 | from tests.lifespan import init_tests 8 | from tests.settings import create_config 9 | 10 | base_ormar_config = create_config() 11 | 12 | 13 | class BaseModel(ormar.Model): 14 | ormar_config = base_ormar_config.copy(abstract=True) 15 | 16 | id: int = ormar.Integer(primary_key=True) 17 | str_field: str = ormar.String(min_length=5, max_length=10, nullable=False) 18 | 19 | @field_validator("str_field") 20 | def validate_str_field(cls, v): 21 | if " " not in v: 22 | raise ValueError("must contain a space") 23 | return v 24 | 25 | 26 | class EnumExample(str, enum.Enum): 27 | A = "A" 28 | B = "B" 29 | C = "C" 30 | 31 | 32 | class ModelExample(BaseModel): 33 | ormar_config = base_ormar_config.copy(tablename="examples") 34 | 35 | enum_field: str = ormar.Enum(enum_class=EnumExample, nullable=False) 36 | 37 | 38 | ModelExampleCreate = ModelExample.get_pydantic(exclude={"id"}) 39 | 40 | 41 | create_test_database = init_tests(base_ormar_config) 42 | 43 | 44 | def test_ormar_validator(): 45 | ModelExample(str_field="a aaaaaa", enum_field="A") 46 | with pytest.raises(ValidationError) as e: 47 | ModelExample(str_field="aaaaaaa", enum_field="A") 48 | assert "must contain a space" in str(e) 49 | with pytest.raises(ValidationError) as e: 50 | ModelExample(str_field="a aaaaaaa", enum_field="Z") 51 | assert "Input should be 'A', 'B' or 'C'" in str(e) 52 | 53 | 54 | def test_pydantic_validator(): 55 | ModelExampleCreate(str_field="a aaaaaa", enum_field="A") 56 | with pytest.raises(ValidationError) as e: 57 | ModelExampleCreate(str_field="aaaaaaa", enum_field="A") 58 | assert "must contain a space" in str(e) 59 | with pytest.raises(ValidationError) as e: 60 | ModelExampleCreate(str_field="a aaaaaaa", enum_field="Z") 61 | assert "Input should be 'A', 'B' or 'C'" in str(e) 62 | -------------------------------------------------------------------------------- /tests/test_inheritance_and_pydantic_generation/test_validators_in_generated_pydantic.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | import ormar 4 | import pytest 5 | from pydantic import ValidationError, field_validator 6 | 7 | from tests.lifespan import init_tests 8 | from tests.settings import create_config 9 | 10 | base_ormar_config = create_config() 11 | 12 | 13 | class EnumExample(str, enum.Enum): 14 | A = "A" 15 | B = "B" 16 | C = "C" 17 | 18 | 19 | class ModelExample(ormar.Model): 20 | ormar_config = base_ormar_config.copy(tablename="examples") 21 | 22 | id: int = ormar.Integer(primary_key=True) 23 | str_field: str = ormar.String(min_length=5, max_length=10, nullable=False) 24 | enum_field: str = ormar.Enum(nullable=False, enum_class=EnumExample) 25 | 26 | @field_validator("str_field") 27 | def validate_str_field(cls, v): 28 | if " " not in v: 29 | raise ValueError("must contain a space") 30 | return v 31 | 32 | 33 | ModelExampleCreate = ModelExample.get_pydantic(exclude={"id"}) 34 | 35 | 36 | create_test_database = init_tests(base_ormar_config) 37 | 38 | 39 | def test_ormar_validator(): 40 | ModelExample(str_field="a aaaaaa", enum_field="A") 41 | with pytest.raises(ValidationError) as e: 42 | ModelExample(str_field="aaaaaaa", enum_field="A") 43 | assert "must contain a space" in str(e) 44 | with pytest.raises(ValidationError) as e: 45 | ModelExample(str_field="a aaaaaaa", enum_field="Z") 46 | assert "Input should be 'A', 'B' or 'C'" in str(e) 47 | 48 | 49 | def test_pydantic_validator(): 50 | ModelExampleCreate(str_field="a aaaaaa", enum_field="A") 51 | with pytest.raises(ValidationError) as e: 52 | ModelExampleCreate(str_field="aaaaaaa", enum_field="A") 53 | assert "must contain a space" in str(e) 54 | with pytest.raises(ValidationError) as e: 55 | ModelExampleCreate(str_field="a aaaaaaa", enum_field="Z") 56 | assert "Input should be 'A', 'B' or 'C'" in str(e) 57 | -------------------------------------------------------------------------------- /tests/test_meta_constraints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_meta_constraints/__init__.py -------------------------------------------------------------------------------- /tests/test_meta_constraints/test_check_constraints.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | import asyncpg # type: ignore 4 | import ormar.fields.constraints 5 | import pytest 6 | 7 | from tests.lifespan import init_tests 8 | from tests.settings import create_config 9 | 10 | base_ormar_config = create_config() 11 | 12 | 13 | class Product(ormar.Model): 14 | ormar_config = base_ormar_config.copy( 15 | tablename="products", 16 | constraints=[ 17 | ormar.fields.constraints.CheckColumns("inventory > buffer"), 18 | ], 19 | ) 20 | 21 | id: int = ormar.Integer(primary_key=True) 22 | name: str = ormar.String(max_length=100) 23 | company: str = ormar.String(max_length=200) 24 | inventory: int = ormar.Integer() 25 | buffer: int = ormar.Integer() 26 | 27 | 28 | create_test_database = init_tests(base_ormar_config) 29 | 30 | 31 | @pytest.mark.asyncio 32 | async def test_check_columns_exclude_mysql(): 33 | if Product.ormar_config.database._backend._dialect.name != "mysql": 34 | async with base_ormar_config.database: # pragma: no cover 35 | async with base_ormar_config.database.transaction(force_rollback=True): 36 | await Product.objects.create( 37 | name="Mars", company="Nestle", inventory=100, buffer=10 38 | ) 39 | 40 | with pytest.raises( 41 | ( 42 | sqlite3.IntegrityError, 43 | asyncpg.exceptions.CheckViolationError, 44 | ) 45 | ): 46 | await Product.objects.create( 47 | name="Cookies", company="Nestle", inventory=1, buffer=10 48 | ) 49 | -------------------------------------------------------------------------------- /tests/test_meta_constraints/test_index_constraints.py: -------------------------------------------------------------------------------- 1 | import ormar.fields.constraints 2 | import pytest 3 | 4 | from tests.lifespan import init_tests 5 | from tests.settings import create_config 6 | 7 | base_ormar_config = create_config() 8 | 9 | 10 | class Product(ormar.Model): 11 | ormar_config = base_ormar_config.copy( 12 | tablename="products", 13 | constraints=[ 14 | ormar.fields.constraints.IndexColumns("company", "name", name="my_index"), 15 | ormar.fields.constraints.IndexColumns("location", "company_type"), 16 | ], 17 | ) 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | name: str = ormar.String(max_length=100) 21 | company: str = ormar.String(max_length=200) 22 | location: str = ormar.String(max_length=200) 23 | company_type: str = ormar.String(max_length=200) 24 | 25 | 26 | create_test_database = init_tests(base_ormar_config) 27 | 28 | 29 | def test_table_structure(): 30 | assert len(Product.ormar_config.table.indexes) > 0 31 | indexes = sorted( 32 | list(Product.ormar_config.table.indexes), key=lambda x: x.name, reverse=True 33 | ) 34 | test_index = indexes[0] 35 | assert test_index.name == "my_index" 36 | assert [col.name for col in test_index.columns] == ["company", "name"] 37 | 38 | test_index = indexes[1] 39 | assert test_index.name == "ix_products_location_company_type" 40 | assert [col.name for col in test_index.columns] == ["location", "company_type"] 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_index_is_not_unique(): 45 | async with base_ormar_config.database: 46 | async with base_ormar_config.database.transaction(force_rollback=True): 47 | await Product.objects.create( 48 | name="Cookies", company="Nestle", location="A", company_type="B" 49 | ) 50 | await Product.objects.create( 51 | name="Mars", company="Mars", location="B", company_type="Z" 52 | ) 53 | await Product.objects.create( 54 | name="Mars", company="Nestle", location="C", company_type="X" 55 | ) 56 | await Product.objects.create( 57 | name="Mars", company="Mars", location="D", company_type="Y" 58 | ) 59 | -------------------------------------------------------------------------------- /tests/test_meta_constraints/test_unique_constraints.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | import asyncpg # type: ignore 4 | import ormar.fields.constraints 5 | import pymysql 6 | import pytest 7 | 8 | from tests.lifespan import init_tests 9 | from tests.settings import create_config 10 | 11 | base_ormar_config = create_config() 12 | 13 | 14 | class Product(ormar.Model): 15 | ormar_config = base_ormar_config.copy( 16 | tablename="products", 17 | constraints=[ormar.fields.constraints.UniqueColumns("name", "company")], 18 | ) 19 | 20 | id: int = ormar.Integer(primary_key=True) 21 | name: str = ormar.String(max_length=100) 22 | company: str = ormar.String(max_length=200) 23 | 24 | 25 | create_test_database = init_tests(base_ormar_config) 26 | 27 | 28 | @pytest.mark.asyncio 29 | async def test_unique_columns(): 30 | async with base_ormar_config.database: 31 | async with base_ormar_config.database.transaction(force_rollback=True): 32 | await Product.objects.create(name="Cookies", company="Nestle") 33 | await Product.objects.create(name="Mars", company="Mars") 34 | await Product.objects.create(name="Mars", company="Nestle") 35 | 36 | with pytest.raises( 37 | ( 38 | sqlite3.IntegrityError, 39 | pymysql.IntegrityError, 40 | asyncpg.exceptions.UniqueViolationError, 41 | ) 42 | ): 43 | await Product.objects.create(name="Mars", company="Mars") 44 | -------------------------------------------------------------------------------- /tests/test_model_definition/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_model_definition/__init__.py -------------------------------------------------------------------------------- /tests/test_model_definition/pks_and_fks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_model_definition/pks_and_fks/__init__.py -------------------------------------------------------------------------------- /tests/test_model_definition/pks_and_fks/test_non_integer_pkey.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import databases 4 | import ormar 5 | import pytest 6 | import sqlalchemy 7 | 8 | from tests.settings import DATABASE_URL 9 | 10 | database = databases.Database(DATABASE_URL, force_rollback=True) 11 | metadata = sqlalchemy.MetaData() 12 | 13 | 14 | def key(): 15 | return "".join(random.choice("abcdefgh123456") for _ in range(8)) 16 | 17 | 18 | class Model(ormar.Model): 19 | ormar_config = ormar.OrmarConfig( 20 | tablename="models", 21 | metadata=metadata, 22 | database=database, 23 | ) 24 | 25 | id: str = ormar.String(primary_key=True, default=key, max_length=8) 26 | name: str = ormar.String(max_length=32) 27 | 28 | 29 | @pytest.fixture(autouse=True, scope="function") 30 | def create_test_database(): 31 | engine = sqlalchemy.create_engine(DATABASE_URL) 32 | metadata.create_all(engine) 33 | yield 34 | metadata.drop_all(engine) 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_pk_1(): 39 | async with database: 40 | model = await Model.objects.create(name="NAME") 41 | assert isinstance(model.id, str) 42 | 43 | 44 | @pytest.mark.asyncio 45 | async def test_pk_2(): 46 | async with database: 47 | model = await Model.objects.create(name="NAME") 48 | assert await Model.objects.all() == [model] 49 | -------------------------------------------------------------------------------- /tests/test_model_definition/pks_and_fks/test_saving_string_pks.py: -------------------------------------------------------------------------------- 1 | from random import choice 2 | from string import ascii_uppercase 3 | 4 | import databases 5 | import ormar 6 | import pytest 7 | import pytest_asyncio 8 | import sqlalchemy 9 | from ormar import Float, String 10 | from sqlalchemy import create_engine 11 | 12 | from tests.settings import DATABASE_URL 13 | 14 | database = databases.Database(DATABASE_URL, force_rollback=True) 15 | metadata = sqlalchemy.MetaData() 16 | 17 | 18 | def get_id() -> str: 19 | return "".join(choice(ascii_uppercase) for _ in range(12)) 20 | 21 | 22 | base_ormar_config = ormar.OrmarConfig( 23 | metadata=metadata, 24 | database=database, 25 | ) 26 | 27 | 28 | class PositionOrm(ormar.Model): 29 | ormar_config = base_ormar_config.copy() 30 | 31 | name: str = String(primary_key=True, max_length=50) 32 | x: float = Float() 33 | y: float = Float() 34 | degrees: float = Float() 35 | 36 | 37 | class PositionOrmDef(ormar.Model): 38 | ormar_config = base_ormar_config.copy() 39 | 40 | name: str = String(primary_key=True, max_length=50, default=get_id) 41 | x: float = Float() 42 | y: float = Float() 43 | degrees: float = Float() 44 | 45 | 46 | @pytest.fixture(autouse=True, scope="module") 47 | def create_test_database(): 48 | engine = create_engine(DATABASE_URL) 49 | metadata.create_all(engine) 50 | yield 51 | metadata.drop_all(engine) 52 | 53 | 54 | @pytest_asyncio.fixture(scope="function") 55 | async def cleanup(): 56 | yield 57 | async with database: 58 | await PositionOrm.objects.delete(each=True) 59 | await PositionOrmDef.objects.delete(each=True) 60 | 61 | 62 | @pytest.mark.asyncio 63 | async def test_creating_a_position(cleanup): 64 | async with database: 65 | instance = PositionOrm(name="my_pos", x=1.0, y=2.0, degrees=3.0) 66 | await instance.save() 67 | assert instance.saved 68 | assert instance.name == "my_pos" 69 | 70 | instance2 = PositionOrmDef(x=1.0, y=2.0, degrees=3.0) 71 | await instance2.save() 72 | assert instance2.saved 73 | assert instance2.name is not None 74 | assert len(instance2.name) == 12 75 | 76 | instance3 = PositionOrmDef(x=1.0, y=2.0, degrees=3.0) 77 | await instance3.save() 78 | assert instance3.saved 79 | assert instance3.name is not None 80 | assert len(instance3.name) == 12 81 | assert instance2.name != instance3.name 82 | -------------------------------------------------------------------------------- /tests/test_model_definition/pks_and_fks/test_uuid_fks.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import databases 4 | import ormar 5 | import pytest 6 | import sqlalchemy 7 | from sqlalchemy import create_engine 8 | 9 | from tests.settings import DATABASE_URL 10 | 11 | metadata = sqlalchemy.MetaData() 12 | db = databases.Database(DATABASE_URL) 13 | 14 | 15 | class User(ormar.Model): 16 | ormar_config = ormar.OrmarConfig( 17 | tablename="user", 18 | metadata=metadata, 19 | database=db, 20 | ) 21 | 22 | id: uuid.UUID = ormar.UUID( 23 | primary_key=True, default=uuid.uuid4, uuid_format="string" 24 | ) 25 | username = ormar.String(index=True, unique=True, null=False, max_length=255) 26 | email = ormar.String(index=True, unique=True, nullable=False, max_length=255) 27 | hashed_password = ormar.String(null=False, max_length=255) 28 | is_active = ormar.Boolean(default=True, nullable=False) 29 | is_superuser = ormar.Boolean(default=False, nullable=False) 30 | 31 | 32 | class Token(ormar.Model): 33 | ormar_config = ormar.OrmarConfig( 34 | tablename="token", 35 | metadata=metadata, 36 | database=db, 37 | ) 38 | 39 | id = ormar.Integer(primary_key=True) 40 | text = ormar.String(max_length=4, unique=True) 41 | user = ormar.ForeignKey(User, related_name="tokens") 42 | created_at = ormar.DateTime(server_default=sqlalchemy.func.now()) 43 | 44 | 45 | @pytest.fixture(autouse=True, scope="module") 46 | def create_test_database(): 47 | engine = create_engine(DATABASE_URL) 48 | metadata.create_all(engine) 49 | yield 50 | metadata.drop_all(engine) 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_uuid_fk(): 55 | async with db: 56 | async with db.transaction(force_rollback=True): 57 | user = await User.objects.create( 58 | username="User1", 59 | email="email@example.com", 60 | hashed_password="^$EDACVS(&A&Y@2131aa", 61 | is_active=True, 62 | is_superuser=False, 63 | ) 64 | await Token.objects.create(text="AAAA", user=user) 65 | await Token.objects.order_by("-created_at").all() 66 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_create_uses_init_for_consistency.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import ClassVar 3 | 4 | import ormar 5 | import pytest 6 | from pydantic import model_validator 7 | 8 | from tests.lifespan import init_tests 9 | from tests.settings import create_config 10 | 11 | base_ormar_config = create_config() 12 | 13 | 14 | class Mol(ormar.Model): 15 | # fixed namespace to generate always unique uuid from the smiles 16 | _UUID_NAMESPACE: ClassVar[uuid.UUID] = uuid.UUID( 17 | "12345678-abcd-1234-abcd-123456789abc" 18 | ) 19 | 20 | ormar_config = base_ormar_config.copy(tablename="mols") 21 | 22 | id: uuid.UUID = ormar.UUID(primary_key=True, index=True, uuid_format="hex") 23 | smiles: str = ormar.String(nullable=False, unique=True, max_length=256) 24 | 25 | def __init__(self, **kwargs): 26 | # this is required to generate id from smiles in init, if id is not given 27 | if "id" not in kwargs: 28 | kwargs["id"] = self._UUID_NAMESPACE 29 | super().__init__(**kwargs) 30 | 31 | @model_validator(mode="before") 32 | def make_canonical_smiles_and_uuid(cls, values): 33 | values["id"], values["smiles"] = cls.uuid(values["smiles"]) 34 | return values 35 | 36 | @classmethod 37 | def uuid(cls, smiles): 38 | id_ = uuid.uuid5(cls._UUID_NAMESPACE, smiles) 39 | return id_, smiles 40 | 41 | 42 | create_test_database = init_tests(base_ormar_config) 43 | 44 | 45 | @pytest.mark.asyncio 46 | async def test_json_column(): 47 | async with base_ormar_config.database: 48 | await Mol.objects.create(smiles="Cc1ccccc1") 49 | count = await Mol.objects.count() 50 | assert count == 1 51 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_equality_and_hash.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | import ormar 3 | import pytest 4 | 5 | from tests.lifespan import init_tests 6 | from tests.settings import create_config 7 | 8 | base_ormar_config = create_config() 9 | 10 | 11 | class Song(ormar.Model): 12 | ormar_config = base_ormar_config.copy(tablename="songs") 13 | 14 | id: int = ormar.Integer(primary_key=True) 15 | name: str = ormar.String(max_length=100) 16 | 17 | 18 | create_test_database = init_tests(base_ormar_config) 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_equality(): 23 | async with base_ormar_config.database: 24 | song1 = await Song.objects.create(name="Song") 25 | song2 = await Song.objects.create(name="Song") 26 | song3 = Song(name="Song") 27 | song4 = Song(name="Song") 28 | 29 | assert song1 == song1 30 | assert song3 == song4 31 | 32 | assert song1 != song2 33 | assert song1 != song3 34 | assert song3 != song1 35 | assert song1 is not None 36 | 37 | 38 | @pytest.mark.asyncio 39 | async def test_hash_doesnt_change_with_fields_if_pk(): 40 | async with base_ormar_config.database: 41 | song1 = await Song.objects.create(name="Song") 42 | prev_hash = hash(song1) 43 | 44 | await song1.update(name="Song 2") 45 | assert hash(song1) == prev_hash 46 | 47 | 48 | @pytest.mark.asyncio 49 | async def test_hash_changes_with_fields_if_no_pk(): 50 | async with base_ormar_config.database: 51 | song1 = Song(name="Song") 52 | prev_hash = hash(song1) 53 | 54 | song1.name = "Song 2" 55 | assert hash(song1) != prev_hash 56 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_extra_ignore_parameter.py: -------------------------------------------------------------------------------- 1 | import ormar 2 | from ormar import Extra 3 | 4 | from tests.lifespan import init_tests 5 | from tests.settings import create_config 6 | 7 | base_ormar_config = create_config(force_rollback=True) 8 | 9 | 10 | class Child(ormar.Model): 11 | ormar_config = base_ormar_config.copy( 12 | tablename="children", 13 | extra=Extra.ignore, 14 | ) 15 | 16 | id: int = ormar.Integer(name="child_id", primary_key=True) 17 | first_name: str = ormar.String(name="fname", max_length=100) 18 | last_name: str = ormar.String(name="lname", max_length=100) 19 | 20 | 21 | create_test_database = init_tests(base_ormar_config) 22 | 23 | 24 | def test_allow_extra_parameter(): 25 | child = Child(first_name="Test", last_name="Name", extra_param="Unexpected") 26 | assert child.first_name == "Test" 27 | assert child.last_name == "Name" 28 | assert not hasattr(child, "extra_param") 29 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_foreign_key_value_used_for_related_model.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Optional 3 | 4 | import ormar 5 | import pytest 6 | 7 | from tests.lifespan import init_tests 8 | from tests.settings import create_config 9 | 10 | base_ormar_config = create_config() 11 | 12 | 13 | class PageLink(ormar.Model): 14 | ormar_config = base_ormar_config.copy(tablename="pagelinks") 15 | 16 | id: int = ormar.Integer(primary_key=True) 17 | value: str = ormar.String(max_length=2048) 18 | country: str = ormar.String(max_length=1000) 19 | 20 | 21 | class Post(ormar.Model): 22 | ormar_config = base_ormar_config.copy(tablename="posts") 23 | 24 | id: int = ormar.Integer(primary_key=True) 25 | title: str = ormar.String(max_length=500) 26 | link: PageLink = ormar.ForeignKey( 27 | PageLink, related_name="posts", ondelete="CASCADE" 28 | ) 29 | 30 | 31 | class Department(ormar.Model): 32 | ormar_config = base_ormar_config.copy() 33 | 34 | id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4()) 35 | name: str = ormar.String(max_length=100) 36 | 37 | 38 | class Course(ormar.Model): 39 | ormar_config = base_ormar_config.copy() 40 | 41 | id: int = ormar.Integer(primary_key=True) 42 | name: str = ormar.String(max_length=100) 43 | completed: bool = ormar.Boolean(default=False) 44 | department: Optional[Department] = ormar.ForeignKey(Department) 45 | 46 | 47 | create_test_database = init_tests(base_ormar_config) 48 | 49 | 50 | @pytest.mark.asyncio 51 | async def test_pass_int_values_as_fk(): 52 | async with base_ormar_config.database: 53 | async with base_ormar_config.database.transaction(force_rollback=True): 54 | link = await PageLink(id=1, value="test", country="USA").save() 55 | await Post.objects.create(title="My post", link=link.id) 56 | post_check = await Post.objects.select_related("link").get() 57 | assert post_check.link == link 58 | 59 | 60 | @pytest.mark.asyncio 61 | async def test_pass_uuid_value_as_fk(): 62 | async with base_ormar_config.database: 63 | async with base_ormar_config.database.transaction(force_rollback=True): 64 | dept = await Department(name="Department test").save() 65 | await Course(name="Test course", department=dept.id).save() 66 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_models_are_pickable.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from typing import Optional 3 | 4 | import ormar 5 | import pytest 6 | 7 | from tests.lifespan import init_tests 8 | from tests.settings import create_config 9 | 10 | base_ormar_config = create_config() 11 | 12 | 13 | class User(ormar.Model): 14 | ormar_config = base_ormar_config.copy(tablename="users") 15 | 16 | id: int = ormar.Integer(primary_key=True) 17 | name: str = ormar.String(max_length=100) 18 | properties = ormar.JSON(nullable=True) 19 | 20 | 21 | class Post(ormar.Model): 22 | ormar_config = base_ormar_config.copy(tablename="posts") 23 | 24 | id: int = ormar.Integer(primary_key=True) 25 | name: str = ormar.String(max_length=100) 26 | created_by: Optional[User] = ormar.ForeignKey(User) 27 | 28 | 29 | create_test_database = init_tests(base_ormar_config) 30 | 31 | 32 | @pytest.mark.asyncio 33 | async def test_dumping_and_loading_model_works(): 34 | async with base_ormar_config.database: 35 | user = await User(name="Test", properties={"aa": "bb"}).save() 36 | post = Post(name="Test post") 37 | await user.posts.add(post) 38 | pickled_value = pickle.dumps(user) 39 | python_value = pickle.loads(pickled_value) 40 | assert isinstance(python_value, User) 41 | assert python_value.name == "Test" 42 | assert python_value.properties == {"aa": "bb"} 43 | assert python_value.posts[0].name == "Test post" 44 | await python_value.load() 45 | await python_value.update(name="Test2") 46 | check = await User.objects.get() 47 | assert check.name == "Test2" 48 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_overwriting_pydantic_field_type.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional 2 | 3 | import ormar 4 | import pydantic 5 | import pytest 6 | from pydantic import Json, PositiveInt, ValidationError 7 | 8 | from tests.lifespan import init_tests 9 | from tests.settings import create_config 10 | 11 | base_ormar_config = create_config() 12 | 13 | 14 | class OverwriteTest(ormar.Model): 15 | ormar_config = base_ormar_config.copy(tablename="overwrites") 16 | 17 | id: int = ormar.Integer(primary_key=True) 18 | my_int: int = ormar.Integer(overwrite_pydantic_type=PositiveInt) 19 | constraint_dict: Json = ormar.JSON( 20 | overwrite_pydantic_type=Optional[Json[Dict[str, int]]] 21 | ) # type: ignore 22 | 23 | 24 | class User(ormar.Model): 25 | ormar_config = base_ormar_config.copy(tablename="users") 26 | id: int = ormar.Integer(primary_key=True) 27 | email: str = ormar.String( 28 | max_length=255, 29 | unique=True, 30 | nullable=False, 31 | overwrite_pydantic_type=pydantic.EmailStr, 32 | ) 33 | 34 | 35 | create_test_database = init_tests(base_ormar_config) 36 | 37 | 38 | def test_constraints(): 39 | with pytest.raises(ValidationError, match="Input should be greater than 0"): 40 | OverwriteTest(my_int=-10) 41 | 42 | with pytest.raises( 43 | ValidationError, 44 | match="Input should be a valid integer, unable to parse string as an integer", 45 | ): 46 | OverwriteTest(my_int=10, constraint_dict={"aa": "ab"}) 47 | 48 | with pytest.raises( 49 | ValidationError, 50 | match=( 51 | r"The email address is not valid. It must have exactly one @-sign|" 52 | r"An email address must have an @-sign" 53 | ), 54 | ): 55 | User(email="wrong") 56 | 57 | 58 | @pytest.mark.asyncio 59 | async def test_saving(): 60 | async with base_ormar_config.database: 61 | await OverwriteTest(my_int=5, constraint_dict={"aa": 123}).save() 62 | 63 | test = await OverwriteTest.objects.get() 64 | assert test.my_int == 5 65 | assert test.constraint_dict == {"aa": 123} 66 | 67 | await User(email="test@as.eu").save() 68 | test = await User.objects.get() 69 | assert test.email == "test@as.eu" 70 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_overwriting_sql_nullable.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from typing import Optional 3 | 4 | import asyncpg 5 | import ormar 6 | import pymysql 7 | import pytest 8 | from sqlalchemy import text 9 | 10 | from tests.lifespan import init_tests 11 | from tests.settings import create_config 12 | 13 | base_ormar_config = create_config() 14 | 15 | 16 | class PrimaryModel(ormar.Model): 17 | ormar_config = base_ormar_config.copy(tablename="primary_models") 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | name: str = ormar.String(max_length=255, index=True) 21 | some_text: Optional[str] = ormar.Text(nullable=True, sql_nullable=False) 22 | some_other_text: Optional[str] = ormar.String( 23 | max_length=255, nullable=True, sql_nullable=False, server_default=text("''") 24 | ) 25 | 26 | 27 | create_test_database = init_tests(base_ormar_config) 28 | 29 | 30 | @pytest.mark.asyncio 31 | async def test_create_models(): 32 | async with base_ormar_config.database: 33 | primary = await PrimaryModel( 34 | name="Foo", some_text="Bar", some_other_text="Baz" 35 | ).save() 36 | assert primary.id == 1 37 | 38 | primary2 = await PrimaryModel(name="Foo2", some_text="Bar2").save() 39 | assert primary2.id == 2 40 | 41 | with pytest.raises( 42 | ( 43 | sqlite3.IntegrityError, 44 | pymysql.IntegrityError, 45 | asyncpg.exceptions.NotNullViolationError, 46 | ) 47 | ): 48 | await PrimaryModel(name="Foo3").save() 49 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_pk_field_is_always_not_null.py: -------------------------------------------------------------------------------- 1 | import ormar 2 | 3 | from tests.lifespan import init_tests 4 | from tests.settings import create_config 5 | 6 | base_ormar_config = create_config() 7 | 8 | 9 | class AutoincrementModel(ormar.Model): 10 | ormar_config = base_ormar_config.copy() 11 | 12 | id: int = ormar.Integer(primary_key=True) 13 | 14 | 15 | class NonAutoincrementModel(ormar.Model): 16 | ormar_config = base_ormar_config.copy() 17 | 18 | id: int = ormar.Integer(primary_key=True, autoincrement=False) 19 | 20 | 21 | class ExplicitNullableModel(ormar.Model): 22 | ormar_config = base_ormar_config.copy() 23 | 24 | id: int = ormar.Integer(primary_key=True, nullable=True) 25 | 26 | 27 | create_test_database = init_tests(base_ormar_config) 28 | 29 | 30 | def test_pk_field_is_not_null(): 31 | for model in [AutoincrementModel, NonAutoincrementModel, ExplicitNullableModel]: 32 | assert not model.ormar_config.table.c.get("id").nullable 33 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_properties.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | import ormar 3 | import pytest 4 | from pydantic import PydanticUserError, computed_field 5 | 6 | from tests.lifespan import init_tests 7 | from tests.settings import create_config 8 | 9 | base_ormar_config = create_config() 10 | 11 | 12 | class Song(ormar.Model): 13 | ormar_config = base_ormar_config.copy(tablename="songs") 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | name: str = ormar.String(max_length=100) 17 | sort_order: int = ormar.Integer() 18 | 19 | @computed_field 20 | def sorted_name(self) -> str: 21 | return f"{self.sort_order}: {self.name}" 22 | 23 | @computed_field 24 | def sample(self) -> str: 25 | return "sample" 26 | 27 | @computed_field 28 | def sample2(self) -> str: 29 | return "sample2" 30 | 31 | 32 | create_test_database = init_tests(base_ormar_config) 33 | 34 | 35 | @pytest.mark.asyncio 36 | async def test_sort_order_on_main_model(): 37 | async with base_ormar_config.database: 38 | await Song.objects.create(name="Song 3", sort_order=3) 39 | await Song.objects.create(name="Song 1", sort_order=1) 40 | await Song.objects.create(name="Song 2", sort_order=2) 41 | 42 | songs = await Song.objects.all() 43 | song_dict = [song.model_dump() for song in songs] 44 | assert all("sorted_name" in x for x in song_dict) 45 | assert all( 46 | x["sorted_name"] == f"{x['sort_order']}: {x['name']}" for x in song_dict 47 | ) 48 | song_json = [song.model_dump_json() for song in songs] 49 | assert all("sorted_name" in x for x in song_json) 50 | 51 | check_include = songs[0].model_dump(include={"sample"}) 52 | assert "sample" in check_include 53 | assert "sample2" not in check_include 54 | assert "sorted_name" not in check_include 55 | 56 | check_include = songs[0].model_dump(exclude={"sample"}) 57 | assert "sample" not in check_include 58 | assert "sample2" in check_include 59 | assert "sorted_name" in check_include 60 | 61 | 62 | def test_wrong_definition(): 63 | with pytest.raises(PydanticUserError): 64 | 65 | class WrongModel(ormar.Model): # pragma: no cover 66 | @computed_field 67 | def test(self, aa=10, bb=30): 68 | pass 69 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_pydantic_only_fields.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import ormar 4 | import pydantic 5 | import pytest 6 | from pydantic import computed_field 7 | 8 | from tests.lifespan import init_tests 9 | from tests.settings import create_config 10 | 11 | base_ormar_config = create_config() 12 | 13 | 14 | class Album(ormar.Model): 15 | ormar_config = base_ormar_config.copy(tablename="albums") 16 | 17 | id: int = ormar.Integer(primary_key=True) 18 | name: str = ormar.String(max_length=100) 19 | timestamp: datetime.datetime = pydantic.Field(default=None) 20 | 21 | @computed_field 22 | def name10(self) -> str: 23 | return self.name + "_10" 24 | 25 | @computed_field 26 | def name20(self) -> str: 27 | return self.name + "_20" 28 | 29 | @property 30 | def name30(self) -> str: 31 | return self.name + "_30" 32 | 33 | @computed_field 34 | def name40(self) -> str: 35 | return self.name + "_40" 36 | 37 | 38 | create_test_database = init_tests(base_ormar_config) 39 | 40 | 41 | @pytest.mark.asyncio 42 | async def test_pydantic_only_fields(): 43 | async with base_ormar_config.database: 44 | async with base_ormar_config.database.transaction(force_rollback=True): 45 | album = await Album.objects.create(name="Hitchcock") 46 | assert album.pk is not None 47 | assert album.saved 48 | assert album.timestamp is None 49 | 50 | album = await Album.objects.exclude_fields("timestamp").get() 51 | assert album.timestamp is None 52 | 53 | album = await Album.objects.fields({"name", "timestamp"}).get() 54 | assert album.timestamp is None 55 | 56 | test_dict = album.model_dump() 57 | assert "timestamp" in test_dict 58 | assert test_dict["timestamp"] is None 59 | 60 | assert album.name30 == "Hitchcock_30" 61 | 62 | album.timestamp = datetime.datetime.now() 63 | test_dict = album.model_dump() 64 | assert "timestamp" in test_dict 65 | assert test_dict["timestamp"] is not None 66 | assert test_dict.get("name10") == "Hitchcock_10" 67 | assert test_dict.get("name20") == "Hitchcock_20" 68 | assert test_dict.get("name40") == "Hitchcock_40" 69 | assert "name30" not in test_dict 70 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_pydantic_private_attributes.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import ormar 4 | from pydantic import PrivateAttr 5 | 6 | from tests.lifespan import init_tests 7 | from tests.settings import create_config 8 | 9 | base_ormar_config = create_config() 10 | 11 | 12 | class Subscription(ormar.Model): 13 | ormar_config = base_ormar_config.copy(tablename="subscriptions") 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | stripe_subscription_id: str = ormar.String(nullable=False, max_length=256) 17 | 18 | _add_payments: List[str] = PrivateAttr(default_factory=list) 19 | 20 | def add_payment(self, payment: str): 21 | self._add_payments.append(payment) 22 | 23 | 24 | create_test_database = init_tests(base_ormar_config) 25 | 26 | 27 | def test_private_attribute(): 28 | sub = Subscription(stripe_subscription_id="2312312sad231") 29 | sub.add_payment("test") 30 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_saving_nullable_fields.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import ormar 4 | import pytest 5 | 6 | from tests.lifespan import init_tests 7 | from tests.settings import create_config 8 | 9 | base_ormar_config = create_config() 10 | 11 | 12 | class PrimaryModel(ormar.Model): 13 | ormar_config = base_ormar_config.copy(tablename="primary_models") 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | name: str = ormar.String(max_length=255, index=True) 17 | some_text: str = ormar.Text() 18 | # NOTE: Removing nullable=True makes the test pass. 19 | some_other_text: Optional[str] = ormar.Text(nullable=True) 20 | 21 | 22 | class SecondaryModel(ormar.Model): 23 | ormar_config = base_ormar_config.copy(tablename="secondary_models") 24 | 25 | id: int = ormar.Integer(primary_key=True) 26 | name: str = ormar.String(max_length=100) 27 | primary_model: PrimaryModel = ormar.ForeignKey( 28 | PrimaryModel, related_name="secondary_models" 29 | ) 30 | 31 | 32 | create_test_database = init_tests(base_ormar_config) 33 | 34 | 35 | @pytest.mark.asyncio 36 | async def test_create_models(): 37 | async with base_ormar_config.database: 38 | async with base_ormar_config.database.transaction(force_rollback=True): 39 | primary = await PrimaryModel( 40 | name="Foo", some_text="Bar", some_other_text="Baz" 41 | ).save() 42 | assert primary.id == 1 43 | 44 | secondary = await SecondaryModel(name="Foo", primary_model=primary).save() 45 | assert secondary.id == 1 46 | assert secondary.primary_model.id == 1 47 | 48 | secondary = await SecondaryModel.objects.get() 49 | assert secondary.name == "Foo" 50 | await secondary.update(name="Updated") 51 | assert secondary.name == "Updated" 52 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_setting_comments_in_db.py: -------------------------------------------------------------------------------- 1 | import ormar 2 | import pytest 3 | from ormar.models import Model 4 | 5 | from tests.lifespan import init_tests 6 | from tests.settings import create_config 7 | 8 | base_ormar_config = create_config() 9 | 10 | 11 | class Comment(Model): 12 | ormar_config = base_ormar_config.copy(tablename="comments") 13 | 14 | test: int = ormar.Integer(primary_key=True, comment="primary key of comments") 15 | test_string: str = ormar.String(max_length=250, comment="test that it works") 16 | 17 | 18 | create_test_database = init_tests(base_ormar_config) 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_comments_are_set_in_db(): 23 | columns = Comment.ormar_config.table.c 24 | for c in columns: 25 | assert c.comment == Comment.ormar_config.model_fields[c.name].comment 26 | -------------------------------------------------------------------------------- /tests/test_model_definition/test_through_model_relation_setup_on_clone.py: -------------------------------------------------------------------------------- 1 | import ormar 2 | import pytest 3 | 4 | from tests.lifespan import init_tests 5 | from tests.settings import create_config 6 | 7 | base_ormar_config = create_config() 8 | 9 | 10 | class Author(ormar.Model): 11 | ormar_config = base_ormar_config.copy(tablename="authors") 12 | 13 | id = ormar.Integer(primary_key=True) 14 | name = ormar.String(max_length=100) 15 | 16 | 17 | class Book(ormar.Model): 18 | ormar_config = base_ormar_config.copy(tablename="books") 19 | 20 | id = ormar.Integer(primary_key=True) 21 | title = ormar.String(max_length=100) 22 | author = ormar.ManyToMany( 23 | Author, 24 | ) 25 | year = ormar.Integer(nullable=True) 26 | 27 | 28 | create_test_database = init_tests(base_ormar_config) 29 | 30 | 31 | @pytest.mark.asyncio 32 | async def test_tables_are_created(): 33 | async with base_ormar_config.database: 34 | assert await Book.objects.all() == [] 35 | -------------------------------------------------------------------------------- /tests/test_model_methods/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_model_methods/__init__.py -------------------------------------------------------------------------------- /tests/test_model_methods/test_excludes_in_load_all.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import ormar 4 | import pytest 5 | 6 | from tests.lifespan import init_tests 7 | from tests.settings import create_config 8 | 9 | base_ormar_config = create_config(force_rollback=True) 10 | 11 | 12 | class JimmyUser(ormar.Model): 13 | ormar_config = base_ormar_config.copy(tablename="jimmy_users") 14 | 15 | id: uuid.UUID = ormar.UUID( 16 | primary_key=True, default=uuid.uuid4(), uuid_format="string" 17 | ) 18 | 19 | 20 | class JimmyProfile(ormar.Model): 21 | ormar_config = base_ormar_config.copy(tablename="jimmy_profiles") 22 | 23 | id: uuid.UUID = ormar.UUID( 24 | primary_key=True, default=uuid.uuid4(), uuid_format="string" 25 | ) 26 | name = ormar.String(max_length=42, default="JimmyProfile") 27 | user: JimmyUser = ormar.ForeignKey(to=JimmyUser) 28 | 29 | 30 | class JimmyAccount(ormar.Model): 31 | ormar_config = base_ormar_config.copy(tablename="jimmy_accounts") 32 | 33 | id: uuid.UUID = ormar.UUID( 34 | primary_key=True, default=uuid.uuid4(), uuid_format="string" 35 | ) 36 | name = ormar.String(max_length=42, default="JimmyAccount") 37 | user: JimmyUser = ormar.ForeignKey(to=JimmyUser) 38 | 39 | 40 | create_test_database = init_tests(base_ormar_config) 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_excluding_one_relation(): 45 | async with base_ormar_config.database: 46 | user = JimmyUser() 47 | await user.save() 48 | 49 | await JimmyAccount(user=user).save() 50 | await JimmyProfile(user=user).save() 51 | 52 | await user.load_all(exclude={"jimmyprofiles"}) 53 | assert hasattr(user.jimmyaccounts[0], "name") 54 | assert len(user.jimmyprofiles) == 0 55 | 56 | 57 | @pytest.mark.asyncio 58 | async def test_excluding_other_relation(): 59 | async with base_ormar_config.database: 60 | user = JimmyUser() 61 | await user.save() 62 | 63 | await JimmyAccount(user=user).save() 64 | await JimmyProfile(user=user).save() 65 | 66 | await user.load_all(exclude={"jimmyaccounts"}) 67 | assert await JimmyProfile.objects.get() 68 | 69 | assert hasattr(user.jimmyprofiles[0], "name") 70 | assert len(user.jimmyaccounts) == 0 71 | -------------------------------------------------------------------------------- /tests/test_model_methods/test_populate_default_values.py: -------------------------------------------------------------------------------- 1 | import ormar 2 | from sqlalchemy import text 3 | 4 | from tests.lifespan import init_tests 5 | from tests.settings import create_config 6 | 7 | base_ormar_config = create_config() 8 | 9 | 10 | class Task(ormar.Model): 11 | ormar_config = base_ormar_config.copy(tablename="tasks") 12 | 13 | id: int = ormar.Integer(primary_key=True) 14 | name: str = ormar.String( 15 | max_length=255, minimum=0, server_default=text("'Default Name'"), nullable=False 16 | ) 17 | points: int = ormar.Integer( 18 | default=0, minimum=0, server_default=text("0"), nullable=False 19 | ) 20 | score: int = ormar.Integer(default=5) 21 | 22 | 23 | create_test_database = init_tests(base_ormar_config) 24 | 25 | 26 | def test_populate_default_values(): 27 | new_kwargs = { 28 | "id": None, 29 | "name": "", 30 | "points": 0, 31 | } 32 | result = Task.populate_default_values(new_kwargs) 33 | 34 | assert result["id"] is None 35 | assert result["name"] == "" 36 | assert result["points"] == 0 37 | assert result["score"] == 5 38 | -------------------------------------------------------------------------------- /tests/test_model_methods/test_save_related_pk_only.py: -------------------------------------------------------------------------------- 1 | import ormar 2 | import pytest 3 | 4 | from tests.lifespan import init_tests 5 | from tests.settings import create_config 6 | 7 | base_ormar_config = create_config() 8 | 9 | 10 | class A(ormar.Model): 11 | ormar_config = base_ormar_config.copy() 12 | 13 | id = ormar.Integer(primary_key=True) 14 | 15 | 16 | class B(ormar.Model): 17 | ormar_config = base_ormar_config.copy() 18 | 19 | id = ormar.Integer(primary_key=True) 20 | a = ormar.ForeignKey(A) 21 | 22 | 23 | create_test_database = init_tests(base_ormar_config) 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_saving_related_pk_only(): 28 | async with base_ormar_config.database: 29 | async with base_ormar_config.database.transaction(force_rollback=True): 30 | a = A() 31 | await a.save() 32 | await a.save_related(follow=True, save_all=True) 33 | -------------------------------------------------------------------------------- /tests/test_model_methods/test_save_related_uuid.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Optional 3 | 4 | import ormar 5 | import pytest 6 | 7 | from tests.lifespan import init_tests 8 | from tests.settings import create_config 9 | 10 | base_ormar_config = create_config() 11 | 12 | 13 | class Department(ormar.Model): 14 | ormar_config = base_ormar_config.copy() 15 | 16 | id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4) 17 | department_name: str = ormar.String(max_length=100) 18 | 19 | 20 | class Course(ormar.Model): 21 | ormar_config = base_ormar_config.copy() 22 | 23 | id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4) 24 | course_name: str = ormar.String(max_length=100) 25 | completed: bool = ormar.Boolean() 26 | department: Optional[Department] = ormar.ForeignKey(Department) 27 | 28 | 29 | class Student(ormar.Model): 30 | ormar_config = base_ormar_config.copy() 31 | 32 | id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4) 33 | name: str = ormar.String(max_length=100) 34 | courses = ormar.ManyToMany(Course) 35 | 36 | 37 | create_test_database = init_tests(base_ormar_config) 38 | 39 | 40 | @pytest.mark.asyncio 41 | async def test_uuid_pk_in_save_related(): 42 | async with base_ormar_config.database: 43 | to_save = { 44 | "department_name": "Ormar", 45 | "courses": [ 46 | { 47 | "course_name": "basic1", 48 | "completed": True, 49 | "students": [{"name": "Abi"}, {"name": "Jack"}], 50 | }, 51 | { 52 | "course_name": "basic2", 53 | "completed": True, 54 | "students": [{"name": "Kate"}, {"name": "Miranda"}], 55 | }, 56 | ], 57 | } 58 | department = Department(**to_save) 59 | await department.save_related(follow=True, save_all=True) 60 | department_check = ( 61 | await Department.objects.select_all(follow=True) 62 | .order_by(Department.courses.students.name.asc()) 63 | .get() 64 | ) 65 | to_exclude = { 66 | "id": ..., 67 | "courses": {"id": ..., "students": {"id", "studentcourse"}}, 68 | } 69 | assert department_check.model_dump(exclude=to_exclude) == to_save 70 | -------------------------------------------------------------------------------- /tests/test_model_methods/test_upsert.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import ormar 4 | import pytest 5 | 6 | from tests.lifespan import init_tests 7 | from tests.settings import create_config 8 | 9 | base_ormar_config = create_config() 10 | 11 | 12 | class Director(ormar.Model): 13 | ormar_config = base_ormar_config.copy(tablename="directors") 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | name: str = ormar.String(max_length=100, nullable=False, name="first_name") 17 | last_name: str = ormar.String(max_length=100, nullable=False, name="last_name") 18 | 19 | 20 | class Movie(ormar.Model): 21 | ormar_config = base_ormar_config.copy(tablename="movies") 22 | 23 | id: int = ormar.Integer(primary_key=True) 24 | name: str = ormar.String(max_length=100, nullable=False, name="title") 25 | year: int = ormar.Integer() 26 | profit: float = ormar.Float() 27 | director: Optional[Director] = ormar.ForeignKey(Director) 28 | 29 | 30 | create_test_database = init_tests(base_ormar_config) 31 | 32 | 33 | @pytest.mark.asyncio 34 | async def test_updating_selected_columns(): 35 | async with base_ormar_config.database: 36 | director1 = await Director(name="Peter", last_name="Jackson").save() 37 | 38 | await Movie( 39 | id=1, name="Lord of The Rings", year=2003, director=director1, profit=1.212 40 | ).upsert() 41 | 42 | with pytest.raises(ormar.NoMatch): 43 | await Movie.objects.get() 44 | 45 | await Movie( 46 | id=1, name="Lord of The Rings", year=2003, director=director1, profit=1.212 47 | ).upsert(__force_save__=True) 48 | lotr = await Movie.objects.get() 49 | assert lotr.year == 2003 50 | assert lotr.name == "Lord of The Rings" 51 | -------------------------------------------------------------------------------- /tests/test_ordering/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_ordering/__init__.py -------------------------------------------------------------------------------- /tests/test_ordering/test_proper_order_of_sorting_apply.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import ormar 4 | import pytest 5 | import pytest_asyncio 6 | 7 | from tests.lifespan import init_tests 8 | from tests.settings import create_config 9 | 10 | base_ormar_config = create_config() 11 | 12 | 13 | class Author(ormar.Model): 14 | ormar_config = base_ormar_config.copy(tablename="authors") 15 | 16 | id: int = ormar.Integer(primary_key=True) 17 | name: str = ormar.String(max_length=100) 18 | 19 | 20 | class Book(ormar.Model): 21 | ormar_config = base_ormar_config.copy(tablename="books", order_by=["-ranking"]) 22 | 23 | id: int = ormar.Integer(primary_key=True) 24 | author: Optional[Author] = ormar.ForeignKey( 25 | Author, orders_by=["name"], related_orders_by=["-year"] 26 | ) 27 | title: str = ormar.String(max_length=100) 28 | year: int = ormar.Integer(nullable=True) 29 | ranking: int = ormar.Integer(nullable=True) 30 | 31 | 32 | create_test_database = init_tests(base_ormar_config) 33 | 34 | 35 | @pytest_asyncio.fixture(autouse=True, scope="function") 36 | async def cleanup(): 37 | yield 38 | async with base_ormar_config.database: 39 | await Book.objects.delete(each=True) 40 | await Author.objects.delete(each=True) 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_default_orders_is_applied_from_reverse_relation(): 45 | async with base_ormar_config.database: 46 | tolkien = await Author(name="J.R.R. Tolkien").save() 47 | hobbit = await Book(author=tolkien, title="The Hobbit", year=1933).save() 48 | silmarillion = await Book( 49 | author=tolkien, title="The Silmarillion", year=1977 50 | ).save() 51 | lotr = await Book( 52 | author=tolkien, title="The Lord of the Rings", year=1955 53 | ).save() 54 | 55 | tolkien = await Author.objects.select_related("books").get() 56 | assert tolkien.books[2] == hobbit 57 | assert tolkien.books[1] == lotr 58 | assert tolkien.books[0] == silmarillion 59 | 60 | tolkien = ( 61 | await Author.objects.select_related("books").order_by("books__title").get() 62 | ) 63 | assert tolkien.books[0] == hobbit 64 | assert tolkien.books[1] == lotr 65 | assert tolkien.books[2] == silmarillion 66 | -------------------------------------------------------------------------------- /tests/test_queries/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_queries/__init__.py -------------------------------------------------------------------------------- /tests/test_queries/test_adding_related.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import ormar 4 | import pytest 5 | 6 | from tests.lifespan import init_tests 7 | from tests.settings import create_config 8 | 9 | base_ormar_config = create_config() 10 | 11 | 12 | class Department(ormar.Model): 13 | ormar_config = base_ormar_config.copy() 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | name: str = ormar.String(max_length=100) 17 | 18 | 19 | class Course(ormar.Model): 20 | ormar_config = base_ormar_config.copy() 21 | 22 | id: int = ormar.Integer(primary_key=True) 23 | name: str = ormar.String(max_length=100) 24 | completed: bool = ormar.Boolean(default=False) 25 | department: Optional[Department] = ormar.ForeignKey(Department) 26 | 27 | 28 | create_test_database = init_tests(base_ormar_config) 29 | 30 | 31 | @pytest.mark.asyncio 32 | async def test_adding_relation_to_reverse_saves_the_child(): 33 | async with base_ormar_config.database: 34 | department = await Department(name="Science").save() 35 | course = Course(name="Math", completed=False) 36 | 37 | await department.courses.add(course) 38 | assert course.pk is not None 39 | assert course.department == department 40 | assert department.courses[0] == course 41 | -------------------------------------------------------------------------------- /tests/test_queries/test_indirect_relations_to_self.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import ormar 4 | import pytest 5 | 6 | from tests.lifespan import init_tests 7 | from tests.settings import create_config 8 | 9 | base_ormar_config = create_config() 10 | 11 | 12 | class Node(ormar.Model): 13 | ormar_config = base_ormar_config.copy(tablename="node") 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | name: str = ormar.String(max_length=120) 17 | type: str = ormar.String(max_length=12, default="FLOW") 18 | created_at: datetime = ormar.DateTime(timezone=True, default=datetime.now) 19 | 20 | 21 | class Edge(ormar.Model): 22 | ormar_config = base_ormar_config.copy(tablename="edge") 23 | 24 | id: str = ormar.String(primary_key=True, max_length=12) 25 | src_node: Node = ormar.ForeignKey(Node, related_name="next_edges") 26 | dst_node: Node = ormar.ForeignKey(Node, related_name="previous_edges") 27 | order: int = ormar.Integer(default=1) 28 | created_at: datetime = ormar.DateTime(timezone=True, default=datetime.now) 29 | 30 | 31 | create_test_database = init_tests(base_ormar_config) 32 | 33 | 34 | @pytest.mark.asyncio 35 | async def test_sort_order_on_main_model(): 36 | async with base_ormar_config.database: 37 | node1 = await Node(name="Node 1").save() 38 | node2 = await Node(name="Node 2").save() 39 | node3 = await Node(name="Node 3").save() 40 | 41 | await Edge(id="Side 1", src_node=node1, dst_node=node2).save() 42 | await Edge(id="Side 2", src_node=node2, dst_node=node3, order=2).save() 43 | await Edge(id="Side 3", src_node=node3, dst_node=node1, order=3).save() 44 | 45 | active_nodes = await Node.objects.select_related( 46 | ["next_edges", "next_edges__dst_node"] 47 | ).all() 48 | 49 | assert len(active_nodes) == 3 50 | assert active_nodes[0].next_edges[0].id == "Side 1" 51 | assert active_nodes[0].next_edges[0].dst_node.type == "FLOW" 52 | -------------------------------------------------------------------------------- /tests/test_queries/test_non_relation_fields_not_merged.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import ormar 4 | import pytest 5 | 6 | from tests.lifespan import init_tests 7 | from tests.settings import create_config 8 | 9 | base_ormar_config = create_config() 10 | 11 | 12 | class Chart(ormar.Model): 13 | ormar_config = base_ormar_config.copy(tablename="authors") 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | datasets = ormar.JSON() 17 | 18 | 19 | class Config(ormar.Model): 20 | ormar_config = base_ormar_config.copy(tablename="books") 21 | 22 | id: int = ormar.Integer(primary_key=True) 23 | chart: Optional[Chart] = ormar.ForeignKey(Chart) 24 | 25 | 26 | create_test_database = init_tests(base_ormar_config) 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_list_field_that_is_not_relation_is_not_merged(): 31 | async with base_ormar_config.database: 32 | chart = await Chart.objects.create(datasets=[{"test": "ok"}]) 33 | await Config.objects.create(chart=chart) 34 | await Config.objects.create(chart=chart) 35 | 36 | chart2 = await Chart.objects.select_related("configs").get() 37 | assert len(chart2.datasets) == 1 38 | assert chart2.datasets == [{"test": "ok"}] 39 | -------------------------------------------------------------------------------- /tests/test_queries/test_quoting_table_names_in_on_join_clause.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | from typing import Dict, Optional, Union 4 | 5 | import ormar 6 | import pytest 7 | 8 | from tests.lifespan import init_tests 9 | from tests.settings import create_config 10 | 11 | base_ormar_config = create_config() 12 | 13 | 14 | class Team(ormar.Model): 15 | ormar_config = base_ormar_config.copy(tablename="team") 16 | 17 | id: uuid.UUID = ormar.UUID(default=uuid.uuid4, primary_key=True, index=True) 18 | name = ormar.Text(nullable=True) 19 | client_id = ormar.Text(nullable=True) 20 | client_secret = ormar.Text(nullable=True) 21 | created_on = ormar.DateTime(timezone=True, default=datetime.datetime.utcnow()) 22 | 23 | 24 | class User(ormar.Model): 25 | ormar_config = base_ormar_config.copy(tablename="user") 26 | 27 | id: uuid.UUID = ormar.UUID(default=uuid.uuid4, primary_key=True, index=True) 28 | client_user_id = ormar.Text() 29 | token = ormar.Text(nullable=True) 30 | team: Optional[Team] = ormar.ForeignKey(to=Team, name="team_id") 31 | 32 | 33 | class Order(ormar.Model): 34 | ormar_config = base_ormar_config.copy(tablename="order") 35 | 36 | id: uuid.UUID = ormar.UUID(default=uuid.uuid4, primary_key=True, index=True) 37 | user: Optional[Union[User, Dict]] = ormar.ForeignKey(User) 38 | 39 | 40 | create_test_database = init_tests(base_ormar_config) 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_quoting_on_clause_without_prefix(): 45 | async with base_ormar_config.database: 46 | await User.objects.select_related("orders").all() 47 | -------------------------------------------------------------------------------- /tests/test_relations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_relations/__init__.py -------------------------------------------------------------------------------- /tests/test_relations/test_relations_default_exception.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | from typing import List, Optional 3 | 4 | import ormar 5 | import pytest 6 | from ormar.exceptions import ModelDefinitionError 7 | 8 | from tests.lifespan import init_tests 9 | from tests.settings import create_config 10 | 11 | base_ormar_config = create_config() 12 | 13 | 14 | class Author(ormar.Model): 15 | ormar_config = base_ormar_config.copy(tablename="authors") 16 | 17 | id: int = ormar.Integer(primary_key=True) 18 | first_name: str = ormar.String(max_length=80) 19 | last_name: str = ormar.String(max_length=80) 20 | 21 | 22 | class Category(ormar.Model): 23 | ormar_config = base_ormar_config.copy(tablename="categories") 24 | 25 | id: int = ormar.Integer(primary_key=True) 26 | name: str = ormar.String(max_length=40) 27 | 28 | 29 | create_test_database = init_tests(base_ormar_config) 30 | 31 | 32 | def test_fk_error(): 33 | with pytest.raises(ModelDefinitionError): 34 | 35 | class Post(ormar.Model): 36 | ormar_config = base_ormar_config.copy(tablename="posts") 37 | 38 | id: int = ormar.Integer(primary_key=True) 39 | title: str = ormar.String(max_length=200) 40 | categories: Optional[List[Category]] = ormar.ManyToMany(Category) 41 | author: Optional[Author] = ormar.ForeignKey(Author, default="aa") 42 | 43 | 44 | def test_m2m_error(): 45 | with pytest.raises(ModelDefinitionError): 46 | 47 | class Post(ormar.Model): 48 | ormar_config = base_ormar_config.copy(tablename="posts") 49 | 50 | id: int = ormar.Integer(primary_key=True) 51 | title: str = ormar.String(max_length=200) 52 | categories: Optional[List[Category]] = ormar.ManyToMany( 53 | Category, default="aa" 54 | ) 55 | -------------------------------------------------------------------------------- /tests/test_relations/test_replacing_models_with_copy.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, Tuple, Union 2 | 3 | import ormar 4 | import pytest 5 | 6 | from tests.lifespan import init_tests 7 | from tests.settings import create_config 8 | 9 | base_ormar_config = create_config() 10 | 11 | 12 | class Album(ormar.Model): 13 | ormar_config = base_ormar_config.copy(tablename="albums") 14 | 15 | id: int = ormar.Integer(primary_key=True) 16 | name: str = ormar.String(max_length=100) 17 | is_best_seller: bool = ormar.Boolean(default=False) 18 | properties: Tuple[str, Any] 19 | score: Union[str, int] 20 | 21 | 22 | class Track(ormar.Model): 23 | ormar_config = base_ormar_config.copy(tablename="tracks") 24 | 25 | id: int = ormar.Integer(primary_key=True) 26 | album: Optional[Album] = ormar.ForeignKey(Album) 27 | title: str = ormar.String(max_length=100) 28 | position: int = ormar.Integer() 29 | play_count: int = ormar.Integer(nullable=True, default=0) 30 | is_disabled: bool = ormar.Boolean(default=False) 31 | properties: Tuple[str, Any] 32 | 33 | 34 | create_test_database = init_tests(base_ormar_config) 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_model_is_replaced_by_a_copy(): 39 | assert Album.model_fields["tracks"].annotation.__args__[1] != Track 40 | assert ( 41 | Album.model_fields["tracks"].annotation.__args__[1].model_fields.keys() 42 | == Track.model_fields.keys() 43 | ) 44 | -------------------------------------------------------------------------------- /tests/test_relations/test_reverse_relation_preserves_validator.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Union 2 | 3 | import ormar 4 | import pytest_asyncio 5 | from pydantic import field_validator 6 | 7 | from tests.lifespan import init_tests 8 | from tests.settings import create_config 9 | 10 | base_ormar_config = create_config() 11 | 12 | 13 | class Author(ormar.Model): 14 | ormar_config = base_ormar_config.copy() 15 | 16 | id: int = ormar.Integer(primary_key=True) 17 | name: str = ormar.String(max_length=80) 18 | 19 | @field_validator("name", mode="before") 20 | @classmethod 21 | def validate_name(cls, v: Union[str, List[str]]) -> str: 22 | if isinstance(v, list): 23 | v = " ".join(v) 24 | return v 25 | 26 | 27 | class Post(ormar.Model): 28 | ormar_config = base_ormar_config.copy() 29 | 30 | id: int = ormar.Integer(primary_key=True) 31 | title: str = ormar.String(max_length=200) 32 | author: Optional[Author] = ormar.ForeignKey(Author) 33 | 34 | 35 | create_test_database = init_tests(base_ormar_config) 36 | 37 | 38 | @pytest_asyncio.fixture(scope="function", autouse=True) 39 | async def cleanup(): 40 | yield 41 | async with base_ormar_config.database: 42 | await Post.objects.delete(each=True) 43 | await Author.objects.delete(each=True) 44 | 45 | 46 | def test_validator(): 47 | author = Author(name=["Test", "Author"]) 48 | assert author.name == "Test Author" 49 | -------------------------------------------------------------------------------- /tests/test_relations/test_saving_related.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import ormar 4 | import pytest 5 | from ormar.exceptions import ModelPersistenceError 6 | 7 | from tests.lifespan import init_tests 8 | from tests.settings import create_config 9 | 10 | base_ormar_config = create_config() 11 | 12 | 13 | class Category(ormar.Model): 14 | ormar_config = base_ormar_config.copy(tablename="categories") 15 | 16 | id: int = ormar.Integer(primary_key=True) 17 | name: str = ormar.String(max_length=50, unique=True, index=True) 18 | code: int = ormar.Integer() 19 | 20 | 21 | class Workshop(ormar.Model): 22 | ormar_config = base_ormar_config.copy(tablename="workshops") 23 | 24 | id: int = ormar.Integer(primary_key=True) 25 | topic: str = ormar.String(max_length=255, index=True) 26 | category: Union[ormar.Model, Category] = ormar.ForeignKey( 27 | Category, related_name="workshops", nullable=False 28 | ) 29 | 30 | 31 | create_test_database = init_tests(base_ormar_config) 32 | 33 | 34 | @pytest.mark.asyncio 35 | async def test_model_relationship(): 36 | async with base_ormar_config.database: 37 | async with base_ormar_config.database.transaction(force_rollback=True): 38 | cat = await Category(name="Foo", code=123).save() 39 | ws = await Workshop(topic="Topic 1", category=cat).save() 40 | 41 | assert ws.id == 1 42 | assert ws.topic == "Topic 1" 43 | assert ws.category.name == "Foo" 44 | 45 | ws.topic = "Topic 2" 46 | await ws.update() 47 | 48 | assert ws.id == 1 49 | assert ws.topic == "Topic 2" 50 | assert ws.category.name == "Foo" 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_model_relationship_with_not_saved(): 55 | async with base_ormar_config.database: 56 | async with base_ormar_config.database.transaction(force_rollback=True): 57 | cat = Category(name="Foo", code=123) 58 | with pytest.raises(ModelPersistenceError): 59 | await Workshop(topic="Topic 1", category=cat).save() 60 | 61 | with pytest.raises(ModelPersistenceError): 62 | await Workshop.objects.create(topic="Topic 1", category=cat) 63 | -------------------------------------------------------------------------------- /tests/test_relations/test_through_relations_fail.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | 3 | import ormar 4 | import pytest 5 | from ormar import ModelDefinitionError 6 | 7 | from tests.lifespan import init_tests 8 | from tests.settings import create_config 9 | 10 | base_ormar_config = create_config() 11 | 12 | 13 | def test_through_with_relation_fails(): 14 | class Category(ormar.Model): 15 | ormar_config = base_ormar_config.copy(tablename="categories") 16 | 17 | id = ormar.Integer(primary_key=True) 18 | name = ormar.String(max_length=40) 19 | 20 | class Blog(ormar.Model): 21 | ormar_config = base_ormar_config.copy() 22 | 23 | id: int = ormar.Integer(primary_key=True) 24 | title: str = ormar.String(max_length=200) 25 | 26 | class PostCategory(ormar.Model): 27 | ormar_config = base_ormar_config.copy(tablename="posts_x_categories") 28 | 29 | id: int = ormar.Integer(primary_key=True) 30 | sort_order: int = ormar.Integer(nullable=True) 31 | param_name: str = ormar.String(default="Name", max_length=200) 32 | blog = ormar.ForeignKey(Blog) 33 | 34 | with pytest.raises(ModelDefinitionError): 35 | 36 | class Post(ormar.Model): 37 | ormar_config = base_ormar_config.copy() 38 | 39 | id: int = ormar.Integer(primary_key=True) 40 | title: str = ormar.String(max_length=200) 41 | categories = ormar.ManyToMany(Category, through=PostCategory) 42 | 43 | 44 | create_test_database = init_tests(base_ormar_config) 45 | -------------------------------------------------------------------------------- /tests/test_relations/test_weakref_checking.py: -------------------------------------------------------------------------------- 1 | import ormar 2 | 3 | from tests.settings import create_config 4 | 5 | base_ormar_config = create_config() 6 | from tests.lifespan import init_tests 7 | 8 | 9 | class Band(ormar.Model): 10 | ormar_config = base_ormar_config.copy(tablename="bands") 11 | 12 | id: int = ormar.Integer(primary_key=True) 13 | name: str = ormar.String(max_length=100) 14 | 15 | 16 | class Artist(ormar.Model): 17 | ormar_config = base_ormar_config.copy(tablename="artists") 18 | 19 | id: int = ormar.Integer(primary_key=True) 20 | name: str = ormar.String(max_length=100) 21 | 22 | band: Band = ormar.ForeignKey(Band) 23 | 24 | 25 | create_test_database = init_tests(base_ormar_config) 26 | 27 | 28 | def test_weakref_init(): 29 | band = Band(name="Band") 30 | artist1 = Artist(name="Artist 1", band=band) 31 | artist2 = Artist(name="Artist 2", band=band) 32 | artist3 = Artist(name="Artist 3", band=band) 33 | 34 | del artist1 35 | Artist( 36 | name="Artist 2", band=band 37 | ) # Force it to check for weakly-referenced objects 38 | del artist3 39 | 40 | band.artists # Force it to clean 41 | 42 | assert len(band.artists) == 1 43 | assert band.artists[0].name == artist2.name 44 | -------------------------------------------------------------------------------- /tests/test_signals/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_signals/__init__.py -------------------------------------------------------------------------------- /tests/test_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collerek/ormar/b1d1fde0cecf55ba65daf1273fd510183284fd25/tests/test_utils/__init__.py -------------------------------------------------------------------------------- /tests/test_utils/test_models_helpers.py: -------------------------------------------------------------------------------- 1 | from ormar.models.helpers.models import group_related_list 2 | 3 | 4 | def test_group_related_list(): 5 | given = [ 6 | "friends__least_favourite_game", 7 | "least_favourite_game", 8 | "friends", 9 | "favourite_game", 10 | "friends__favourite_game", 11 | ] 12 | expected = { 13 | "least_favourite_game": [], 14 | "favourite_game": [], 15 | "friends": ["favourite_game", "least_favourite_game"], 16 | } 17 | assert group_related_list(given) == expected 18 | --------------------------------------------------------------------------------