├── .editorconfig ├── .flake8 ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-issue.md │ └── config.yml ├── dependabot.yml └── workflows │ ├── publish.yml │ └── test-suite.yml ├── .gitignore ├── .pdbrc ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── docker-compose.yml ├── docs ├── connection.md ├── contributing.md ├── declarative-models.md ├── exceptions.md ├── extras.md ├── fields.md ├── index.md ├── inspectdb.md ├── managers.md ├── migrations │ ├── discovery.md │ └── migrations.md ├── models.md ├── overrides │ ├── assets │ │ ├── bootstrap │ │ │ ├── css │ │ │ │ └── bootstrap.min.css │ │ │ └── js │ │ │ │ └── bootstrap.min.js │ │ ├── css │ │ │ ├── bs-theme-overrides.css │ │ │ └── esmerald.css │ │ ├── img │ │ │ └── favicon.ico │ │ └── js │ │ │ ├── esmerald.js │ │ │ └── startup-modern.js │ ├── home.html │ └── nav.html ├── queries │ ├── many-to-many.md │ ├── prefetch.md │ ├── queries.md │ ├── related-name.md │ └── secrets.md ├── references │ ├── database.md │ ├── fields.md │ ├── foreignkey.md │ ├── index.md │ ├── manager.md │ ├── many-to-many.md │ ├── models.md │ ├── one-to-one.md │ ├── queryset.md │ ├── reflect-model.md │ ├── registry.md │ ├── schemas.md │ └── signals.md ├── reflection.md ├── registry.md ├── relationships.md ├── release-notes.md ├── saffier-people.md ├── saffier.md ├── settings.md ├── shell.md ├── signals.md ├── sponsorship.md ├── statics │ ├── css │ │ ├── custom.css │ │ └── extra.css │ ├── images │ │ ├── favicon.ico │ │ └── logo-white.svg │ └── js │ │ └── .gitkeep ├── tenancy │ ├── contrib.md │ └── saffier.md ├── test-client.md ├── tips-and-tricks.md └── transactions.md ├── docs_src ├── commands │ └── discover.py ├── connections │ └── simple.py ├── extras │ └── app.py ├── migrations │ ├── accounts_models.py │ ├── fastapi.py │ ├── lru.py │ ├── migrations.py │ ├── model.py │ ├── starlette.py │ ├── via_dict.py │ ├── via_list.py │ └── via_tuple.py ├── models │ ├── abstract │ │ ├── common.py │ │ └── simple.py │ ├── declarative │ │ ├── example.py │ │ └── fk_relationship.py │ ├── declaring_models.py │ ├── declaring_models_no_id.py │ ├── declaring_models_pk_no_id.py │ ├── default_model.py │ ├── indexes │ │ ├── complex_together.py │ │ ├── simple.py │ │ └── simple2.py │ ├── managers │ │ ├── custom.py │ │ ├── override.py │ │ └── simple.py │ ├── pk_no_default.py │ ├── pk_with_default.py │ ├── registry │ │ ├── inheritance_abstract.py │ │ ├── inheritance_no_repeat.py │ │ └── nutshell.py │ ├── tablename │ │ ├── model_diff_tn.py │ │ ├── model_no_tablename.py │ │ └── model_with_tablename.py │ └── unique_together │ │ ├── complex_combined.py │ │ ├── complex_independent.py │ │ ├── complex_mixed.py │ │ ├── complex_together.py │ │ ├── constraints │ │ ├── complex.py │ │ └── mixing.py │ │ ├── simple.py │ │ └── simple2.py ├── prefetch │ ├── first │ │ ├── asserting.py │ │ ├── data.py │ │ ├── models.py │ │ └── prefetch.py │ └── second │ │ ├── data.py │ │ ├── models.py │ │ ├── prefetch.py │ │ └── prefetch_filtered.py ├── queries │ ├── clauses │ │ ├── and.py │ │ ├── and_m_filter.py │ │ ├── and_two.py │ │ ├── model.py │ │ ├── not.py │ │ ├── not_m_filter.py │ │ ├── not_two.py │ │ ├── or.py │ │ ├── or_m_filter.py │ │ ├── or_two.py │ │ └── style │ │ │ ├── and.py │ │ │ ├── and_m_filter.py │ │ │ ├── and_two.py │ │ │ ├── model.py │ │ │ ├── not.py │ │ │ ├── not_m_filter.py │ │ │ ├── not_two.py │ │ │ ├── or.py │ │ │ ├── or_m_filter.py │ │ │ └── or_two.py │ ├── manytomany │ │ ├── example.py │ │ ├── no_rel.py │ │ ├── no_rel_query_example.py │ │ └── query_example.py │ ├── model.py │ ├── related_name │ │ ├── data.py │ │ ├── example.py │ │ ├── models.py │ │ ├── new_data.py │ │ └── new_models.py │ └── secrets │ │ └── model.py ├── quickstart │ └── example1.py ├── reflection │ ├── model.py │ ├── reflect.py │ └── reflect │ │ ├── model.py │ │ └── reflect.py ├── registry │ ├── create_schema.py │ ├── custom_registry.py │ ├── default_schema.py │ ├── drop_schema.py │ ├── extra │ │ ├── create.py │ │ └── declaration.py │ ├── model.py │ └── multiple.py ├── relationships │ ├── model.py │ ├── multiple.py │ └── onetoone.py ├── settings │ └── custom_settings.py ├── shared │ └── extra.md ├── signals │ ├── custom.py │ ├── disconnect.py │ ├── logic.py │ ├── receiver │ │ ├── disconnect.py │ │ ├── model.py │ │ ├── multiple.py │ │ ├── multiple_receivers.py │ │ ├── post_multiple.py │ │ └── post_save.py │ └── register.py ├── tenancy │ ├── contrib │ │ ├── domain_mixin.py │ │ ├── example │ │ │ ├── api.py │ │ │ ├── app.py │ │ │ ├── middleware.py │ │ │ ├── mock_data.py │ │ │ ├── models.py │ │ │ ├── queries.py │ │ │ └── settings.py │ │ ├── tenant_mixin.py │ │ ├── tenant_model.py │ │ ├── tenant_registry.py │ │ └── tenant_user_mixin.py │ ├── example │ │ ├── api.py │ │ ├── data.py │ │ ├── middleware.py │ │ ├── models.py │ │ └── query.py │ └── using │ │ └── schemas.py ├── testclient │ └── tests.py ├── tips │ ├── connection.py │ ├── lru.py │ ├── migrations.py │ ├── models.py │ └── settings.py └── transactions │ ├── context_manager.py │ ├── decorator.py │ └── models.py ├── mkdocs.yml ├── pyproject.toml ├── saffier ├── __init__.py ├── __main__.py ├── cli │ ├── __init__.py │ ├── base.py │ ├── cli.py │ ├── constants.py │ ├── decorators.py │ ├── env.py │ ├── operations │ │ ├── __init__.py │ │ ├── branches.py │ │ ├── check.py │ │ ├── current.py │ │ ├── downgrade.py │ │ ├── edit.py │ │ ├── heads.py │ │ ├── history.py │ │ ├── init.py │ │ ├── inspectdb.py │ │ ├── list_templates.py │ │ ├── makemigrations.py │ │ ├── merge.py │ │ ├── migrate.py │ │ ├── revision.py │ │ ├── shell │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── enums.py │ │ │ ├── ipython.py │ │ │ ├── ptpython.py │ │ │ └── utils.py │ │ ├── show.py │ │ └── stamp.py │ └── templates │ │ └── default │ │ ├── README │ │ ├── alembic.ini.mako │ │ ├── env.py │ │ └── script.py.mako ├── conf │ ├── __init__.py │ ├── enums.py │ ├── functional.py │ ├── global_settings.py │ └── module_import.py ├── contrib │ ├── __init__.py │ ├── multi_tenancy │ │ ├── __init__.py │ │ ├── base.py │ │ ├── exceptions.py │ │ ├── metaclasses.py │ │ ├── models.py │ │ ├── registry.py │ │ ├── settings.py │ │ └── utils.py │ └── sqlalchemy │ │ ├── __init__.py │ │ ├── fields.py │ │ ├── protocols.py │ │ └── types.py ├── core │ ├── __init__.py │ ├── connection │ │ ├── __init__.py │ │ ├── database.py │ │ ├── registry.py │ │ └── schemas.py │ ├── datastructures.py │ ├── db │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── context_vars.py │ │ ├── datastructures.py │ │ ├── fields │ │ │ ├── __init__.py │ │ │ ├── _internal.py │ │ │ └── base.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── managers.py │ │ │ ├── metaclasses.py │ │ │ ├── mixins │ │ │ │ ├── __init__.py │ │ │ │ └── generics.py │ │ │ ├── model.py │ │ │ ├── model_proxy.py │ │ │ ├── row.py │ │ │ └── utils.py │ │ ├── querysets │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── clauses.py │ │ │ ├── mixins.py │ │ │ ├── prefetch.py │ │ │ └── protocols.py │ │ └── relationships │ │ │ ├── __init__.py │ │ │ ├── related.py │ │ │ └── relation.py │ ├── events.py │ ├── extras │ │ ├── __init__.py │ │ ├── base.py │ │ └── extra.py │ ├── signals │ │ ├── __init__.py │ │ ├── handlers.py │ │ └── signal.py │ ├── sync.py │ ├── terminal │ │ ├── __init__.py │ │ ├── base.py │ │ ├── print.py │ │ └── terminal.py │ └── utils │ │ ├── __init__.py │ │ ├── base.py │ │ ├── formats.py │ │ ├── models.py │ │ ├── schemas.py │ │ ├── sync.py │ │ └── unique.py ├── exceptions.py ├── protocols │ ├── __init__.py │ ├── many_relationship.py │ └── queryset.py ├── py.typed ├── testclient.py ├── types.py └── utils │ ├── __init__.py │ ├── compat.py │ └── inspect.py ├── scripts ├── build ├── check ├── clean ├── coverage ├── docs ├── install ├── lint ├── release ├── sync-version └── test └── tests ├── clauses ├── __init__.py ├── test_and_clauses.py ├── test_not_clauses.py └── test_or_clauses.py ├── cli ├── conftest.py ├── custom │ ├── README │ ├── alembic.ini.mako │ ├── env.py │ └── script.py.mako ├── main.py ├── main_extra.py ├── test_custom_template.py ├── test_custom_template_with_flag.py ├── test_inspectdb.py ├── test_saffier_extra.py └── utils.py ├── conftest.py ├── contrib ├── __init__.py └── multi_tenancy │ ├── test_mt_models.py │ ├── test_tenant_models_using.py │ └── test_tenant_models_working.py ├── exclude_secrets └── test_exclude.py ├── foreign_keys ├── m2m_string │ ├── __init__.py │ ├── test_many_to_many.py │ ├── test_many_to_many_field.py │ ├── test_many_to_many_field_related_name.py │ └── test_many_to_many_related_name.py ├── test_fk_related_name.py ├── test_foreignkey.py ├── test_many_to_many.py ├── test_many_to_many_field.py ├── test_many_to_many_field_related_name.py └── test_many_to_many_related_name.py ├── indexes ├── test_indexes.py └── test_indexes_errors.py ├── integration ├── test_esmerald_tenant.py └── test_esmerald_tenant_user.py ├── managers ├── test_managers.py ├── test_managers_abstract.py ├── test_managers_abstract_multiple.py ├── test_managers_inherited.py └── test_queryset.py ├── metaclass ├── test_meta.py └── test_meta_errors.py ├── models ├── run_sync │ ├── __init__.py │ ├── test_bulk_create.py │ ├── test_bulk_update.py │ ├── test_model_abstract.py │ ├── test_model_class.py │ ├── test_model_count.py │ ├── test_model_distinct.py │ ├── test_model_exists.py │ ├── test_model_first.py │ ├── test_model_get_or_create.py │ ├── test_model_get_or_none.py │ ├── test_model_group_by.py │ ├── test_model_inheritance.py │ ├── test_model_last.py │ ├── test_model_limit.py │ ├── test_model_multiple_inheritance.py │ ├── test_model_offset.py │ ├── test_model_order_by.py │ ├── test_model_primary_key.py │ ├── test_model_queryset_delete.py │ ├── test_model_queryset_update.py │ ├── test_model_registry.py │ ├── test_model_search.py │ ├── test_model_sqlalchmy.py │ ├── test_model_sync.py │ ├── test_model_update_or_create.py │ ├── test_model_values.py │ ├── test_model_values_list.py │ ├── test_models_filter.py │ └── test_save.py ├── test_bulk_create.py ├── test_bulk_update.py ├── test_datetime.py ├── test_inner_select.py ├── test_model_abstract.py ├── test_model_class.py ├── test_model_count.py ├── test_model_defer.py ├── test_model_distinct.py ├── test_model_exists.py ├── test_model_first.py ├── test_model_get_or_create.py ├── test_model_get_or_none.py ├── test_model_group_by.py ├── test_model_inheritance.py ├── test_model_last.py ├── test_model_limit.py ├── test_model_multiple_inheritance.py ├── test_model_offset.py ├── test_model_only.py ├── test_model_order_by.py ├── test_model_primary_key.py ├── test_model_primary_key_error.py ├── test_model_proxy.py ├── test_model_queryset_delete.py ├── test_model_queryset_update.py ├── test_model_registry.py ├── test_model_search.py ├── test_model_sqlalchmy.py ├── test_model_update_or_create.py ├── test_model_values.py ├── test_model_values_list.py ├── test_models_filter.py ├── test_save.py ├── test_select_related_mul.py └── test_select_related_single.py ├── prefetch ├── test_prefetch_error.py ├── test_prefetch_multiple.py ├── test_prefetch_object.py ├── test_prefetch_related_nested.py └── test_prefetch_related_with_select_related.py ├── reflection └── test_table_reflection.py ├── registry ├── test_different_registry_default.py ├── test_registries.py └── test_registry.py ├── settings.py ├── signals └── test_signals.py ├── tenancy ├── test_activate_tenant.py ├── test_activate_tenant_precedent.py ├── test_load.py ├── test_mt_bulk_create.py ├── test_mt_bulk_create_different_db.py ├── test_raise_assertation_error.py ├── test_select_related.py ├── test_select_related_multiple.py ├── test_select_related_two.py └── test_tenancy_queries.py ├── test_columns.py ├── test_migrate.py └── uniques ├── test_unique.py ├── test_unique_constraint.py └── test_unique_together.py /.editorconfig: -------------------------------------------------------------------------------- 1 | ; More information at http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = false 11 | 12 | # See Google Shell Style Guide 13 | # https://google.github.io/styleguide/shell.xml 14 | [*.sh] 15 | indent_size = 2 # shfmt: like -i 2 16 | insert_final_newline = true 17 | switch_case_indent = true # shfmt: like -ci 18 | 19 | [*.py] 20 | insert_final_newline = true 21 | indent_size = 4 22 | max_line_length = 120 23 | 24 | [*.md] 25 | trim_trailing_whitespace = false 26 | indent_size = 4 27 | 28 | [*.yml] 29 | indent_size = 2 30 | 31 | [Makefile] 32 | indent_style = tab 33 | 34 | [*.js] 35 | indent_size = 4 36 | insert_final_newline = true 37 | 38 | [*.ts] 39 | insert_final_newline = true 40 | 41 | [*.scss] 42 | insert_final_newline = true 43 | 44 | [*.json] 45 | indent_size = 2 46 | 47 | [*.html] 48 | insert_final_newline = true -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 99 3 | max-complexity = 12 4 | ignore = E501, E203, B008, W503, C408, B009, B023, C417, C901, PT006, PT007, PT004, PT012, SIM401 5 | select = C,E,F,W,B,B9 6 | exclude = __init__.py 7 | type-checking-pydantic-enabled = true 8 | type-checking-fastapi-enabled = true 9 | classmethod-decorators = 10 | classmethod 11 | validator 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ["tarsil"] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: Please only raise an issue if you've been advised to do so after discussion. Much appreciated! 🙏 4 | --- 5 | 6 | The starting point for issues should usually be a discussion... 7 | 8 | https://github.com/tarsil/saffier/discussions 9 | 10 | Potential bugs may be raised as a "Potential Issue" discussion. The feature requests may be raised as an 11 | "Ideas" discussion. 12 | 13 | We can then decide if the discussion needs to be escalated into an "Issue" or not. 14 | 15 | This will make sure that everything is organised properly. 16 | --- 17 | 18 | **Saffier version**: 19 | **Python version**: 20 | **OS**: 21 | **Platform**: 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser 2 | blank_issues_enabled: false 3 | contact_links: 4 | - name: Discussions 5 | url: https://github.com/tarsil/saffier/discussions 6 | about: > 7 | The "Discussions" forum is where you want to start. 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | commit-message: 8 | prefix: ⬆ 9 | - package-ecosystem: "pip" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | commit-message: 14 | prefix: ⬆ 15 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | publish: 10 | name: "Publish release" 11 | runs-on: "ubuntu-latest" 12 | 13 | environment: 14 | name: deploy 15 | 16 | steps: 17 | - uses: "actions/checkout@v4" 18 | - uses: "actions/setup-python@v5" 19 | with: 20 | python-version: 3.8 21 | - name: "Install dependencies" 22 | run: "scripts/install" 23 | - name: Install build dependencies 24 | if: steps.cache.outputs.cache-hit != 'true' 25 | run: pip install build twine 26 | - name: "Build package" 27 | run: "scripts/build" 28 | - name: "Publish to PyPI" 29 | run: "scripts/release" 30 | env: 31 | TWINE_USERNAME: __token__ 32 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 33 | - name: "Deploy docs" 34 | run: curl -X POST '${{ secrets.DEPLOY_DOCS }}' 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # folders 2 | *.egg-info/ 3 | .hypothesis/ 4 | .idea/ 5 | .mypy_cache/ 6 | .pytest_cache/ 7 | .scannerwork/ 8 | .tox/ 9 | .venv/ 10 | .vscode/ 11 | __pycache__/ 12 | virtualenv/ 13 | build/ 14 | dist/ 15 | node_modules/ 16 | results/ 17 | site/ 18 | target/ 19 | venv 20 | 21 | # files 22 | **/*.so 23 | **/*.sqlite 24 | *.iml 25 | .DS_Store 26 | .coverage 27 | .python-version 28 | coverage.* 29 | example.sqlite 30 | -------------------------------------------------------------------------------- /.pdbrc: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | alias kkk os.system('kill -9 %d' % os.getpid()) 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information. 2 | # See https://pre-commit.com/hooks.html for more hooks. 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.3.0 6 | hooks: 7 | - id: check-added-large-files 8 | - id: check-toml 9 | - id: check-yaml 10 | args: 11 | - --unsafe 12 | - id: end-of-file-fixer 13 | - id: trailing-whitespace 14 | - repo: https://github.com/asottile/pyupgrade 15 | rev: v2.37.3 16 | hooks: 17 | - id: pyupgrade 18 | args: 19 | - --py3-plus 20 | - --keep-runtime-typing 21 | - repo: https://github.com/charliermarsh/ruff-pre-commit 22 | rev: v0.3.0 23 | hooks: 24 | - id: ruff 25 | args: ["--fix", "--line-length=99"] 26 | - repo: https://github.com/psf/black 27 | rev: 24.4.1 28 | hooks: 29 | - id: black 30 | args: ["--line-length=99"] 31 | ci: 32 | autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks 33 | autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please read the [Contributing](https://saffier.tarsild.io/contributing/) 4 | guidelines in the official documentation. 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Tiago Silva 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | .PHONY: help 4 | help: 5 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 6 | 7 | .PHONY: clean 8 | clean: clean_pyc ## Clean all PYC in the system 9 | 10 | .PHONY: clean_pyc 11 | clean_pyc: ## Cleans all *.pyc in the system 12 | find . -type f -name "*.pyc" -delete || true 13 | 14 | .PHONY: clean_pycache 15 | clean_pycache: ## Removes the __pycaches__ 16 | find . -type d -name "*__pycache__*" -delete 17 | 18 | .PHONY: serve-docs 19 | serve-docs: ## Runs the local docs 20 | mkdocs serve 21 | 22 | .PHONY: build-docs 23 | build-docs: ## Runs the local docs 24 | mkdocs build 25 | 26 | .PHONY: test 27 | test: ## Runs the tests 28 | pytest $(TESTONLY) --disable-pytest-warnings -s -vv 29 | 30 | .PHONY: coverage 31 | coverage: ## Run tests and coverage 32 | pytest --cov=saffier --cov=tests --cov-report=term-missing:skip-covered --cov-report=html tests 33 | 34 | .PHONY: requirements 35 | requirements: ## Install requirements for development 36 | pip install -e .[all,dev,test,doc,postgres,mysql,sqlite,testing] 37 | 38 | 39 | ifndef VERBOSE 40 | .SILENT: 41 | endif 42 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you believe you have encountered a security vulnerability in any of the tarsil's projects, 4 | please **do not open a public issue** 5 | 6 | To report a security issue in a responsible way, please navigate to the Security tab for the repo and click "Report a vulnerability." 7 | 8 | ![image](https://user-images.githubusercontent.com/11027931/212678219-c63df1a5-bd91-40bd-88c3-6ad5e2a180f4.png) 9 | 10 | Make sure you provide as much detail as you can and that includes: 11 | 12 | * Details of the vulnarability 13 | * OS Platform 14 | * Reproducible cases/examples 15 | 16 | This helps the maintainers to filter, fix and assure the issues are addressed fast. 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | db: 4 | restart: always 5 | image: postgres:15.3 6 | environment: 7 | POSTGRES_HOST_AUTH_METHOD: trust 8 | POSTGRES_USER: "postgres" 9 | POSTGRES_PASSWORD: "postgres" 10 | POSTGRES_DB: "saffier" 11 | expose: 12 | - "5432" 13 | volumes: 14 | - "saffier:/var/lib/postgresql/data" 15 | command: >- 16 | --jit=false 17 | ports: 18 | - "5432:5432" 19 | 20 | saffier_alt: 21 | restart: always 22 | image: postgres:12.3 23 | environment: 24 | POSTGRES_HOST_AUTH_METHOD: trust 25 | POSTGRES_USER: "postgres" 26 | POSTGRES_PASSWORD: "postgres" 27 | POSTGRES_DB: "saffier_alt" 28 | volumes: 29 | - "saffier_alt:/var/lib/postgresql/data" 30 | command: >- 31 | --jit=false 32 | ports: 33 | - "5433:5432" 34 | 35 | volumes: 36 | saffier: 37 | external: true 38 | saffier_alt: 39 | external: true 40 | -------------------------------------------------------------------------------- /docs/exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | 3 | All **Saffier** custom exceptions derive from the base `SaffierException`. 4 | 5 | ## ObjectNotFound 6 | 7 | Raised when querying a model instance and it does not exist. 8 | 9 | ```python 10 | from saffier.exceptions import ObjectNotFound 11 | ``` 12 | 13 | Or simply: 14 | 15 | ```python 16 | from saffier import ObjectNotFound 17 | ``` 18 | 19 | ## MultipleObjectsReturned 20 | 21 | Raised when querying a model and returns multiple results for the given query result. 22 | 23 | ```python 24 | from saffier.exceptions import MultipleObjectsReturned 25 | ``` 26 | 27 | Or simply: 28 | 29 | ```python 30 | from saffier import MultipleObjectsReturned 31 | ``` 32 | 33 | ## ValidationError 34 | 35 | Raised when a validation error is thrown. 36 | 37 | ```python 38 | from saffier.exceptions import ValidationError 39 | ``` 40 | 41 | Or simply: 42 | 43 | ```python 44 | from saffier import ValidationError 45 | ``` 46 | 47 | ## ImproperlyConfigured 48 | 49 | Raised when misconfiguration in the models and metaclass is passed. 50 | 51 | ```python 52 | from saffier.exceptions import ImproperlyConfigured 53 | ``` 54 | 55 | Or simply: 56 | 57 | ```python 58 | from saffier import ImproperlyConfigured 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | template: home.html 3 | title: Saffier | The only python ORM you will ever need 4 | --- 5 | 6 | Welcome to Saffier. 7 | -------------------------------------------------------------------------------- /docs/overrides/assets/css/bs-theme-overrides.css: -------------------------------------------------------------------------------- 1 | :root, [data-bs-theme=light] { 2 | --bs-primary: #ab47bd; 3 | --bs-primary-rgb: 171,71,189; 4 | --bs-primary-text-emphasis: #441C4C; 5 | --bs-primary-bg-subtle: #EEDAF2; 6 | --bs-primary-border-subtle: #DDB5E5; 7 | } 8 | 9 | .btn-primary { 10 | --bs-btn-color: #fff; 11 | --bs-btn-bg: #ab47bd; 12 | --bs-btn-border-color: #ab47bd; 13 | --bs-btn-hover-color: #fff; 14 | --bs-btn-hover-bg: #913CA1; 15 | --bs-btn-hover-border-color: #893997; 16 | --bs-btn-focus-shadow-rgb: 242,227,245; 17 | --bs-btn-active-color: #fff; 18 | --bs-btn-active-bg: #893997; 19 | --bs-btn-active-border-color: #80358E; 20 | --bs-btn-disabled-color: #fff; 21 | --bs-btn-disabled-bg: #ab47bd; 22 | --bs-btn-disabled-border-color: #ab47bd; 23 | } 24 | 25 | .btn-outline-primary { 26 | --bs-btn-color: #ab47bd; 27 | --bs-btn-border-color: #ab47bd; 28 | --bs-btn-focus-shadow-rgb: 171,71,189; 29 | --bs-btn-hover-color: #fff; 30 | --bs-btn-hover-bg: #ab47bd; 31 | --bs-btn-hover-border-color: #ab47bd; 32 | --bs-btn-active-color: #fff; 33 | --bs-btn-active-bg: #ab47bd; 34 | --bs-btn-active-border-color: #ab47bd; 35 | --bs-btn-disabled-color: #ab47bd; 36 | --bs-btn-disabled-bg: transparent; 37 | --bs-btn-disabled-border-color: #ab47bd; 38 | } 39 | -------------------------------------------------------------------------------- /docs/overrides/assets/css/esmerald.css: -------------------------------------------------------------------------------- 1 | .bs-icon.bs-icon-secondary { 2 | color: var(--bs-primary); 3 | background: var(--bs-primary); 4 | } 5 | 6 | .underline:after { 7 | content: ""; 8 | position: absolute; 9 | bottom: -2px; 10 | left: 0; 11 | width: 100%; 12 | height: 8px; 13 | border-radius: 5px; 14 | background: var(--bs-primary); 15 | z-index: -1; 16 | } 17 | -------------------------------------------------------------------------------- /docs/overrides/assets/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/docs/overrides/assets/img/favicon.ico -------------------------------------------------------------------------------- /docs/overrides/assets/js/esmerald.js: -------------------------------------------------------------------------------- 1 | hljs.highlightAll(); 2 | -------------------------------------------------------------------------------- /docs/overrides/assets/js/startup-modern.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; // Start of use strict 3 | 4 | var mainNav = document.querySelector('#mainNav'); 5 | 6 | if (mainNav) { 7 | 8 | // Collapse Navbar 9 | var collapseNavbar = function() { 10 | 11 | var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop; 12 | 13 | if (scrollTop > 100) { 14 | mainNav.classList.add("navbar-shrink"); 15 | } else { 16 | mainNav.classList.remove("navbar-shrink"); 17 | } 18 | }; 19 | // Collapse now if page is not at top 20 | collapseNavbar(); 21 | // Collapse the navbar when page is scrolled 22 | document.addEventListener("scroll", collapseNavbar); 23 | } 24 | 25 | })(); // End of use strict 26 | -------------------------------------------------------------------------------- /docs/overrides/nav.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block outdated %} You're not viewing the latest 2 | version. 3 | 4 | Click here to go to latest. 5 | 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /docs/references/database.md: -------------------------------------------------------------------------------- 1 | # **`Database`** class 2 | 3 | 4 | ::: saffier.Database 5 | options: 6 | filters: 7 | - "!^model_config" 8 | - "!^__slots__" 9 | - "!^__getattr__" 10 | - "!^__aenter__" 11 | - "!^__aexit__" 12 | - "!^SUPPORTED_BACKENDS" 13 | - "!^DIRECT_URL_SCHEME" 14 | - "!^MANDATORY_FIELDS" 15 | -------------------------------------------------------------------------------- /docs/references/fields.md: -------------------------------------------------------------------------------- 1 | # **`BaseField`** class 2 | 3 | 4 | ::: saffier.core.db.fields.base.Field 5 | options: 6 | filters: 7 | - "!^model_config" 8 | - "!^__slots__" 9 | - "!^__getattr__" 10 | -------------------------------------------------------------------------------- /docs/references/foreignkey.md: -------------------------------------------------------------------------------- 1 | # **`ForeignKey`** class 2 | 3 | 4 | ::: saffier.ForeignKey 5 | options: 6 | filters: 7 | - "!^_type" 8 | - "!^model_config" 9 | - "!^__slots__" 10 | - "!^__getattr__" 11 | -------------------------------------------------------------------------------- /docs/references/index.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | Here lies all the useful API references for the classes, functions and attributes of all 4 | parts you can use in your application development. 5 | 6 | If you want to start with **Saffier** you should always [start here](https://saffier.tarsild.io/saffier). 7 | -------------------------------------------------------------------------------- /docs/references/manager.md: -------------------------------------------------------------------------------- 1 | # **`Manager`** class 2 | 3 | 4 | ::: saffier.Manager 5 | options: 6 | filters: 7 | - "!^model_config" 8 | - "!^__slots__" 9 | - "!^__getattr__" 10 | -------------------------------------------------------------------------------- /docs/references/many-to-many.md: -------------------------------------------------------------------------------- 1 | # **`ManyToMany`** class 2 | 3 | 4 | ::: saffier.ManyToManyField 5 | options: 6 | filters: 7 | - "!^model_config" 8 | - "!^__slots__" 9 | - "!^__getattr__" 10 | - "!^_type" 11 | -------------------------------------------------------------------------------- /docs/references/models.md: -------------------------------------------------------------------------------- 1 | # **`Model`** class 2 | 3 | 4 | ::: saffier.Model 5 | options: 6 | filters: 7 | - "!^model_config" 8 | - "!^__dict__" 9 | - "!^__repr__" 10 | - "!^__str__" 11 | -------------------------------------------------------------------------------- /docs/references/one-to-one.md: -------------------------------------------------------------------------------- 1 | # **`OneToOne`** class 2 | 3 | 4 | ::: saffier.OneToOneField 5 | options: 6 | filters: 7 | - "!^_type" 8 | - "!^model_config" 9 | - "!^__slots__" 10 | - "!^__getattr__" 11 | - "!^__new__" 12 | -------------------------------------------------------------------------------- /docs/references/queryset.md: -------------------------------------------------------------------------------- 1 | # **`QuerySet`** class 2 | 3 | ::: saffier.QuerySet 4 | options: 5 | filters: 6 | - "!^model_config" 7 | - "!^__slots__" 8 | - "!^__await__" 9 | - "!^__class_getitem__" 10 | - "!^__get__" 11 | - "!^ESCAPE_CHARACTERS" 12 | - "!^m2m_related" 13 | - "!^pkname" 14 | - "!^_m2m_related" 15 | -------------------------------------------------------------------------------- /docs/references/reflect-model.md: -------------------------------------------------------------------------------- 1 | # **`ReflectModel`** class 2 | 3 | 4 | ::: saffier.ReflectModel 5 | options: 6 | filters: 7 | - "!^model_config" 8 | - "!^__dict__" 9 | - "!^__repr__" 10 | - "!^__str__" 11 | -------------------------------------------------------------------------------- /docs/references/registry.md: -------------------------------------------------------------------------------- 1 | # **`Registry`** class 2 | 3 | 4 | ::: saffier.Registry 5 | options: 6 | filters: 7 | - "!^model_config" 8 | - "!^__slots__" 9 | - "!^__getattr__" 10 | -------------------------------------------------------------------------------- /docs/references/schemas.md: -------------------------------------------------------------------------------- 1 | # **`Schema`** class 2 | 3 | 4 | ::: saffier.core.connection.schemas.Schema 5 | options: 6 | filters: 7 | - "!^model_config" 8 | - "!^__slots__" 9 | - "!^__getattr__" 10 | - "!^__aenter__" 11 | - "!^__aexit__" 12 | - "!^SUPPORTED_BACKENDS" 13 | - "!^DIRECT_URL_SCHEME" 14 | - "!^MANDATORY_FIELDS" 15 | -------------------------------------------------------------------------------- /docs/references/signals.md: -------------------------------------------------------------------------------- 1 | # **`Signal`** class 2 | 3 | 4 | ::: saffier.Signal 5 | options: 6 | filters: 7 | - "!^model_config" 8 | - "!^__slots__" 9 | - "!^__getattr__" 10 | -------------------------------------------------------------------------------- /docs/saffier-people.md: -------------------------------------------------------------------------------- 1 | # Saffier People 2 | 3 | ## The Special Ones 4 | 5 | Currently there are no special ones but we are hoping this will change soon. 6 | 7 | ## The Legends 8 | 9 | Currently there are no legends but we are hoping this will change soon. 10 | -------------------------------------------------------------------------------- /docs/statics/css/custom.css: -------------------------------------------------------------------------------- 1 | .text-justify { 2 | text-align: justify; 3 | } 4 | -------------------------------------------------------------------------------- /docs/statics/css/extra.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/docs/statics/css/extra.css -------------------------------------------------------------------------------- /docs/statics/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/docs/statics/images/favicon.ico -------------------------------------------------------------------------------- /docs/statics/js/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/docs/statics/js/.gitkeep -------------------------------------------------------------------------------- /docs_src/commands/discover.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | from esmerald import Esmerald, Include 7 | from my_project.utils import get_db_connection 8 | 9 | from saffier.cli import Migrate 10 | 11 | 12 | def build_path(): 13 | """ 14 | Builds the path of the project and project root. 15 | """ 16 | Path(__file__).resolve().parent.parent 17 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__)) 18 | 19 | if SITE_ROOT not in sys.path: 20 | sys.path.append(SITE_ROOT) 21 | sys.path.append(os.path.join(SITE_ROOT, "apps")) 22 | 23 | 24 | def get_application(): 25 | """ 26 | This is optional. The function is only used for organisation purposes. 27 | """ 28 | build_path() 29 | database, registry = get_db_connection() 30 | 31 | app = Esmerald( 32 | routes=[Include(namespace="my_project.urls")], 33 | ) 34 | 35 | Migrate(app=app, registry=registry) 36 | return app 37 | 38 | 39 | app = get_application() 40 | -------------------------------------------------------------------------------- /docs_src/connections/simple.py: -------------------------------------------------------------------------------- 1 | from esmerald import Esmerald 2 | 3 | from saffier import Database, Registry 4 | 5 | database = Database("sqlite:///db.sqlite") 6 | models = Registry(database=database) 7 | 8 | 9 | app = Esmerald( 10 | routes=[...], 11 | on_startup=[database.connect], 12 | on_shutdown=[database.disconnect], 13 | ) 14 | -------------------------------------------------------------------------------- /docs_src/extras/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Generated by 'esmerald-admin createproject' 4 | """ 5 | import os 6 | import sys 7 | from pathlib import Path 8 | 9 | from esmerald import Esmerald, Include 10 | 11 | import saffier 12 | from saffier import Database, Registry, SaffierExtra 13 | 14 | database = Database("sqlite:///db.sqlite") 15 | registry = Registry(database) 16 | 17 | 18 | class CustomModel(saffier.Model): 19 | name = saffier.CharField(max_length=255) 20 | email = saffier.EmailField(max_length=255) 21 | 22 | class Meta: 23 | registry = registry 24 | 25 | 26 | def build_path(): 27 | """ 28 | Builds the path of the project and project root. 29 | """ 30 | Path(__file__).resolve().parent.parent 31 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__)) 32 | 33 | if SITE_ROOT not in sys.path: 34 | sys.path.append(SITE_ROOT) 35 | sys.path.append(os.path.join(SITE_ROOT, "apps")) 36 | 37 | 38 | def get_application(): 39 | """ 40 | This is optional. The function is only used for organisation purposes. 41 | """ 42 | 43 | app = Esmerald( 44 | routes=[Include(namespace="my_project.urls")], 45 | ) 46 | 47 | SaffierExtra(app=app, registry=registry) 48 | return app 49 | 50 | 51 | app = get_application() 52 | -------------------------------------------------------------------------------- /docs_src/migrations/accounts_models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from my_project.utils import get_db_connection 4 | 5 | import saffier 6 | 7 | _, registry = get_db_connection() 8 | 9 | 10 | class User(saffier.Model): 11 | """ 12 | Base model for a user 13 | """ 14 | 15 | first_name = saffier.CharField(max_length=150) 16 | last_name = saffier.CharField(max_length=150) 17 | username = saffier.CharField(max_length=150, unique=True) 18 | email = saffier.EmailField(max_length=120, unique=True) 19 | password = saffier.CharField(max_length=128) 20 | last_login = saffier.DateTimeField(null=True) 21 | is_active = saffier.BooleanField(default=True) 22 | is_staff = saffier.BooleanField(default=False) 23 | is_superuser = saffier.BooleanField(default=False) 24 | 25 | class Meta: 26 | registry = registry 27 | -------------------------------------------------------------------------------- /docs_src/migrations/fastapi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | from fastapi import FastAPI 7 | from my_project.utils import get_db_connection 8 | 9 | from saffier.cli import Migrate 10 | 11 | 12 | def build_path(): 13 | """ 14 | Builds the path of the project and project root. 15 | """ 16 | Path(__file__).resolve().parent.parent 17 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__)) 18 | 19 | if SITE_ROOT not in sys.path: 20 | sys.path.append(SITE_ROOT) 21 | sys.path.append(os.path.join(SITE_ROOT, "apps")) 22 | 23 | 24 | def get_application(): 25 | """ 26 | This is optional. The function is only used for organisation purposes. 27 | """ 28 | build_path() 29 | database, registry = get_db_connection() 30 | 31 | app = FastAPI(__name__) 32 | 33 | Migrate(app=app, registry=registry) 34 | return app 35 | 36 | 37 | app = get_application() 38 | -------------------------------------------------------------------------------- /docs_src/migrations/lru.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | 3 | from saffier import Database, Registry 4 | 5 | 6 | @lru_cache() 7 | def get_db_connection(): 8 | database = Database("postgresql+asyncpg://user:pass@localhost:5432/my_database") 9 | return database, Registry(database=database) 10 | -------------------------------------------------------------------------------- /docs_src/migrations/migrations.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Generated by 'esmerald-admin createproject' 4 | """ 5 | import os 6 | import sys 7 | from pathlib import Path 8 | 9 | from esmerald import Esmerald, Include 10 | from my_project.utils import get_db_connection 11 | 12 | from saffier.cli import Migrate 13 | 14 | 15 | def build_path(): 16 | """ 17 | Builds the path of the project and project root. 18 | """ 19 | Path(__file__).resolve().parent.parent 20 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__)) 21 | 22 | if SITE_ROOT not in sys.path: 23 | sys.path.append(SITE_ROOT) 24 | sys.path.append(os.path.join(SITE_ROOT, "apps")) 25 | 26 | 27 | def get_application(): 28 | """ 29 | This is optional. The function is only used for organisation purposes. 30 | """ 31 | build_path() 32 | database, registry = get_db_connection() 33 | 34 | app = Esmerald( 35 | routes=[Include(namespace="my_project.urls")], 36 | ) 37 | 38 | Migrate(app=app, registry=registry) 39 | return app 40 | 41 | 42 | app = get_application() 43 | -------------------------------------------------------------------------------- /docs_src/migrations/model.py: -------------------------------------------------------------------------------- 1 | from my_project.utils import get_db_connection 2 | 3 | import saffier 4 | 5 | _, registry = get_db_connection() 6 | 7 | 8 | class User(saffier.Model): 9 | """ 10 | Base model for a user 11 | """ 12 | 13 | first_name = saffier.CharField(max_length=150) 14 | last_name = saffier.CharField(max_length=150) 15 | username = saffier.CharField(max_length=150, unique=True) 16 | email = saffier.EmailField(max_length=120, unique=True) 17 | password = saffier.CharField(max_length=128) 18 | last_login = saffier.DateTimeField(null=True) 19 | is_active = saffier.BooleanField(default=True) 20 | is_staff = saffier.BooleanField(default=False) 21 | is_superuser = saffier.BooleanField(default=False) 22 | 23 | class Meta: 24 | registry = registry 25 | -------------------------------------------------------------------------------- /docs_src/migrations/starlette.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | from lilya.apps import Lilya 7 | from my_project.utils import get_db_connection 8 | 9 | from saffier.cli import Migrate 10 | 11 | 12 | def build_path(): 13 | """ 14 | Builds the path of the project and project root. 15 | """ 16 | Path(__file__).resolve().parent.parent 17 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__)) 18 | 19 | if SITE_ROOT not in sys.path: 20 | sys.path.append(SITE_ROOT) 21 | sys.path.append(os.path.join(SITE_ROOT, "apps")) 22 | 23 | 24 | def get_application(): 25 | """ 26 | This is optional. The function is only used for organisation purposes. 27 | """ 28 | build_path() 29 | database, registry = get_db_connection() 30 | 31 | app = Lilya(__name__) 32 | 33 | Migrate(app=app, registry=registry) 34 | return app 35 | 36 | 37 | app = get_application() 38 | -------------------------------------------------------------------------------- /docs_src/migrations/via_dict.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | from esmerald import Esmerald, Include 7 | from my_project.utils import get_db_connection 8 | 9 | from saffier import Migrate 10 | 11 | 12 | def build_path(): 13 | """ 14 | Builds the path of the project and project root. 15 | """ 16 | Path(__file__).resolve().parent.parent 17 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__)) 18 | 19 | if SITE_ROOT not in sys.path: 20 | sys.path.append(SITE_ROOT) 21 | sys.path.append(os.path.join(SITE_ROOT, "apps")) 22 | 23 | 24 | def get_application(): 25 | """ 26 | This is optional. The function is only used for organisation purposes. 27 | """ 28 | build_path() 29 | database, registry = get_db_connection() 30 | 31 | app = Esmerald( 32 | routes=[Include(namespace="my_project.urls")], 33 | ) 34 | 35 | Migrate( 36 | app=app, 37 | registry=registry, 38 | model_apps={"accounts": "accounts.models"}, 39 | ) 40 | return app 41 | 42 | 43 | app = get_application() 44 | -------------------------------------------------------------------------------- /docs_src/migrations/via_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | from my_project.utils import get_db_connection 7 | 8 | from esmerald import Esmerald, Include 9 | from saffier import Migrate 10 | 11 | 12 | def build_path(): 13 | """ 14 | Builds the path of the project and project root. 15 | """ 16 | Path(__file__).resolve().parent.parent 17 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__)) 18 | 19 | if SITE_ROOT not in sys.path: 20 | sys.path.append(SITE_ROOT) 21 | sys.path.append(os.path.join(SITE_ROOT, "apps")) 22 | 23 | 24 | def get_application(): 25 | """ 26 | This is optional. The function is only used for organisation purposes. 27 | """ 28 | build_path() 29 | database, registry = get_db_connection() 30 | 31 | app = Esmerald( 32 | routes=[Include(namespace="my_project.urls")], 33 | ) 34 | 35 | Migrate( 36 | app=app, 37 | registry=registry, 38 | model_apps=["accounts.models"], 39 | ) 40 | return app 41 | 42 | 43 | app = get_application() 44 | -------------------------------------------------------------------------------- /docs_src/migrations/via_tuple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | from my_project.utils import get_db_connection 7 | 8 | from esmerald import Esmerald, Include 9 | from saffier import Migrate 10 | 11 | 12 | def build_path(): 13 | """ 14 | Builds the path of the project and project root. 15 | """ 16 | Path(__file__).resolve().parent.parent 17 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__)) 18 | 19 | if SITE_ROOT not in sys.path: 20 | sys.path.append(SITE_ROOT) 21 | sys.path.append(os.path.join(SITE_ROOT, "apps")) 22 | 23 | 24 | def get_application(): 25 | """ 26 | This is optional. The function is only used for organisation purposes. 27 | """ 28 | build_path() 29 | database, registry = get_db_connection() 30 | 31 | app = Esmerald( 32 | routes=[Include(namespace="my_project.urls")], 33 | ) 34 | 35 | Migrate( 36 | app=app, 37 | registry=registry, 38 | model_apps=("accounts.models",), 39 | ) 40 | return app 41 | 42 | 43 | app = get_application() 44 | -------------------------------------------------------------------------------- /docs_src/models/abstract/common.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import saffier 4 | from saffier import Database, Registry 5 | 6 | database = Database("sqlite:///db.sqlite") 7 | models = Registry(database=database) 8 | 9 | 10 | class BaseModel(saffier.Model): 11 | id = saffier.UUIDField(primary_key=True, default=uuid.uuid4) 12 | name = saffier.CharField(max_length=255) 13 | 14 | class Meta: 15 | abstract = True 16 | registry = models 17 | 18 | def get_description(self): 19 | """ 20 | Returns the description of a record 21 | """ 22 | return getattr(self, "description", None) 23 | 24 | 25 | class User(BaseModel): 26 | """ 27 | Inheriting the fields from the abstract class 28 | as well as the Meta data. 29 | """ 30 | 31 | phone_number = saffier.CharField(max_length=15) 32 | description = saffier.TextField() 33 | 34 | def transform_phone_number(self): 35 | # logic here for the phone number 36 | ... 37 | 38 | 39 | class Product(BaseModel): 40 | """ 41 | Inheriting the fields from the abstract class 42 | as well as the Meta data. 43 | """ 44 | 45 | sku = saffier.CharField(max_length=255) 46 | description = saffier.TextField() 47 | 48 | def get_sku(self): 49 | # Logic to obtain the SKU 50 | ... 51 | -------------------------------------------------------------------------------- /docs_src/models/abstract/simple.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class BaseModel(saffier.Model): 9 | class Meta: 10 | abstract = True 11 | registry = models 12 | -------------------------------------------------------------------------------- /docs_src/models/declarative/example.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | # Declare the Saffier model 9 | 10 | 11 | class User(saffier.Model): 12 | is_active = saffier.BooleanField(default=True) 13 | first_name = saffier.CharField(max_length=50) 14 | last_name = saffier.CharField(max_length=50) 15 | email = saffier.EmailField(max_lengh=100) 16 | password = saffier.CharField(max_length=1000) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | # Generate the declarative version 23 | UserModelDeclarative = User.declarative() 24 | -------------------------------------------------------------------------------- /docs_src/models/declarative/fk_relationship.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | is_active = saffier.BooleanField(default=True) 10 | first_name = saffier.CharField(max_length=50) 11 | last_name = saffier.CharField(max_length=50) 12 | email = saffier.EmailField(max_lengh=100) 13 | password = saffier.CharField(max_length=1000) 14 | 15 | class Meta: 16 | registry = models 17 | 18 | 19 | class Thread(saffier.Model): 20 | sender = saffier.ForeignKey( 21 | User, 22 | on_delete=saffier.CASCADE, 23 | related_name="sender", 24 | ) 25 | receiver = saffier.ForeignKey( 26 | User, 27 | on_delete=saffier.CASCADE, 28 | related_name="receiver", 29 | ) 30 | message = saffier.TextField() 31 | 32 | class Meta: 33 | registry = models 34 | -------------------------------------------------------------------------------- /docs_src/models/declaring_models.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | """ 10 | The user model representation 11 | """ 12 | 13 | id = saffier.IntegerField(primary_key=True) 14 | name = saffier.CharField(max_length=255) 15 | age = saffier.IntegerField(minimum=18) 16 | is_active = saffier.BooleanField(default=True) 17 | 18 | class Meta: 19 | registry = models 20 | -------------------------------------------------------------------------------- /docs_src/models/declaring_models_no_id.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | """ 10 | The user model representation. 11 | 12 | The `id` is not provided and Saffier will automatically 13 | generate a primary_key `id` BigIntegerField. 14 | """ 15 | 16 | name = saffier.CharField(max_length=255) 17 | age = saffier.IntegerField(minimum=18) 18 | is_active = saffier.BooleanField(default=True) 19 | 20 | class Meta: 21 | registry = models 22 | -------------------------------------------------------------------------------- /docs_src/models/declaring_models_pk_no_id.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import saffier 4 | from saffier import Database, Registry 5 | 6 | database = Database("sqlite:///db.sqlite") 7 | models = Registry(database=database) 8 | 9 | 10 | class User(saffier.Model): 11 | name = saffier.CharField(max_length=255, primary_key=True, default=str(uuid.uuid4)) 12 | age = saffier.IntegerField(minimum=18) 13 | is_active = saffier.BooleanField(default=True) 14 | 15 | class Meta: 16 | registry = models 17 | -------------------------------------------------------------------------------- /docs_src/models/default_model.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | age = saffier.IntegerField(minimum=18) 10 | is_active = saffier.BooleanField(default=True) 11 | 12 | class Meta: 13 | registry = models 14 | -------------------------------------------------------------------------------- /docs_src/models/indexes/complex_together.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Index, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.EmailField(max_length=70) 11 | is_active = saffier.BooleanField(default=True) 12 | status = saffier.CharField(max_length=255) 13 | 14 | class Meta: 15 | registry = models 16 | indexes = [ 17 | Index(fields=["name", "email"]), 18 | Index(fields=["is_active", "status"], name="active_status_idx"), 19 | ] 20 | -------------------------------------------------------------------------------- /docs_src/models/indexes/simple.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.EmailField(max_length=70, index=True) 11 | is_active = saffier.BooleanField(default=True) 12 | 13 | class Meta: 14 | registry = models 15 | -------------------------------------------------------------------------------- /docs_src/models/indexes/simple2.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Index, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.EmailField(max_length=70) 11 | is_active = saffier.BooleanField(default=True) 12 | 13 | class Meta: 14 | registry = models 15 | indexes = [Index(fields=["email"])] 16 | -------------------------------------------------------------------------------- /docs_src/models/managers/override.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Manager, QuerySet, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class InactiveManager(Manager): 9 | """ 10 | Custom manager that will return only active users 11 | """ 12 | 13 | def get_queryset(self) -> "QuerySet": 14 | queryset = super().get_queryset().filter(is_active=False) 15 | return queryset 16 | 17 | 18 | class User(saffier.Model): 19 | name = saffier.CharField(max_length=255) 20 | email = saffier.EmailField(max_length=70) 21 | is_active = saffier.BooleanField(default=True) 22 | 23 | # Add the new manager 24 | query = InactiveManager() 25 | 26 | class Meta: 27 | registry = models 28 | unique_together = ["name", "email"] 29 | 30 | 31 | # Using ipython that supports await 32 | # Don't use this in production! Use Alembic or any tool to manage 33 | # The migrations for you 34 | await models.create_all() # noqa 35 | 36 | # Create an inactive user 37 | await User.query.create(name="Saffier", email="foo@bar.com", is_active=False) # noqa 38 | 39 | # You can also create a user using the new manager 40 | await User.query.create(name="Another Saffier", email="bar@foo.com", is_active=False) # noqa 41 | 42 | # Create a user using the default manager 43 | await User.query.create(name="Saffier", email="user@saffier.com") # noqa 44 | 45 | # Querying them all 46 | user = await User.query.all() # noqa 47 | # [User(id=1), User(id=2)] 48 | -------------------------------------------------------------------------------- /docs_src/models/managers/simple.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.EmailField(max_length=70) 11 | is_active = saffier.BooleanField(default=True) 12 | 13 | class Meta: 14 | registry = models 15 | unique_together = ["name", "email"] 16 | 17 | 18 | # Using ipython that supports await 19 | # Don't use this in production! Use Alembic or any tool to manage 20 | # The migrations for you 21 | await models.create_all() # noqa 22 | 23 | await User.query.create(name="Saffier", email="foo@bar.com") # noqa 24 | 25 | user = await User.query.get(id=1) # noqa 26 | # User(id=1) 27 | -------------------------------------------------------------------------------- /docs_src/models/pk_no_default.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | id = saffier.CharField(max_length=255, primary_key=True) 10 | age = saffier.IntegerField(minimum=18) 11 | is_active = saffier.BooleanField(default=True) 12 | 13 | class Meta: 14 | registry = models 15 | -------------------------------------------------------------------------------- /docs_src/models/pk_with_default.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import saffier 4 | from saffier import Database, Registry 5 | 6 | database = Database("sqlite:///db.sqlite") 7 | models = Registry(database=database) 8 | 9 | 10 | class User(saffier.Model): 11 | id = saffier.UUIDField(primary_key=True, default=uuid.uuid4) 12 | age = saffier.IntegerField(minimum=18) 13 | is_active = saffier.BooleanField(default=True) 14 | 15 | class Meta: 16 | registry = models 17 | -------------------------------------------------------------------------------- /docs_src/models/registry/inheritance_abstract.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class BaseModel(saffier.Model): 9 | """ 10 | The base model for all models using the `models` registry. 11 | """ 12 | 13 | class Meta: 14 | abstract = True 15 | registry = models 16 | 17 | 18 | class User(BaseModel): 19 | name = saffier.CharField(max_length=255) 20 | is_active = saffier.BooleanField(default=True) 21 | 22 | 23 | class Product(BaseModel): 24 | user = saffier.ForeignKey(User, null=False, on_delete=saffier.CASCADE) 25 | sku = saffier.CharField(max_length=255, null=False) 26 | -------------------------------------------------------------------------------- /docs_src/models/registry/inheritance_no_repeat.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class BaseModel(saffier.Model): 9 | """ 10 | The base model for all models using the `models` registry. 11 | """ 12 | 13 | class Meta: 14 | registry = models 15 | 16 | 17 | class User(BaseModel): 18 | name = saffier.CharField(max_length=255) 19 | is_active = saffier.BooleanField(default=True) 20 | 21 | 22 | class Product(BaseModel): 23 | user = saffier.ForeignKey(User, null=False, on_delete=saffier.CASCADE) 24 | sku = saffier.CharField(max_length=255, null=False) 25 | -------------------------------------------------------------------------------- /docs_src/models/registry/nutshell.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | is_active = saffier.BooleanField(default=True) 11 | 12 | class Meta: 13 | registry = models 14 | -------------------------------------------------------------------------------- /docs_src/models/tablename/model_diff_tn.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | is_active = saffier.BooleanField(default=True) 11 | 12 | class Meta: 13 | tablename = "db_users" 14 | registry = models 15 | -------------------------------------------------------------------------------- /docs_src/models/tablename/model_no_tablename.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | """ 10 | If the `tablename` is not declared in the `Meta`, 11 | saffier will pluralise the class name. 12 | 13 | This table will be called in the database `users`. 14 | """ 15 | 16 | name = saffier.CharField(max_length=255) 17 | is_active = saffier.BooleanField(default=True) 18 | 19 | class Meta: 20 | registry = models 21 | -------------------------------------------------------------------------------- /docs_src/models/tablename/model_with_tablename.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | is_active = saffier.BooleanField(default=True) 11 | 12 | class Meta: 13 | tablename = "users" 14 | registry = models 15 | -------------------------------------------------------------------------------- /docs_src/models/unique_together/complex_combined.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.EmailField(max_length=70) 11 | phone_number = saffier.CharField(max_length=15) 12 | address = saffier.CharField(max_length=500) 13 | is_active = saffier.BooleanField(default=True) 14 | 15 | class Meta: 16 | registry = models 17 | unique_together = [ 18 | ("name", "email"), 19 | ("name", "email", "phone_number"), 20 | ("email", "address"), 21 | ] 22 | -------------------------------------------------------------------------------- /docs_src/models/unique_together/complex_independent.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.EmailField(max_length=70) 11 | is_active = saffier.BooleanField(default=True) 12 | 13 | class Meta: 14 | registry = models 15 | unique_together = ["name", "email"] 16 | -------------------------------------------------------------------------------- /docs_src/models/unique_together/complex_mixed.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.EmailField(max_length=70) 11 | phone_number = saffier.CharField(max_length=15) 12 | address = saffier.CharField(max_length=500) 13 | is_active = saffier.BooleanField(default=True) 14 | 15 | class Meta: 16 | registry = models 17 | unique_together = [ 18 | ("name", "email"), 19 | ("name", "email", "phone_number"), 20 | ("email", "address"), 21 | "is_active", 22 | ] 23 | -------------------------------------------------------------------------------- /docs_src/models/unique_together/complex_together.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.EmailField(max_length=70) 11 | is_active = saffier.BooleanField(default=True) 12 | 13 | class Meta: 14 | registry = models 15 | unique_together = [("name", "email")] 16 | -------------------------------------------------------------------------------- /docs_src/models/unique_together/constraints/complex.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry, UniqueConstraint 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.EmailField(max_length=70) 11 | phone_number = saffier.CharField(max_length=15) 12 | address = saffier.CharField(max_length=500) 13 | is_active = saffier.BooleanField(default=True) 14 | 15 | class Meta: 16 | registry = models 17 | unique_together = [ 18 | UniqueConstraint(fields=["name", "email"]), 19 | UniqueConstraint(fields=["name", "email", "phone_number"]), 20 | UniqueConstraint(fields=["email", "address"]), 21 | ] 22 | -------------------------------------------------------------------------------- /docs_src/models/unique_together/constraints/mixing.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry, UniqueConstraint 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.EmailField(max_length=70) 11 | phone_number = saffier.CharField(max_length=15) 12 | address = saffier.CharField(max_length=500) 13 | is_active = saffier.BooleanField(default=True) 14 | 15 | class Meta: 16 | registry = models 17 | unique_together = [ 18 | UniqueConstraint(fields=["name", "email"]), 19 | ("name", "email", "phone_number"), 20 | ("email", "address"), 21 | ] 22 | -------------------------------------------------------------------------------- /docs_src/models/unique_together/simple.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.EmailField(max_length=70, unique=True) 11 | is_active = saffier.BooleanField(default=True) 12 | 13 | class Meta: 14 | registry = models 15 | -------------------------------------------------------------------------------- /docs_src/models/unique_together/simple2.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.EmailField(max_length=70) 11 | is_active = saffier.BooleanField(default=True) 12 | 13 | class Meta: 14 | registry = models 15 | unique_together = ["name"] # or ("name",) 16 | -------------------------------------------------------------------------------- /docs_src/prefetch/first/asserting.py: -------------------------------------------------------------------------------- 1 | assert len(users) == 2 # Total ussers 2 | 3 | saffier = [value for value in users if value.pk == saffier.pk][0] 4 | assert len(saffier.to_posts) == 5 # Total posts for Saffier 5 | assert len(saffier.to_articles) == 50 # Total articles for Saffier 6 | 7 | esmerald = [value for value in users if value.pk == esmerald.pk][0] 8 | assert len(esmerald.to_posts) == 15 # Total posts for Esmerald 9 | assert len(esmerald.to_articles) == 20 # Total articles for Esmerald 10 | -------------------------------------------------------------------------------- /docs_src/prefetch/first/data.py: -------------------------------------------------------------------------------- 1 | user = await User.query.create(name="Saffier") 2 | 3 | for i in range(5): 4 | await Post.query.create(comment="Comment number %s" % i, user=user) 5 | 6 | for i in range(50): 7 | await Article.query.create(content="Comment number %s" % i, user=user) 8 | 9 | esmerald = await User.query.create(name="Esmerald") 10 | 11 | for i in range(15): 12 | await Post.query.create(comment="Comment number %s" % i, user=esmerald) 13 | 14 | for i in range(20): 15 | await Article.query.create(content="Comment number %s" % i, user=esmerald) 16 | -------------------------------------------------------------------------------- /docs_src/prefetch/first/models.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | database = saffier.Database("sqlite:///db.sqlite") 4 | models = saffier.Registry(database=database) 5 | 6 | 7 | class User(saffier.Model): 8 | name = saffier.CharField(max_length=100) 9 | 10 | class Meta: 11 | registry = models 12 | 13 | 14 | class Post(saffier.Model): 15 | user = saffier.ForeignKey(User, related_name="posts") 16 | comment = saffier.CharField(max_length=255) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | class Article(saffier.Model): 23 | user = saffier.ForeignKey(User, related_name="articles") 24 | content = saffier.CharField(max_length=255) 25 | 26 | class Meta: 27 | registry = models 28 | -------------------------------------------------------------------------------- /docs_src/prefetch/first/prefetch.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Prefetch 3 | 4 | database = saffier.Database("sqlite:///db.sqlite") 5 | models = saffier.Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=100) 10 | 11 | class Meta: 12 | registry = models 13 | 14 | 15 | class Post(saffier.Model): 16 | user = saffier.ForeignKey(User, related_name="posts") 17 | comment = saffier.CharField(max_length=255) 18 | 19 | class Meta: 20 | registry = models 21 | 22 | 23 | class Article(saffier.Model): 24 | user = saffier.ForeignKey(User, related_name="articles") 25 | content = saffier.CharField(max_length=255) 26 | 27 | class Meta: 28 | registry = models 29 | 30 | 31 | # All the users with all the posts and articles 32 | # of each user 33 | users = await User.query.prefetch_related( 34 | Prefetch(related_name="posts", to_attr="to_posts"), 35 | Prefetch(related_name="articles", to_attr="to_articles"), 36 | ).all() 37 | -------------------------------------------------------------------------------- /docs_src/prefetch/second/data.py: -------------------------------------------------------------------------------- 1 | # Create the album 2 | album = await Album.query.create(name="Malibu") 3 | 4 | # Create the track 5 | await Track.query.create(album=album, title="The Bird", position=1) 6 | 7 | # Create the studio 8 | studio = await Studio.query.create(album=album, name="Valentim") 9 | 10 | # Create the company 11 | await Company.query.create(studio=studio) 12 | -------------------------------------------------------------------------------- /docs_src/prefetch/second/models.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | database = saffier.Database("sqlite:///db.sqlite") 4 | models = saffier.Registry(database=database) 5 | 6 | 7 | class Album(saffier.Model): 8 | id = saffier.IntegerField(primary_key=True) 9 | name = saffier.CharField(max_length=100) 10 | 11 | class Meta: 12 | registry = models 13 | 14 | 15 | class Track(saffier.Model): 16 | id = saffier.IntegerField(primary_key=True) 17 | album = saffier.ForeignKey("Album", on_delete=saffier.CASCADE, related_name="tracks") 18 | title = saffier.CharField(max_length=100) 19 | position = saffier.IntegerField() 20 | 21 | class Meta: 22 | registry = models 23 | 24 | 25 | class Studio(saffier.Model): 26 | album = saffier.ForeignKey("Album", related_name="studios") 27 | name = saffier.CharField(max_length=255) 28 | 29 | class Meta: 30 | registry = models 31 | 32 | 33 | class Company(saffier.Model): 34 | studio = saffier.ForeignKey(Studio, related_name="companies") 35 | 36 | class Meta: 37 | registry = models 38 | -------------------------------------------------------------------------------- /docs_src/prefetch/second/prefetch.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Prefetch 3 | 4 | database = saffier.Database("sqlite:///db.sqlite") 5 | models = saffier.Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=100) 10 | 11 | class Meta: 12 | registry = models 13 | 14 | 15 | class Post(saffier.Model): 16 | user = saffier.ForeignKey(User, related_name="posts") 17 | comment = saffier.CharField(max_length=255) 18 | 19 | class Meta: 20 | registry = models 21 | 22 | 23 | class Article(saffier.Model): 24 | user = saffier.ForeignKey(User, related_name="articles") 25 | content = saffier.CharField(max_length=255) 26 | 27 | class Meta: 28 | registry = models 29 | 30 | 31 | # All the tracks that belong to a specific `Company`. 32 | # The tracks are associated with `albums` and `studios` 33 | company = await Company.query.prefetch_related( 34 | Prefetch(related_name="companies__studios__tracks", to_attr="tracks") 35 | ).get(studio=studio) 36 | -------------------------------------------------------------------------------- /docs_src/prefetch/second/prefetch_filtered.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Prefetch 3 | 4 | database = saffier.Database("sqlite:///db.sqlite") 5 | models = saffier.Registry(database=database) 6 | 7 | # All the tracks that belong to a specific `Company`. 8 | # The tracks are associated with `albums` and `studios` 9 | # where the `Track` will be also internally filtered 10 | company = await Company.query.prefetch_related( 11 | Prefetch( 12 | related_name="companies__studios__tracks", 13 | to_attr="tracks", 14 | queryset=Track.query.filter(title__icontains="bird"), 15 | ) 16 | ) 17 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/and.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | # Create some records 4 | 5 | await User.query.create(name="Adam", email="adam@saffier.dev") 6 | await User.query.create(name="Eve", email="eve@saffier.dev") 7 | 8 | # Query using the and_ 9 | await User.query.filter( 10 | saffier.and_(User.columns.name == "Adam", User.columns.email == "adam@saffier.dev"), 11 | ) 12 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/and_m_filter.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | # Create some records 4 | 5 | await User.query.create(name="Adam", email="adam@saffier.dev") 6 | await User.query.create(name="Eve", email="eve@saffier.dev") 7 | 8 | # Query using the and_ 9 | await User.query.filter(saffier.and_(User.columns.name == "Adam")).filter( 10 | saffier.and_(User.columns.email == "adam@saffier.dev") 11 | ) 12 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/and_two.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | # Create some records 4 | 5 | await User.query.create(name="Adam", email="adam@saffier.dev") 6 | await User.query.create(name="Eve", email="eve@saffier.dev") 7 | 8 | # Query using the and_ 9 | await User.query.filter( 10 | saffier.and_( 11 | User.columns.email.contains("saffier"), 12 | ) 13 | ) 14 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/model.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | first_name: str = saffier.CharField(max_length=50, null=True) 10 | email: str = saffier.EmailField(max_lengh=100, null=True) 11 | 12 | class Meta: 13 | registry = models 14 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/not.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | # Create some records 4 | 5 | await User.query.create(name="Adam", email="adam@saffier.dev") 6 | await User.query.create(name="Eve", email="eve@saffier.dev") 7 | 8 | # Query using the not_ 9 | await User.query.filter(saffier.not_(User.columns.name == "Adam")) 10 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/not_m_filter.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | # Create some records 4 | 5 | await User.query.create(name="Adam", email="adam@saffier.dev") 6 | await User.query.create(name="Eve", email="eve@saffier.dev") 7 | await User.query.create(name="John", email="john@example.com") 8 | 9 | # Query using the not_ 10 | await User.query.filter(saffier.not_(User.columns.name == "Adam")).filter( 11 | saffier.not_(User.columns.email.contains("saffier")) 12 | ) 13 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/not_two.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | # Create some records 4 | 5 | await User.query.create(name="Adam", email="adam@saffier.dev") 6 | await User.query.create(name="Eve", email="eve@saffier.dev") 7 | 8 | # Query using the not_ 9 | await User.query.filter( 10 | saffier.not_( 11 | User.columns.email.contains("saffier"), 12 | ) 13 | ) 14 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/or.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | # Create some records 4 | 5 | await User.query.create(name="Adam", email="adam@saffier.dev") 6 | await User.query.create(name="Eve", email="eve@saffier.dev") 7 | 8 | # Query using the or_ 9 | await User.query.filter( 10 | saffier.or_(User.columns.name == "Adam", User.columns.email == "adam@saffier.dev"), 11 | ) 12 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/or_m_filter.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | # Create some records 4 | 5 | await User.query.create(name="Adam", email="adam@saffier.dev") 6 | await User.query.create(name="Eve", email="eve@saffier.dev") 7 | 8 | # Query using the or_ 9 | await User.query.filter(saffier.or_(User.columns.name == "Adam")).filter( 10 | saffier.or_(User.columns.email == "adam@saffier.dev") 11 | ) 12 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/or_two.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | # Create some records 4 | 5 | await User.query.create(name="Adam", email="adam@saffier.dev") 6 | await User.query.create(name="Eve", email="eve@saffier.dev") 7 | 8 | # Query using the or_ 9 | await User.query.filter( 10 | saffier.or_( 11 | User.columns.email.contains("saffier"), 12 | ) 13 | ) 14 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/style/and.py: -------------------------------------------------------------------------------- 1 | # Create some records 2 | 3 | await User.query.create(name="Adam", email="adam@saffier.dev") 4 | await User.query.create(name="Eve", email="eve@saffier.dev") 5 | 6 | # Query using the and_ 7 | await User.query.and_(name="Adam", email="adam@saffier.dev") 8 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/style/and_m_filter.py: -------------------------------------------------------------------------------- 1 | # Create some records 2 | 3 | await User.query.create(name="Adam", email="adam@saffier.dev") 4 | await User.query.create(name="Eve", email="eve@saffier.dev") 5 | 6 | # Query using the and_ 7 | await User.query.filter(name="Adam").and_(email="adam@saffier.dev") 8 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/style/and_two.py: -------------------------------------------------------------------------------- 1 | # Create some records 2 | 3 | await User.query.create(name="Adam", email="adam@saffier.dev") 4 | await User.query.create(name="Eve", email="eve@saffier.dev") 5 | 6 | # Query using the and_ 7 | await User.query.and_(email__icontains="saffier") 8 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/style/model.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | first_name: str = saffier.CharField(max_length=50, null=True) 10 | email: str = saffier.EmailField(max_lengh=100, null=True) 11 | 12 | class Meta: 13 | registry = models 14 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/style/not.py: -------------------------------------------------------------------------------- 1 | # Create some records 2 | 3 | await User.query.create(name="Adam", email="adam@saffier.dev") 4 | await User.query.create(name="Eve", email="eve@saffier.dev") 5 | 6 | # Query using the not_ 7 | await User.query.not_(name="Adam") 8 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/style/not_m_filter.py: -------------------------------------------------------------------------------- 1 | # Create some records 2 | 3 | await User.query.create(name="Adam", email="adam@saffier.dev") 4 | await User.query.create(name="Eve", email="eve@saffier.dev") 5 | await User.query.create(name="John", email="john@example.com") 6 | 7 | # Query using the not_ 8 | await User.query.filter(email__icontains="saffier").not_(name__iexact="Adam") 9 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/style/not_two.py: -------------------------------------------------------------------------------- 1 | # Create some records 2 | 3 | await User.query.create(name="Adam", email="adam@saffier.dev") 4 | await User.query.create(name="Eve", email="eve@saffier.dev") 5 | 6 | # Query using the not_ 7 | await User.query.not_(email__icontains="saffier").not_(name__icontains="a") 8 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/style/or.py: -------------------------------------------------------------------------------- 1 | # Create some records 2 | 3 | await User.query.create(name="Adam", email="adam@saffier.dev") 4 | await User.query.create(name="Eve", email="eve@saffier.dev") 5 | 6 | # Query using the or_ 7 | await User.query.or_(name="Adam", email="adam@saffier.dev") 8 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/style/or_m_filter.py: -------------------------------------------------------------------------------- 1 | # Create some records 2 | 3 | await User.query.create(name="Adam", email="adam@saffier.dev") 4 | await User.query.create(name="Eve", email="eve@saffier.dev") 5 | 6 | # Query using the or_ 7 | await User.query.or_(name="Adam").filter(email="adam@saffier.dev") 8 | -------------------------------------------------------------------------------- /docs_src/queries/clauses/style/or_two.py: -------------------------------------------------------------------------------- 1 | # Create some records 2 | 3 | await User.query.create(name="Adam", email="adam@saffier.dev") 4 | await User.query.create(name="Eve", email="eve@saffier.dev") 5 | 6 | # Query using the multiple or_ 7 | await User.query.or_(email__icontains="saffier").or_(name__icontains="a") 8 | 9 | # Query using the or_ with multiple fields 10 | await User.query.or_(email__icontains="saffier", name__icontains="a") 11 | -------------------------------------------------------------------------------- /docs_src/queries/manytomany/example.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class Team(saffier.Model): 9 | name = saffier.CharField(max_length=100) 10 | 11 | class Meta: 12 | registry = models 13 | 14 | 15 | class Organisation(saffier.Model): 16 | ident = saffier.CharField(max_length=100) 17 | teams = saffier.ManyToManyField(Team, related_name="organisation_teams") 18 | 19 | class Meta: 20 | registry = models 21 | -------------------------------------------------------------------------------- /docs_src/queries/manytomany/no_rel.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class Team(saffier.Model): 9 | name = saffier.CharField(max_length=100) 10 | 11 | class Meta: 12 | registry = models 13 | 14 | 15 | class Organisation(saffier.Model): 16 | ident = saffier.CharField(max_length=100) 17 | teams = saffier.ManyToManyField(Team) 18 | 19 | class Meta: 20 | registry = models 21 | -------------------------------------------------------------------------------- /docs_src/queries/manytomany/no_rel_query_example.py: -------------------------------------------------------------------------------- 1 | # Create some fake data 2 | blue_team = await Team.query.create(name="Blue Team") 3 | green_team = await Team.query.create(name="Green Team") 4 | 5 | # Add the teams to the organisation 6 | organisation = await Organisation.query.create(ident="Acme Ltd") 7 | await organisation.teams.add(blue_team) 8 | await organisation.teams.add(green_team) 9 | 10 | # Query 11 | await blue_team.team_organisationteams_set.filter(name=blue_team.name) 12 | -------------------------------------------------------------------------------- /docs_src/queries/manytomany/query_example.py: -------------------------------------------------------------------------------- 1 | # Create some fake data 2 | blue_team = await Team.query.create(name="Blue Team") 3 | green_team = await Team.query.create(name="Green Team") 4 | 5 | # Add the teams to the organisation 6 | organisation = await Organisation.query.create(ident="Acme Ltd") 7 | organisation.teams.add(blue_team) 8 | organisation.teams.add(green_team) 9 | 10 | # Query 11 | blue_team.organisation_teams.filter(name=blue_team.name) 12 | -------------------------------------------------------------------------------- /docs_src/queries/model.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | is_active = saffier.BooleanField(default=True) 10 | first_name = saffier.CharField(max_length=50) 11 | last_name = saffier.CharField(max_length=50) 12 | email = saffier.EmailField(max_lengh=100) 13 | password = saffier.CharField(max_length=1000) 14 | 15 | class Meta: 16 | registry = models 17 | 18 | 19 | class User(saffier.Model): 20 | user = saffier.ForeignKey(User, on_delete=saffier.CASCADE) 21 | 22 | class Meta: 23 | registry = models 24 | -------------------------------------------------------------------------------- /docs_src/queries/related_name/data.py: -------------------------------------------------------------------------------- 1 | # This assumes you have the models imported 2 | # or accessible from somewhere allowing you to generate 3 | # these records in your database 4 | 5 | 6 | acme = await Organisation.query.create(ident="ACME Ltd") 7 | red_team = await Team.query.create(org=acme, name="Red Team") 8 | blue_team = await Team.query.create(org=acme, name="Blue Team") 9 | 10 | await Member.query.create(team=red_team, email="charlie@redteam.com") 11 | await Member.query.create(team=red_team, email="brown@redteam.com") 12 | await Member.query.create(team=blue_team, email="monica@blueteam.com") 13 | await Member.query.create(team=blue_team, email="snoopy@blueteam.com") 14 | -------------------------------------------------------------------------------- /docs_src/queries/related_name/example.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class Organisation(saffier.Model): 9 | ident = saffier.CharField(max_length=100) 10 | 11 | class Meta: 12 | registry = models 13 | 14 | 15 | class Team(saffier.Model): 16 | org = saffier.ForeignKey(Organisation, on_delete=saffier.RESTRICT) 17 | name = saffier.CharField(max_length=100) 18 | 19 | class Meta: 20 | registry = models 21 | -------------------------------------------------------------------------------- /docs_src/queries/related_name/models.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class Organisation(saffier.Model): 9 | ident = saffier.CharField(max_length=100) 10 | 11 | class Meta: 12 | registry = models 13 | 14 | 15 | class Team(saffier.Model): 16 | org = saffier.ForeignKey(Organisation, on_delete=saffier.RESTRICT) 17 | name = saffier.CharField(max_length=100) 18 | 19 | class Meta: 20 | registry = models 21 | 22 | 23 | class Member(saffier.Model): 24 | team = saffier.ForeignKey(Team, on_delete=saffier.SET_NULL, null=True, related_name="members") 25 | second_team = saffier.ForeignKey( 26 | Team, on_delete=saffier.SET_NULL, null=True, related_name="team_members" 27 | ) 28 | email = saffier.CharField(max_length=100) 29 | name = saffier.CharField(max_length=255, null=True) 30 | 31 | class Meta: 32 | registry = models 33 | -------------------------------------------------------------------------------- /docs_src/queries/related_name/new_data.py: -------------------------------------------------------------------------------- 1 | # This assumes you have the models imported 2 | # or accessible from somewhere allowing you to generate 3 | # these records in your database 4 | 5 | acme = await Organisation.query.create(ident="ACME Ltd") 6 | red_team = await Team.query.create(org=acme, name="Red Team") 7 | blue_team = await Team.query.create(org=acme, name="Blue Team") 8 | green_team = await Team.query.create(org=acme, name="Green Team") 9 | 10 | await Member.query.create(team=red_team, email="charlie@redteam.com") 11 | await Member.query.create(team=red_team, email="brown@redteam.com") 12 | await Member.query.create(team=blue_team, email="snoopy@blueteam.com") 13 | monica = await Member.query.create(team=green_team, email="monica@blueteam.com") 14 | 15 | # New data 16 | user = await User.query.create(member=monica, name="Saffier") 17 | profile = await Profile.query.create(user=user, profile_type="admin") 18 | -------------------------------------------------------------------------------- /docs_src/queries/related_name/new_models.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class Organisation(saffier.Model): 9 | ident = saffier.CharField(max_length=100) 10 | 11 | class Meta: 12 | registry = models 13 | 14 | 15 | class Team(saffier.Model): 16 | org = saffier.ForeignKey(Organisation, on_delete=saffier.RESTRICT) 17 | name = saffier.CharField(max_length=100) 18 | 19 | class Meta: 20 | registry = models 21 | 22 | 23 | class Member(saffier.Model): 24 | team = saffier.ForeignKey(Team, on_delete=saffier.SET_NULL, null=True, related_name="members") 25 | second_team = saffier.ForeignKey( 26 | Team, on_delete=saffier.SET_NULL, null=True, related_name="team_members" 27 | ) 28 | email = saffier.CharField(max_length=100) 29 | name = saffier.CharField(max_length=255, null=True) 30 | 31 | class Meta: 32 | registry = models 33 | 34 | 35 | class User(saffier.Model): 36 | id = saffier.IntegerField(primary_key=True) 37 | name = saffier.CharField(max_length=255, null=True) 38 | member = saffier.ForeignKey( 39 | Member, on_delete=saffier.SET_NULL, null=True, related_name="users" 40 | ) 41 | 42 | class Meta: 43 | registry = models 44 | 45 | 46 | class Profile(saffier.Model): 47 | user = saffier.ForeignKey(User, on_delete=saffier.CASCADE, null=False, related_name="profiles") 48 | profile_type = saffier.CharField(max_length=255, null=False) 49 | 50 | class Meta: 51 | registry = models 52 | -------------------------------------------------------------------------------- /docs_src/queries/secrets/model.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | name = saffier.CharField(max_length=50) 10 | email = saffier.EmailField(max_lengh=100) 11 | password = saffier.CharField(max_length=1000, secret=True) 12 | 13 | class Meta: 14 | registry = models 15 | -------------------------------------------------------------------------------- /docs_src/quickstart/example1.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | """ 10 | The User model to be created in the database as a table 11 | If no name is provided the in Meta class, it will generate 12 | a "users" table for you. 13 | """ 14 | 15 | id = saffier.IntegerField(primary_key=True) 16 | is_active = saffier.BooleanField(default=False) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | # Create the db and tables 23 | # Don't use this in production! Use Alembic or any tool to manage 24 | # The migrations for you 25 | await models.create_all() # noqa 26 | 27 | await User.query.create(is_active=False) # noqa 28 | 29 | user = await User.query.get(id=1) # noqa 30 | print(user) 31 | # User(id=1) 32 | -------------------------------------------------------------------------------- /docs_src/reflection/model.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | age = saffier.IntegerField(minimum=18) 10 | is_active = saffier.BooleanField(default=True) 11 | 12 | class Meta: 13 | registry = models 14 | -------------------------------------------------------------------------------- /docs_src/reflection/reflect.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.ReflectModel): 9 | age = saffier.IntegerField(minimum=18) 10 | is_active = saffier.BooleanField(default=True) 11 | 12 | class Meta: 13 | tablename = "users" 14 | registry = models 15 | -------------------------------------------------------------------------------- /docs_src/reflection/reflect/model.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | age = saffier.IntegerField(minimum=18, null=True) 10 | is_active = saffier.BooleanField(default=True, null=True) 11 | description = saffier.CharField(max_length=255, null=True) 12 | profile_type = saffier.CharField(max_length=255, null=True) 13 | username = saffier.CharField(max_length=255, null=True) 14 | 15 | class Meta: 16 | registry = models 17 | -------------------------------------------------------------------------------- /docs_src/reflection/reflect/reflect.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class Profile(saffier.ReflectModel): 9 | is_active = saffier.BooleanField(default=True, null=True) 10 | profile_type = saffier.CharField(max_length=255, null=True) 11 | username = saffier.CharField(max_length=255, null=True) 12 | 13 | class Meta: 14 | tablename = "users" 15 | registry = models 16 | -------------------------------------------------------------------------------- /docs_src/registry/create_schema.py: -------------------------------------------------------------------------------- 1 | from saffier import Database, Registry 2 | 3 | database = Database("") 4 | registry = Registry(database=database) 5 | 6 | 7 | async def create_schema(name: str) -> None: 8 | """ 9 | Creates a new schema in the database. 10 | """ 11 | await registry.schema.create_schema(name, if_not_exists=True) 12 | -------------------------------------------------------------------------------- /docs_src/registry/custom_registry.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | 5 | class MyRegistry(Registry): 6 | """ 7 | Add logic unique to your registry or override 8 | existing functionality. 9 | """ 10 | 11 | ... 12 | 13 | 14 | database = Database("sqlite:///db.sqlite") 15 | models = MyRegistry(database=database) 16 | 17 | 18 | class User(saffier.Model): 19 | """ 20 | The User model to be created in the database as a table 21 | If no name is provided the in Meta class, it will generate 22 | a "users" table for you. 23 | """ 24 | 25 | id = saffier.IntegerField(primary_key=True) 26 | is_active = saffier.BooleanField(default=False) 27 | 28 | class Meta: 29 | registry = models 30 | -------------------------------------------------------------------------------- /docs_src/registry/default_schema.py: -------------------------------------------------------------------------------- 1 | from saffier import Database, Registry 2 | 3 | database = Database("") 4 | registry = Registry(database=database) 5 | 6 | 7 | async def get_default_schema() -> str: 8 | """ 9 | Returns the default schema name of the given database 10 | """ 11 | await registry.schema.get_default_schema() 12 | -------------------------------------------------------------------------------- /docs_src/registry/drop_schema.py: -------------------------------------------------------------------------------- 1 | from saffier import Database, Registry 2 | 3 | database = Database("") 4 | registry = Registry(database=database) 5 | 6 | 7 | async def drop_schema(name: str) -> None: 8 | """ 9 | Drops a schema from the database. 10 | """ 11 | await registry.schema.drop_schema(name, if_exists=True) 12 | -------------------------------------------------------------------------------- /docs_src/registry/extra/create.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier.core.db import fields 3 | from saffier.testclient import DatabaseTestClient as Database 4 | 5 | database = Database("") 6 | alternative = Database("") 7 | models = saffier.Registry(database=database, extra={"alternative": alternative}) 8 | 9 | 10 | class User(saffier.Model): 11 | id = fields.IntegerField(primary_key=True) 12 | name = fields.CharField(max_length=255) 13 | email = fields.CharField(max_length=255) 14 | 15 | class Meta: 16 | registry = models 17 | 18 | 19 | async def bulk_create_users() -> None: 20 | """ 21 | Bulk creates some users. 22 | """ 23 | await User.query.using_with_db("alternative").bulk_create( 24 | [ 25 | {"name": "Edgy", "email": "saffier@example.com"}, 26 | {"name": "Edgy Alternative", "email": "saffier.alternative@example.com"}, 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /docs_src/registry/extra/declaration.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier.core.db import fields 3 | from saffier.testclient import DatabaseTestClient as Database 4 | 5 | database = Database("") 6 | alternative = Database("") 7 | models = saffier.Registry(database=database, extra={"alternative": alternative}) 8 | 9 | 10 | class User(saffier.Model): 11 | id = fields.IntegerField(primary_key=True) 12 | name = fields.CharField(max_length=255) 13 | email = fields.CharField(max_length=255) 14 | 15 | class Meta: 16 | registry = models 17 | -------------------------------------------------------------------------------- /docs_src/registry/model.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | """ 10 | The User model to be created in the database as a table 11 | If no name is provided the in Meta class, it will generate 12 | a "users" table for you. 13 | """ 14 | 15 | id = saffier.IntegerField(primary_key=True) 16 | is_active = saffier.BooleanField(default=False) 17 | 18 | class Meta: 19 | registry = models 20 | -------------------------------------------------------------------------------- /docs_src/registry/multiple.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | 5 | class MyRegistry(Registry): 6 | """ 7 | Add logic unique to your registry or override 8 | existing functionality. 9 | """ 10 | 11 | ... 12 | 13 | 14 | database = Database("sqlite:///db.sqlite") 15 | models = MyRegistry(database=database) 16 | 17 | 18 | class User(saffier.Model): 19 | is_active = saffier.BooleanField(default=False) 20 | 21 | class Meta: 22 | registry = models 23 | 24 | 25 | another_db = Database("postgressql://user:password@localhost:5432/mydb") 26 | another_registry = MyRegistry(another_db=another_db) 27 | 28 | 29 | class Profile(saffier.Model): 30 | is_active = saffier.BooleanField(default=False) 31 | 32 | class Meta: 33 | registry = another_registry 34 | -------------------------------------------------------------------------------- /docs_src/relationships/model.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | is_active = saffier.BooleanField(default=True) 10 | first_name = saffier.CharField(max_length=50, null=True) 11 | last_name = saffier.CharField(max_length=50, null=True) 12 | email = saffier.EmailField(max_lengh=100) 13 | password = saffier.CharField(max_length=1000, null=True) 14 | 15 | class Meta: 16 | registry = models 17 | 18 | 19 | class Profile(saffier.Model): 20 | user = saffier.ForeignKey(User, on_delete=saffier.CASCADE) 21 | 22 | class Meta: 23 | registry = models 24 | -------------------------------------------------------------------------------- /docs_src/relationships/multiple.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | is_active = saffier.BooleanField(default=True) 10 | first_name = saffier.CharField(max_length=50) 11 | last_name = saffier.CharField(max_length=50) 12 | email = saffier.EmailField(max_lengh=100) 13 | password = saffier.CharField(max_length=1000) 14 | 15 | class Meta: 16 | registry = models 17 | 18 | 19 | class Thread(saffier.Model): 20 | sender = saffier.ForeignKey( 21 | User, 22 | on_delete=saffier.CASCADE, 23 | related_name="sender", 24 | ) 25 | receiver = saffier.ForeignKey( 26 | User, 27 | on_delete=saffier.CASCADE, 28 | related_name="receiver", 29 | ) 30 | message = saffier.TextField() 31 | 32 | class Meta: 33 | registry = models 34 | -------------------------------------------------------------------------------- /docs_src/relationships/onetoone.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | is_active = saffier.BooleanField(default=True) 10 | first_name = saffier.CharField(max_length=50) 11 | last_name = saffier.CharField(max_length=50) 12 | email = saffier.EmailField(max_lengh=100) 13 | password = saffier.CharField(max_length=1000) 14 | 15 | class Meta: 16 | registry = models 17 | 18 | 19 | class Profile(saffier.Model): 20 | user = saffier.OneToOneField(User, on_delete=saffier.CASCADE) 21 | 22 | class Meta: 23 | registry = models 24 | -------------------------------------------------------------------------------- /docs_src/settings/custom_settings.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from saffier import SaffierSettings 4 | from saffier.conf.enums import EnvironmentType 5 | 6 | 7 | class MyCustomSettings(SaffierSettings): 8 | """ 9 | My settings overriding default values and add new ones. 10 | """ 11 | 12 | environment: Optional[str] = EnvironmentType.TESTING 13 | 14 | # new settings 15 | my_new_setting: str = "A text" 16 | -------------------------------------------------------------------------------- /docs_src/signals/custom.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | database = saffier.Database("sqlite:///db.sqlite") 4 | registry = saffier.Registry(database=database) 5 | 6 | 7 | class User(saffier.Model): 8 | id = saffier.BigIntegerField(primary_key=True) 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.CharField(max_length=255) 11 | 12 | class Meta: 13 | registry = registry 14 | 15 | 16 | # Create the custom signal 17 | User.meta.signals.on_verify = saffier.Signal() 18 | -------------------------------------------------------------------------------- /docs_src/signals/disconnect.py: -------------------------------------------------------------------------------- 1 | async def trigger_notifications(sender, instance, **kwargs): 2 | """ 3 | Sends email and push notification 4 | """ 5 | send_email(instance.email) 6 | send_push_notification(instance.email) 7 | 8 | 9 | # Disconnect the given function 10 | User.meta.signals.on_verify.disconnect(trigger_notifications) 11 | -------------------------------------------------------------------------------- /docs_src/signals/logic.py: -------------------------------------------------------------------------------- 1 | async def create_user(**kwargs): 2 | """ 3 | Creates a user 4 | """ 5 | await User.query.create(**kwargs) 6 | 7 | 8 | async def is_verified_user(id: int): 9 | """ 10 | Checks if user is verified and sends notification 11 | if true. 12 | """ 13 | user = await User.query.get(pk=id) 14 | 15 | if user.is_verified: 16 | # triggers the custom signal 17 | await User.meta.signals.on_verify.send(sender=User, instance=user) 18 | -------------------------------------------------------------------------------- /docs_src/signals/receiver/disconnect.py: -------------------------------------------------------------------------------- 1 | from saffier.core.signals import post_save 2 | 3 | 4 | def send_notification(email: str) -> None: 5 | """ 6 | Sends a notification to the user 7 | """ 8 | send_email_confirmation(email) 9 | 10 | 11 | @post_save(User) 12 | async def after_creation(sender, instance, **kwargs): 13 | """ 14 | Sends a notification to the user 15 | """ 16 | send_notification(instance.email) 17 | 18 | 19 | # Disconnect the given function 20 | User.meta.signals.post_save.disconnect(after_creation) 21 | 22 | # Signals are also exposed via instance 23 | user.signals.post_save.disconnect(after_creation) 24 | -------------------------------------------------------------------------------- /docs_src/signals/receiver/model.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | database = saffier.Database("sqlite:///db.sqlite") 4 | registry = saffier.Registry(database=database) 5 | 6 | 7 | class User(saffier.Model): 8 | id = saffier.BigIntegerField(primary_key=True) 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.CharField(max_length=255) 11 | is_verified = saffier.BooleanField(default=False) 12 | 13 | class Meta: 14 | registry = registry 15 | -------------------------------------------------------------------------------- /docs_src/signals/receiver/multiple.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | database = saffier.Database("sqlite:///db.sqlite") 4 | registry = saffier.Registry(database=database) 5 | 6 | 7 | class User(saffier.Model): 8 | id = saffier.BigIntegerField(primary_key=True) 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.CharField(max_length=255) 11 | 12 | class Meta: 13 | registry = registry 14 | 15 | 16 | class Profile(saffier.Model): 17 | id = saffier.BigIntegerField(primary_key=True) 18 | profile_type = saffier.CharField(max_length=255) 19 | 20 | class Meta: 21 | registry = registry 22 | -------------------------------------------------------------------------------- /docs_src/signals/receiver/multiple_receivers.py: -------------------------------------------------------------------------------- 1 | from saffier.core.signals import post_save 2 | 3 | 4 | def push_notification(email: str) -> None: 5 | # Sends a push notification 6 | ... 7 | 8 | 9 | def send_email(email: str) -> None: 10 | # Sends an email 11 | ... 12 | 13 | 14 | @post_save(User) 15 | async def after_creation(sender, instance, **kwargs): 16 | """ 17 | Sends a notification to the user 18 | """ 19 | send_email(instance.email) 20 | 21 | 22 | @post_save(User) 23 | async def do_something_else(sender, instance, **kwargs): 24 | """ 25 | Sends a notification to the user 26 | """ 27 | push_notification(instance.email) 28 | -------------------------------------------------------------------------------- /docs_src/signals/receiver/post_multiple.py: -------------------------------------------------------------------------------- 1 | from saffier.core.signals import post_save 2 | 3 | 4 | def send_notification(email: str) -> None: 5 | """ 6 | Sends a notification to the user 7 | """ 8 | send_email_confirmation(email) 9 | 10 | 11 | @post_save([User, Profile]) 12 | async def after_creation(sender, instance, **kwargs): 13 | """ 14 | Sends a notification to the user 15 | """ 16 | if isinstance(instance, User): 17 | send_notification(instance.email) 18 | else: 19 | # something else for Profile 20 | ... 21 | -------------------------------------------------------------------------------- /docs_src/signals/receiver/post_save.py: -------------------------------------------------------------------------------- 1 | from saffier.core.signals import post_save 2 | 3 | 4 | def send_notification(email: str) -> None: 5 | """ 6 | Sends a notification to the user 7 | """ 8 | send_email_confirmation(email) 9 | 10 | 11 | @post_save(User) 12 | async def after_creation(sender, instance, **kwargs): 13 | """ 14 | Sends a notification to the user 15 | """ 16 | send_notification(instance.email) 17 | -------------------------------------------------------------------------------- /docs_src/signals/register.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | 3 | database = saffier.Database("sqlite:///db.sqlite") 4 | registry = saffier.Registry(database=database) 5 | 6 | 7 | class User(saffier.Model): 8 | id = saffier.BigIntegerField(primary_key=True) 9 | name = saffier.CharField(max_length=255) 10 | email = saffier.CharField(max_length=255) 11 | 12 | class Meta: 13 | registry = registry 14 | 15 | 16 | # Create the custom signal 17 | User.meta.signals.on_verify = saffier.Signal() 18 | 19 | 20 | # Create the receiver 21 | async def trigger_notifications(sender, instance, **kwargs): 22 | """ 23 | Sends email and push notification 24 | """ 25 | send_email(instance.email) 26 | send_push_notification(instance.email) 27 | 28 | 29 | # Register the receiver into the new Signal. 30 | User.meta.signals.on_verify.connect(trigger_notifications) 31 | -------------------------------------------------------------------------------- /docs_src/tenancy/contrib/domain_mixin.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier.contrib.multi_tenancy import TenantRegistry 3 | from saffier.contrib.multi_tenancy.models import DomainMixin, TenantMixin 4 | 5 | database = saffier.Database("") 6 | registry = TenantRegistry(database=database) 7 | 8 | 9 | class Tenant(TenantMixin): 10 | """ 11 | Inherits all the fields from the `TenantMixin`. 12 | """ 13 | 14 | class Meta: 15 | registry = registry 16 | 17 | 18 | class Domain(DomainMixin): 19 | """ 20 | Inherits all the fields from the `DomainMixin`. 21 | """ 22 | 23 | class Meta: 24 | registry = registry 25 | -------------------------------------------------------------------------------- /docs_src/tenancy/contrib/example/api.py: -------------------------------------------------------------------------------- 1 | from esmerald import JSONResponse, get 2 | from myapp.models import Product 3 | 4 | 5 | @get("/products") 6 | async def get_products() -> JSONResponse: 7 | """ 8 | Returns the products associated to a tenant or 9 | all the "shared" products if tenant is None. 10 | 11 | The tenant was set in the `TenantMiddleware` which 12 | means that there is no need to use the `using` anymore. 13 | """ 14 | products = await Product.query.all() 15 | products = [product.pk for product in products] 16 | return JSONResponse(products) 17 | -------------------------------------------------------------------------------- /docs_src/tenancy/contrib/example/app.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from esmerald import Esmerald, Gateway, JSONResponse, get 4 | from myapp.middleware import TenantMiddleware 5 | from myapp.models import Product 6 | 7 | import saffier 8 | 9 | database = saffier.Database("") 10 | models = saffier.Registry(database=database) 11 | 12 | 13 | @get("/products") 14 | async def get_products() -> JSONResponse: 15 | """ 16 | Returns the products associated to a tenant or 17 | all the "shared" products if tenant is None. 18 | 19 | The tenant was set in the `TenantMiddleware` which 20 | means that there is no need to use the `using` anymore. 21 | """ 22 | products = await Product.query.all() 23 | products = [product.pk for product in products] 24 | return JSONResponse(products) 25 | 26 | 27 | app = Esmerald( 28 | routes=[Gateway(handler=get_products)], 29 | on_startup=[database.connect], 30 | on_shutdown=[database.disconnect], 31 | middleware=[TenantMiddleware], 32 | ) 33 | -------------------------------------------------------------------------------- /docs_src/tenancy/contrib/example/mock_data.py: -------------------------------------------------------------------------------- 1 | from myapp.models import HubUser, Product, Tenant, TenantUser, User 2 | 3 | from saffier import Database 4 | 5 | database = Database("") 6 | 7 | 8 | async def create_data(): 9 | """ 10 | Creates mock data 11 | """ 12 | # Global users 13 | john = await User.query.create(name="John Doe", email="john.doe@esmerald.dev") 14 | saffier = await User.query.create(name="Saffier", email="saffier@esmerald.dev") 15 | 16 | # Tenant 17 | edgy_tenant = await Tenant.query.create(schema_name="saffier", tenant_name="saffier") 18 | 19 | # HubUser - A user specific inside the saffier schema 20 | edgy_schema_user = await HubUser.query.using(edgy_tenant.schema_name).create( 21 | name="saffier", email="saffier@esmerald.dev" 22 | ) 23 | 24 | await TenantUser.query.create(user=saffier, tenant=edgy_tenant) 25 | 26 | # Products for Saffier HubUser specific 27 | for i in range(10): 28 | await Product.query.using(edgy_tenant.schema_name).create( 29 | name=f"Product-{i}", user=edgy_schema_user 30 | ) 31 | 32 | # Products for the John without a tenant associated 33 | for i in range(25): 34 | await Product.query.create(name=f"Product-{i}", user=john) 35 | 36 | 37 | # Start the db 38 | await database.connect() 39 | 40 | # Run the create_data 41 | await create_data() 42 | 43 | # Close the database connection 44 | await database.disconnect() 45 | -------------------------------------------------------------------------------- /docs_src/tenancy/contrib/example/queries.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | 3 | # Query the products for the `Saffier` user from the `saffier` schema 4 | # by passing the tenant and email header. 5 | async with httpx.AsyncClient() as client: 6 | response = await client.get( 7 | "/products", headers={"tenant": "saffier", "email": "saffier@esmerald.dev"} 8 | ) 9 | assert response.status_code == 200 10 | assert len(response.json()) == 10 # total inserted in the `saffier` schema. 11 | 12 | # Query the shared database, so no tenant or email associated 13 | # In the headers. 14 | async with httpx.AsyncClient() as client: 15 | response = await client.get("/products") 16 | assert response.status_code == 200 17 | assert len(response.json()) == 25 # total inserted in the `shared` database. 18 | -------------------------------------------------------------------------------- /docs_src/tenancy/contrib/example/settings.py: -------------------------------------------------------------------------------- 1 | from saffier.contrib.multi_tenancy.settings import TenancySettings 2 | 3 | 4 | class EdgySettings(TenancySettings): 5 | tenant_model: str = "Tenant" 6 | """ 7 | The Tenant model created 8 | """ 9 | auth_user_model: str = "User" 10 | """ 11 | The `user` table created. Not the `HubUser`! 12 | """ 13 | -------------------------------------------------------------------------------- /docs_src/tenancy/contrib/tenant_mixin.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier.contrib.multi_tenancy import TenantRegistry 3 | from saffier.contrib.multi_tenancy.models import TenantMixin 4 | 5 | database = saffier.Database("") 6 | registry = TenantRegistry(database=database) 7 | 8 | 9 | class Tenant(TenantMixin): 10 | """ 11 | Inherits all the fields from the `TenantMixin`. 12 | """ 13 | 14 | class Meta: 15 | registry = registry 16 | -------------------------------------------------------------------------------- /docs_src/tenancy/contrib/tenant_model.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier.contrib.multi_tenancy import TenantModel, TenantRegistry 3 | 4 | database = saffier.Database("") 5 | registry = TenantRegistry(database=database) 6 | 7 | 8 | class User(TenantModel): 9 | """ 10 | A `users` table that should be created in the `shared` schema 11 | (or public) and in the subsequent new schemas. 12 | """ 13 | 14 | name = saffier.CharField(max_length=255) 15 | email = saffier.CharField(max_length=255) 16 | 17 | class Meta: 18 | registry = registry 19 | is_tenant = True 20 | -------------------------------------------------------------------------------- /docs_src/tenancy/contrib/tenant_registry.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier.contrib.multi_tenancy import TenantRegistry 3 | 4 | database = saffier.Database("") 5 | registry = TenantRegistry(database=database) 6 | -------------------------------------------------------------------------------- /docs_src/tenancy/contrib/tenant_user_mixin.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier.contrib.multi_tenancy import TenantRegistry 3 | from saffier.contrib.multi_tenancy.models import DomainMixin, TenantMixin, TenantUserMixin 4 | 5 | database = saffier.Database("") 6 | registry = TenantRegistry(database=database) 7 | 8 | 9 | class Tenant(TenantMixin): 10 | """ 11 | Inherits all the fields from the `TenantMixin`. 12 | """ 13 | 14 | class Meta: 15 | registry = registry 16 | 17 | 18 | class Domain(DomainMixin): 19 | """ 20 | Inherits all the fields from the `DomainMixin`. 21 | """ 22 | 23 | class Meta: 24 | registry = registry 25 | 26 | 27 | class TenantUser(TenantUserMixin): 28 | """ 29 | Inherits all the fields from the `TenantUserMixin`. 30 | """ 31 | 32 | class Meta: 33 | registry = registry 34 | -------------------------------------------------------------------------------- /docs_src/tenancy/example/api.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from esmerald import Esmerald, Gateway, JSONResponse, get 4 | 5 | import saffier 6 | 7 | database = saffier.Database("") 8 | models = saffier.Registry(database=database) 9 | 10 | 11 | @get("/products") 12 | async def products() -> JSONResponse: 13 | """ 14 | Returns the products associated to a tenant or 15 | all the "shared" products if tenant is None. 16 | """ 17 | products = await Product.query.all() 18 | products = [product.pk for product in products] 19 | return JSONResponse(products) 20 | 21 | 22 | app = Esmerald( 23 | routes=[Gateway(handler=products)], 24 | on_startup=[database.connect], 25 | on_shutdown=[database.disconnect], 26 | middleware=[TenantMiddleware], 27 | ) 28 | -------------------------------------------------------------------------------- /docs_src/tenancy/example/data.py: -------------------------------------------------------------------------------- 1 | async def create_data(): 2 | """ 3 | Creates mock data. 4 | """ 5 | # Create some users in the main users table 6 | esmerald = await User.query.create(name="esmerald") 7 | 8 | # Create a tenant for Saffier (only) 9 | tenant = await Tenant.query.create( 10 | schema_name="saffier", 11 | tenant_name="saffier", 12 | ) 13 | 14 | # Create a user in the `User` table inside the `saffier` tenant. 15 | saffier = await User.query.using(tenant.schema_name).create( 16 | name="Saffier schema user", 17 | ) 18 | 19 | # Products for Saffier (inside saffier schema) 20 | for i in range(10): 21 | await Product.query.using(tenant.schema_name).create( 22 | name=f"Product-{i}", 23 | user=saffier, 24 | ) 25 | 26 | # Products for Esmerald (no schema associated, defaulting to the public schema or "shared") 27 | for i in range(25): 28 | await Product.query.create(name=f"Product-{i}", user=esmerald) 29 | -------------------------------------------------------------------------------- /docs_src/tenancy/example/middleware.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Coroutine 2 | 3 | from esmerald import Request 4 | from esmerald.protocols.middleware import MiddlewareProtocol 5 | from lilya.types import ASGIApp, Receive, Scope, Send 6 | 7 | from saffier.core.db import set_tenant 8 | from saffier.exceptions import ObjectNotFound 9 | 10 | 11 | class TenantMiddleware(MiddlewareProtocol): 12 | def __init__(self, app: "ASGIApp"): 13 | super().__init__(app) 14 | self.app = app 15 | 16 | async def __call__( 17 | self, scope: Scope, receive: Receive, send: Send 18 | ) -> Coroutine[Any, Any, None]: 19 | """ 20 | Receives a header with the tenant information and lookup in 21 | the database if exists. 22 | 23 | Sets the tenant if true, or none otherwise. 24 | """ 25 | request = Request(scope=scope, receive=receive, send=send) 26 | tenant_header = request.headers.get("tenant", None) 27 | 28 | try: 29 | user = await Tenant.query.get(schema_name=tenant_header) 30 | tenant = user.schema_name 31 | except ObjectNotFound: 32 | tenant = None 33 | 34 | set_tenant(tenant) 35 | await self.app(scope, receive, send) 36 | -------------------------------------------------------------------------------- /docs_src/tenancy/example/query.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | 3 | 4 | async def query(): 5 | response = await httpx.get("/products", headers={"tenant": "saffier"}) 6 | 7 | # Total products created for `saffier` schema 8 | assert len(response.json()) == 10 9 | 10 | # Response for the "shared", no tenant associated. 11 | response = await httpx.get("/products") 12 | assert len(response.json()) == 25 13 | -------------------------------------------------------------------------------- /docs_src/tenancy/using/schemas.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | id = saffier.IntegerField(primary_key=True) 10 | is_active = saffier.BooleanField(default=False) 11 | 12 | class Meta: 13 | registry = models 14 | -------------------------------------------------------------------------------- /docs_src/tips/connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Generated by 'esmerald-admin createproject' 4 | """ 5 | import os 6 | import sys 7 | from pathlib import Path 8 | 9 | from esmerald import Esmerald, Include 10 | from my_project.utils import get_db_connection 11 | 12 | from saffier.cli import Migrate 13 | 14 | 15 | def build_path(): 16 | """ 17 | Builds the path of the project and project root. 18 | """ 19 | Path(__file__).resolve().parent.parent 20 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__)) 21 | 22 | if SITE_ROOT not in sys.path: 23 | sys.path.append(SITE_ROOT) 24 | sys.path.append(os.path.join(SITE_ROOT, "apps")) 25 | 26 | 27 | def get_application(): 28 | """ 29 | This is optional. The function is only used for organisation purposes. 30 | """ 31 | build_path() 32 | database, registry = get_db_connection() 33 | 34 | app = Esmerald( 35 | routes=[Include(namespace="my_project.urls")], 36 | on_startup=[database.connect], 37 | on_shutdown=[database.disconnect], 38 | ) 39 | 40 | Migrate(app=app, registry=registry) 41 | return app 42 | 43 | 44 | app = get_application() 45 | -------------------------------------------------------------------------------- /docs_src/tips/lru.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | 3 | from esmerald.conf import settings 4 | 5 | 6 | @lru_cache() 7 | def get_db_connection(): 8 | database, registry = settings.db_connection 9 | return database, registry 10 | -------------------------------------------------------------------------------- /docs_src/tips/migrations.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Generated by 'esmerald-admin createproject' 4 | """ 5 | import os 6 | import sys 7 | from pathlib import Path 8 | 9 | from esmerald import Esmerald, Include 10 | from my_project.utils import get_db_connection 11 | 12 | from saffier.cli import Migrate 13 | 14 | 15 | def build_path(): 16 | """ 17 | Builds the path of the project and project root. 18 | """ 19 | Path(__file__).resolve().parent.parent 20 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__)) 21 | 22 | if SITE_ROOT not in sys.path: 23 | sys.path.append(SITE_ROOT) 24 | sys.path.append(os.path.join(SITE_ROOT, "apps")) 25 | 26 | 27 | def get_application(): 28 | """ 29 | This is optional. The function is only used for organisation purposes. 30 | """ 31 | build_path() 32 | database, registry = get_db_connection() 33 | 34 | app = Esmerald( 35 | routes=[Include(namespace="my_project.urls")], 36 | ) 37 | 38 | Migrate(app=app, registry=registry) 39 | return app 40 | 41 | 42 | app = get_application() 43 | -------------------------------------------------------------------------------- /docs_src/tips/models.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from my_project.utils import get_db_connection 4 | 5 | import saffier 6 | 7 | _, registry = get_db_connection() 8 | 9 | 10 | class ProfileChoice(Enum): 11 | ADMIN = "ADMIN" 12 | USER = "USER" 13 | 14 | 15 | class BaseModel(saffier.Model): 16 | class Meta: 17 | abstract = True 18 | registry = registry 19 | 20 | 21 | class User(BaseModel): 22 | """ 23 | Base model for a user 24 | """ 25 | 26 | first_name = saffier.CharField(max_length=150) 27 | last_name = saffier.CharField(max_length=150) 28 | username = saffier.CharField(max_length=150, unique=True) 29 | email = saffier.EmailField(max_length=120, unique=True) 30 | password = saffier.CharField(max_length=128) 31 | last_login = saffier.DateTimeField(null=True) 32 | is_active = saffier.BooleanField(default=True) 33 | is_staff = saffier.BooleanField(default=False) 34 | is_superuser = saffier.BooleanField(default=False) 35 | 36 | 37 | class Profile(BaseModel): 38 | """ 39 | A profile for a given user. 40 | """ 41 | 42 | user = saffier.OneToOneField(User, on_delete=saffier.CASCADE) 43 | profile_type = saffier.ChoiceField(ProfileChoice, default=ProfileChoice.USER) 44 | -------------------------------------------------------------------------------- /docs_src/tips/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generated by 'esmerald-admin createproject' 3 | """ 4 | 5 | from functools import cached_property 6 | from typing import Optional, Tuple 7 | 8 | from esmerald.conf.enums import EnvironmentType 9 | from esmerald.conf.global_settings import EsmeraldAPISettings 10 | 11 | from saffier import Database, Registry 12 | 13 | 14 | class AppSettings(EsmeraldAPISettings): 15 | app_name: str = "My application in production mode." 16 | environment: Optional[str] = EnvironmentType.PRODUCTION 17 | secret_key: str = "esmerald-insecure-h35r*b9$+hw-x2hnt5c)vva=!zn$*a7#" # auto generated 18 | 19 | @cached_property 20 | def db_connection(self) -> Tuple[Database, Registry]: 21 | """ 22 | To make sure the registry and the database connection remains the same 23 | all the time, always use the cached_property. 24 | """ 25 | database = Database("postgresql+asyncpg://user:pass@localhost:5432/my_database") 26 | return database, Registry(database=database) 27 | -------------------------------------------------------------------------------- /docs_src/transactions/context_manager.py: -------------------------------------------------------------------------------- 1 | from esmerald import Request, post 2 | from models import Profile, User 3 | from pydantic import BaseModel, EmailStr 4 | 5 | from saffier import Database, Registry 6 | 7 | # These settings should be placed somewhere 8 | # Central where it can be accessed anywhere. 9 | database = Database("sqlite:///db.sqlite") 10 | models = Registry(database=database) 11 | 12 | 13 | class UserIn(BaseModel): 14 | email: EmailStr 15 | 16 | 17 | @post("/create", description="Creates a user and associates to a profile.") 18 | async def create_user(data: UserIn, request: Request) -> None: 19 | # This database insert occurs within a transaction. 20 | # It will be rolled back by the `RuntimeError`. 21 | 22 | async with database.transaction(): 23 | user = await User.query.create(email=data.email, is_active=True) 24 | await Profile.query.create(user=user) 25 | raise RuntimeError() 26 | -------------------------------------------------------------------------------- /docs_src/transactions/decorator.py: -------------------------------------------------------------------------------- 1 | from esmerald import Request, post 2 | from models import Profile, User 3 | from pydantic import BaseModel, EmailStr 4 | 5 | from saffier import Database, Registry 6 | 7 | # These settings should be placed somewhere 8 | # Central where it can be accessed anywhere. 9 | database = Database("sqlite:///db.sqlite") 10 | models = Registry(database=database) 11 | 12 | 13 | class UserIn(BaseModel): 14 | email: EmailStr 15 | 16 | 17 | @post("/create", description="Creates a user and associates to a profile.") 18 | @database.transaction() 19 | async def create_user(data: UserIn, request: Request) -> None: 20 | # This database insert occurs within a transaction. 21 | # It will be rolled back by the `RuntimeError`. 22 | 23 | user = await User.query.create(email=data.email, is_active=True) 24 | await Profile.query.create(user=user) 25 | raise RuntimeError() 26 | -------------------------------------------------------------------------------- /docs_src/transactions/models.py: -------------------------------------------------------------------------------- 1 | import saffier 2 | from saffier import Database, Registry 3 | 4 | database = Database("sqlite:///db.sqlite") 5 | models = Registry(database=database) 6 | 7 | 8 | class User(saffier.Model): 9 | """ 10 | The User model to be created in the database as a table 11 | If no name is provided the in Meta class, it will generate 12 | a "users" table for you. 13 | """ 14 | 15 | email = saffier.EmailField(unique=True, max_length=120) 16 | is_active = saffier.BooleanField(default=False) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | class Profile(saffier.Model): 23 | user = saffier.ForeignKey(User, on_delete=saffier.CASCADE) 24 | 25 | class Meta: 26 | registry = models 27 | -------------------------------------------------------------------------------- /saffier/__main__.py: -------------------------------------------------------------------------------- 1 | from saffier.cli.cli import saffier_cli 2 | 3 | 4 | def run_cli() -> None: 5 | saffier_cli() 6 | 7 | 8 | if __name__ == "__main__": # pragma: no cover 9 | run_cli() 10 | -------------------------------------------------------------------------------- /saffier/cli/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Migrate, alembic_version 2 | 3 | __all__ = ["alembic_version", "Migrate"] 4 | -------------------------------------------------------------------------------- /saffier/cli/constants.py: -------------------------------------------------------------------------------- 1 | SAFFIER_DISCOVER_APP = "SAFFIER_DEFAULT_APP" 2 | DEFAULT_TEMPLATE_NAME = "default" 3 | APP_PARAMETER = "--app" 4 | HELP_PARAMETER = "--help" 5 | DISCOVERY_FILES = ["application.py", "app.py", "main.py"] 6 | DISCOVERY_FUNCTIONS = ["get_application", "get_app"] 7 | SAFFIER_DB = "_saffier_db" 8 | SAFFIER_EXTRA = "_saffier_extra" 9 | EXCLUDED_COMMANDS = ["list-templates"] 10 | IGNORE_COMMANDS = ["list-templates"] 11 | -------------------------------------------------------------------------------- /saffier/cli/decorators.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from functools import wraps 3 | from typing import Any, TypeVar 4 | 5 | from alembic.util import CommandError 6 | from loguru import logger 7 | 8 | T = TypeVar("T") 9 | 10 | 11 | def catch_errors(fn: T) -> T: 12 | @wraps(fn) # type: ignore 13 | def wrap(*args: Any, **kwargs: Any) -> T: 14 | try: 15 | fn(*args, **kwargs) # type: ignore 16 | except (CommandError, RuntimeError) as exc: 17 | logger.error(f"Error: {str(exc)}") 18 | sys.exit(1) 19 | 20 | return wrap # type: ignore 21 | -------------------------------------------------------------------------------- /saffier/cli/operations/__init__.py: -------------------------------------------------------------------------------- 1 | from .branches import branches as branches # noqa 2 | from .check import check as check # noqa 3 | from .current import current as current # noqa 4 | from .downgrade import downgrade as downgrade # noqa 5 | from .edit import edit as edit # noqa 6 | from .heads import heads as heads # noqa 7 | from .history import history as history # noqa 8 | from .init import init as init # noqa 9 | from .inspectdb import inspect_db as inspect_db # noqa 10 | from .list_templates import list_templates as list_templates # noqa 11 | from .makemigrations import makemigrations as makemigrations # noqa 12 | from .merge import merge as merge # noqa 13 | from .migrate import migrate as migrate # noqa 14 | from .revision import revision as revision # noqa 15 | from .shell import shell as shell # noqa 16 | from .show import show as show # noqa 17 | from .stamp import stamp as stamp # noqa 18 | -------------------------------------------------------------------------------- /saffier/cli/operations/branches.py: -------------------------------------------------------------------------------- 1 | """ 2 | Client to interact with Saffier models and migrations. 3 | """ 4 | 5 | import click 6 | 7 | from saffier.cli.base import branches as _branches 8 | from saffier.cli.env import MigrationEnv 9 | 10 | 11 | @click.option( 12 | "-d", 13 | "--directory", 14 | default=None, 15 | help=('Migration script directory (default is "migrations")'), 16 | ) 17 | @click.option("-v", "--verbose", is_flag=True, help="Use more verbose output") 18 | @click.command() 19 | def branches(env: MigrationEnv, directory: str, verbose: bool) -> None: 20 | """Show current branch points""" 21 | _branches(env.app, directory, verbose) 22 | -------------------------------------------------------------------------------- /saffier/cli/operations/check.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from saffier.cli.base import check as _check 4 | from saffier.cli.env import MigrationEnv 5 | 6 | 7 | @click.option( 8 | "-d", 9 | "--directory", 10 | default=None, 11 | help=('Migration script directory (default is "migrations")'), 12 | ) 13 | @click.command() 14 | def check(env: MigrationEnv, directory: str) -> None: 15 | """Check if there are any new operations to migrate""" 16 | _check(env.app, directory) 17 | -------------------------------------------------------------------------------- /saffier/cli/operations/current.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from saffier.cli.base import current as _current 4 | from saffier.cli.env import MigrationEnv 5 | 6 | 7 | @click.option( 8 | "-d", 9 | "--directory", 10 | default=None, 11 | help=('Migration script directory (default is "migrations")'), 12 | ) 13 | @click.option("-v", "--verbose", is_flag=True, help="Use more verbose output") 14 | @click.command() 15 | def current(env: MigrationEnv, directory: str, verbose: bool) -> None: 16 | """Display the current revision for each database.""" 17 | _current(env.app, directory, verbose) 18 | -------------------------------------------------------------------------------- /saffier/cli/operations/downgrade.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import click 4 | 5 | from saffier.cli.base import downgrade as _downgrade 6 | from saffier.cli.env import MigrationEnv 7 | 8 | 9 | @click.option( 10 | "-d", 11 | "--directory", 12 | default=None, 13 | help=('Migration script directory (default is "migrations")'), 14 | ) 15 | @click.option( 16 | "--sql", is_flag=True, help=("Don't emit SQL to database - dump to standard output " "instead") 17 | ) 18 | @click.option( 19 | "--tag", default=None, help=('Arbitrary "tag" name - can be used by custom env.py ' "scripts") 20 | ) 21 | @click.option( 22 | "-x", "--arg", multiple=True, help="Additional arguments consumed by custom env.py scripts" 23 | ) 24 | @click.command() 25 | @click.argument("revision", default="-1") 26 | def downgrade( 27 | env: MigrationEnv, directory: str, sql: bool, tag: str, arg: Any, revision: str 28 | ) -> None: 29 | """Revert to a previous version""" 30 | _downgrade(env.app, directory, revision, sql, tag, arg) 31 | -------------------------------------------------------------------------------- /saffier/cli/operations/edit.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from saffier.cli.base import edit as _edit 4 | from saffier.cli.env import MigrationEnv 5 | 6 | 7 | @click.option( 8 | "-d", 9 | "--directory", 10 | default=None, 11 | help=('Migration script directory (default is "migrations")'), 12 | ) 13 | @click.command() 14 | @click.argument("revision", default="head") 15 | def edit(env: MigrationEnv, directory: str, revision: str) -> None: 16 | """Edit a revision file""" 17 | _edit(env.app, directory, revision) 18 | -------------------------------------------------------------------------------- /saffier/cli/operations/heads.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from saffier.cli.base import heads as _heads 4 | from saffier.cli.env import MigrationEnv 5 | 6 | 7 | @click.option( 8 | "-d", 9 | "--directory", 10 | default=None, 11 | help=('Migration script directory (default is "migrations")'), 12 | ) 13 | @click.option("-v", "--verbose", is_flag=True, help="Use more verbose output") 14 | @click.option( 15 | "--resolve-dependencies", is_flag=True, help="Treat dependency versions as down revisions" 16 | ) 17 | @click.command() 18 | def heads(env: MigrationEnv, directory: str, verbose: bool, resolve_dependencies: bool) -> None: 19 | """Show current available heads in the script directory""" 20 | _heads(env.app, directory, verbose, resolve_dependencies) 21 | -------------------------------------------------------------------------------- /saffier/cli/operations/history.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from saffier.cli.base import history as _history 4 | from saffier.cli.env import MigrationEnv 5 | 6 | 7 | @click.option( 8 | "-d", 9 | "--directory", 10 | default=None, 11 | help=('Migration script directory (default is "migrations")'), 12 | ) 13 | @click.option( 14 | "-r", "--rev-range", default=None, help="Specify a revision range; format is [start]:[end]" 15 | ) 16 | @click.option("-v", "--verbose", is_flag=True, help="Use more verbose output") 17 | @click.option( 18 | "-i", 19 | "--indicate-current", 20 | is_flag=True, 21 | help=("Indicate current version (Alembic 0.9.9 or greater is " "required)"), 22 | ) 23 | @click.command() 24 | def history( 25 | env: MigrationEnv, directory: str, rev_range: str, verbose: bool, indicate_current: bool 26 | ) -> None: 27 | """List changeset scripts in chronological order.""" 28 | _history(env.app, directory, rev_range, verbose, indicate_current) 29 | -------------------------------------------------------------------------------- /saffier/cli/operations/init.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from saffier.cli.base import init as _init 4 | from saffier.cli.env import MigrationEnv 5 | 6 | 7 | @click.option( 8 | "-d", 9 | "--directory", 10 | default=None, 11 | help=('Migration script directory (default is "migrations")'), 12 | ) 13 | @click.option( 14 | "-t", "--template", default=None, help=('Repository template to use (default is "flask")') 15 | ) 16 | @click.option( 17 | "--package", 18 | is_flag=True, 19 | help=("Write empty __init__.py files to the environment and " "version locations"), 20 | ) 21 | @click.command(name="init") 22 | def init(env: MigrationEnv, directory: str, template: str, package: bool) -> None: 23 | """Creates a new migration repository.""" 24 | _init(env.app, directory, template, package) 25 | -------------------------------------------------------------------------------- /saffier/cli/operations/inspectdb.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import click 4 | 5 | from saffier.utils.inspect import InspectDB 6 | 7 | 8 | @click.option( 9 | "--database", 10 | required=True, 11 | help=("Connection string. Example: postgres+asyncpg://user:password@localhost:5432/my_db"), 12 | ) 13 | @click.option( 14 | "--schema", 15 | default=None, 16 | help=("Database schema to be applied."), 17 | ) 18 | @click.command() 19 | def inspect_db( 20 | database: str, 21 | schema: Union[str, None] = None, 22 | ) -> None: 23 | """ 24 | Inspects an existing database and generates the Edgy reflect models. 25 | """ 26 | inspect_db = InspectDB(database=database, schema=schema) 27 | inspect_db.inspect() 28 | -------------------------------------------------------------------------------- /saffier/cli/operations/list_templates.py: -------------------------------------------------------------------------------- 1 | """ 2 | Client to interact with Saffier models and migrations. 3 | """ 4 | 5 | import click 6 | 7 | from saffier.cli.base import list_templates as template_list 8 | 9 | 10 | @click.command(name="list-templates") 11 | def list_templates() -> None: 12 | """ 13 | Lists all the available templates available to Saffier 14 | """ 15 | template_list() 16 | -------------------------------------------------------------------------------- /saffier/cli/operations/merge.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import click 4 | 5 | from saffier.cli.base import merge as _merge 6 | from saffier.cli.env import MigrationEnv 7 | 8 | 9 | @click.option( 10 | "-d", 11 | "--directory", 12 | default=None, 13 | help=('Migration script directory (default is "migrations")'), 14 | ) 15 | @click.option("-m", "--message", default=None, help="Merge revision message") 16 | @click.option( 17 | "--branch-label", default=None, help=("Specify a branch label to apply to the new revision") 18 | ) 19 | @click.option( 20 | "--rev-id", default=None, help=("Specify a hardcoded revision id instead of generating " "one") 21 | ) 22 | @click.command() 23 | @click.argument("revisions", nargs=-1) 24 | def merge( 25 | env: MigrationEnv, directory: str, message: str, branch_label: str, rev_id: str, revisions: Any 26 | ) -> None: 27 | """Merge two revisions together, creating a new revision file""" 28 | _merge(env.app, directory, revisions, message, branch_label, rev_id) 29 | -------------------------------------------------------------------------------- /saffier/cli/operations/migrate.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import click 4 | 5 | from saffier.cli.base import upgrade as _upgrade 6 | from saffier.cli.env import MigrationEnv 7 | 8 | 9 | @click.option( 10 | "-d", 11 | "--directory", 12 | default=None, 13 | help=('Migration script directory (default is "migrations")'), 14 | ) 15 | @click.option( 16 | "--sql", is_flag=True, help=("Don't emit SQL to database - dump to standard output " "instead") 17 | ) 18 | @click.option( 19 | "--tag", default=None, help=('Arbitrary "tag" name - can be used by custom env.py ' "scripts") 20 | ) 21 | @click.option( 22 | "-x", "--arg", multiple=True, help="Additional arguments consumed by custom env.py scripts" 23 | ) 24 | @click.command() 25 | @click.argument("revision", default="head") 26 | def migrate( 27 | env: MigrationEnv, directory: str, sql: bool, tag: str, arg: Any, revision: str 28 | ) -> None: 29 | """ 30 | Upgrades to the latest version or to a specific version 31 | provided by the --tag. 32 | """ 33 | _upgrade(env.app, directory, revision, sql, tag, arg) 34 | -------------------------------------------------------------------------------- /saffier/cli/operations/shell/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import shell as shell # noqa 2 | -------------------------------------------------------------------------------- /saffier/cli/operations/shell/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ShellOption(str, Enum): 5 | IPYTHON = "ipython" 6 | PTPYTHON = "ptpython" 7 | PYTHON = "python" 8 | -------------------------------------------------------------------------------- /saffier/cli/operations/shell/ipython.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import typing 4 | 5 | from saffier import Registry 6 | from saffier.cli.operations.shell.utils import import_objects 7 | from saffier.conf import settings 8 | from saffier.core.terminal import Print 9 | 10 | printer = Print() 11 | 12 | 13 | def get_ipython_arguments(options: typing.Any = None) -> typing.Any: 14 | """Loads the IPython arguments from the settings or defaults to 15 | main saffier settings. 16 | """ 17 | ipython_args = "IPYTHON_ARGUMENTS" 18 | arguments = getattr(settings, "ipython_args", []) 19 | if not arguments: 20 | arguments = os.environ.get(ipython_args, "").split() 21 | return arguments 22 | 23 | 24 | def get_ipython(app: typing.Any, registry: Registry, options: typing.Any = None) -> typing.Any: 25 | """Gets the IPython shell. 26 | 27 | Loads the initial configurations from the main Saffier settings 28 | and boots up the kernel. 29 | """ 30 | try: 31 | from IPython import start_ipython 32 | 33 | def run_ipython() -> None: 34 | imported_objects = import_objects(app, registry) 35 | ipython_arguments = get_ipython_arguments(options) 36 | start_ipython(argv=ipython_arguments, user_ns=imported_objects) # type: ignore 37 | 38 | except (ModuleNotFoundError, ImportError): 39 | error = "You must have IPython installed to run this. Run `pip install saffier[ipython]`" 40 | printer.write_error(error) 41 | sys.exit(1) 42 | 43 | return run_ipython 44 | -------------------------------------------------------------------------------- /saffier/cli/operations/show.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from saffier.cli.base import show as _show 4 | from saffier.cli.env import MigrationEnv 5 | 6 | 7 | @click.option( 8 | "-d", 9 | "--directory", 10 | default=None, 11 | help=('Migration script directory (default is "migrations")'), 12 | ) 13 | @click.command() 14 | @click.argument("revision", default="head") 15 | def show(env: MigrationEnv, directory: str, revision: str) -> None: 16 | """Show the revision denoted by the given symbol.""" 17 | _show(env.app, directory, revision) 18 | -------------------------------------------------------------------------------- /saffier/cli/operations/stamp.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from saffier.cli.base import stamp as _stamp 4 | from saffier.cli.env import MigrationEnv 5 | 6 | 7 | @click.option( 8 | "-d", 9 | "--directory", 10 | default=None, 11 | help=('Migration script directory (default is "migrations")'), 12 | ) 13 | @click.option( 14 | "--sql", is_flag=True, help=("Don't emit SQL to database - dump to standard output " "instead") 15 | ) 16 | @click.option( 17 | "--tag", default=None, help=('Arbitrary "tag" name - can be used by custom env.py ' "scripts") 18 | ) 19 | @click.argument("revision", default="head") 20 | @click.command() 21 | def stamp(env: MigrationEnv, directory: str, sql: bool, tag: str, revision: str) -> None: 22 | """'stamp' the revision table with the given revision; don't run any 23 | migrations""" 24 | _stamp(env.app, directory, revision, sql, tag) 25 | -------------------------------------------------------------------------------- /saffier/cli/templates/default/README: -------------------------------------------------------------------------------- 1 | Database configuration with Alembic. 2 | -------------------------------------------------------------------------------- /saffier/cli/templates/default/alembic.ini.mako: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,saffier 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_saffier] 38 | level = INFO 39 | handlers = 40 | qualname = saffier 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /saffier/cli/templates/default/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /saffier/conf/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class EnvironmentType(str, Enum): 5 | """An Enum for HTTP methods.""" 6 | 7 | DEVELOPMENT = "development" 8 | TESTING = "testing" 9 | PRODUCTION = "production" 10 | -------------------------------------------------------------------------------- /saffier/conf/global_settings.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import ClassVar, Dict, List, Set 3 | 4 | from dymmond_settings import Settings 5 | 6 | 7 | @dataclass 8 | class SaffierSettings(Settings): 9 | ipython_args: ClassVar[List[str]] = ["--no-banner"] 10 | ptpython_config_file: str = "~/.config/ptpython/config.py" 11 | 12 | # Dialects 13 | postgres_dialects: ClassVar[Set[str]] = {"postgres", "postgresql"} 14 | mysql_dialects: ClassVar[Set[str]] = {"mysql"} 15 | sqlite_dialects: ClassVar[Set[str]] = {"sqlite"} 16 | mssql_dialects: ClassVar[Set[str]] = {"mssql"} 17 | 18 | # Drivers 19 | postgres_drivers: ClassVar[Set[str]] = {"aiopg", "asyncpg"} 20 | mysql_drivers: ClassVar[Set[str]] = {"aiomysql", "asyncmy"} 21 | sqlite_drivers: ClassVar[Set[str]] = {"aiosqlite"} 22 | 23 | @property 24 | def mssql_drivers(self) -> Set[str]: 25 | """ 26 | Do not override this one as SQLAlchemy doesn't support async for MSSQL. 27 | """ 28 | return {"aioodbc"} 29 | 30 | # General settings 31 | default_related_lookup_field: str = "id" 32 | filter_operators: ClassVar[Dict[str, str]] = { 33 | "exact": "__eq__", 34 | "iexact": "ilike", 35 | "contains": "like", 36 | "icontains": "ilike", 37 | "in": "in_", 38 | "gt": "__gt__", 39 | "gte": "__ge__", 40 | "lt": "__lt__", 41 | "lte": "__le__", 42 | } 43 | 44 | many_to_many_relation: str = "relation_{key}" 45 | -------------------------------------------------------------------------------- /saffier/conf/module_import.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | from typing import Any 3 | 4 | 5 | def import_string(dotted_path: str) -> Any: 6 | """ 7 | Import a dotted module path and return the attribute/class designated by the 8 | last name in the path. Raise ImportError if the import failed. 9 | """ 10 | try: 11 | module_path, class_name = dotted_path.rsplit(".", 1) 12 | except ValueError as err: 13 | raise ImportError("%s doesn't look like a module path" % dotted_path) from err 14 | 15 | module = import_module(module_path) 16 | 17 | try: 18 | return getattr(module, class_name) 19 | except AttributeError as err: 20 | raise ImportError( 21 | 'Module "{}" does not define a "{}" attribute/class'.format(module_path, class_name) 22 | ) from err 23 | -------------------------------------------------------------------------------- /saffier/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/saffier/contrib/__init__.py -------------------------------------------------------------------------------- /saffier/contrib/multi_tenancy/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import TenantModel 2 | from .registry import TenantRegistry 3 | from .settings import TenancySettings 4 | 5 | __all__ = ["TenantModel", "TenantRegistry", "TenancySettings"] 6 | -------------------------------------------------------------------------------- /saffier/contrib/multi_tenancy/base.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from saffier.contrib.multi_tenancy.metaclasses import BaseTenantMeta, TenantMeta 4 | from saffier.core.db.models import Model 5 | 6 | 7 | class TenantModel(Model, metaclass=BaseTenantMeta): 8 | """ 9 | Base for a multi tenant model from the Edgy contrib. 10 | This is **not mandatory** and can be used as a possible 11 | out of the box solution for multi tenancy. 12 | 13 | This design is not meant to be "the one" but instead an 14 | example of how to achieve the multi-tenancy in a simple fashion 15 | using Edgy and Edgy models. 16 | """ 17 | 18 | meta: ClassVar[TenantMeta] = TenantMeta(None) 19 | -------------------------------------------------------------------------------- /saffier/contrib/multi_tenancy/exceptions.py: -------------------------------------------------------------------------------- 1 | from saffier.exceptions import SaffierException 2 | 3 | 4 | class ModelSchemaError(SaffierException): ... 5 | -------------------------------------------------------------------------------- /saffier/contrib/multi_tenancy/registry.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from saffier.core.connection.database import Database 4 | from saffier.core.connection.registry import Registry 5 | 6 | 7 | class TenantRegistry(Registry): 8 | def __init__(self, database: Database, **kwargs: Any) -> None: 9 | super().__init__(database, **kwargs) 10 | self.tenant_models: Dict[str, Any] = {} 11 | -------------------------------------------------------------------------------- /saffier/contrib/multi_tenancy/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass 3 | from typing import Any, Optional 4 | 5 | from saffier.conf.global_settings import SaffierSettings 6 | 7 | 8 | @dataclass 9 | class TenancySettings(SaffierSettings): 10 | """ 11 | BaseSettings used for the contrib of Saffier tenancy 12 | """ 13 | 14 | auto_create_schema: bool = True 15 | auto_drop_schema: bool = False 16 | tenant_schema_default: str = "public" 17 | tenant_model: Optional[str] = None 18 | domain: Any = os.getenv("DOMAIN") 19 | domain_name: str = "localhost" 20 | auth_user_model: Optional[str] = None 21 | -------------------------------------------------------------------------------- /saffier/contrib/multi_tenancy/utils.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Dict, Type 2 | 3 | import sqlalchemy 4 | from loguru import logger 5 | 6 | from saffier.core.terminal import Terminal 7 | 8 | terminal = Terminal() 9 | 10 | if TYPE_CHECKING: 11 | from saffier.contrib.multi_tenancy import TenantModel, TenantRegistry 12 | 13 | 14 | def table_schema(model_class: Type["TenantModel"], schema: str) -> sqlalchemy.Table: 15 | """ 16 | Making sure the tables on inheritance state, creates the new 17 | one properly. 18 | 19 | The use of context vars instead of using the lru_cache comes from 20 | a warning from `ruff` where lru can lead to memory leaks. 21 | """ 22 | return model_class.build(schema) 23 | 24 | 25 | async def create_tables( 26 | registry: "TenantRegistry", tenant_models: Dict[str, Type["TenantModel"]], schema: str 27 | ) -> None: 28 | """ 29 | Creates the table models for a specific schema just generated. 30 | 31 | Iterates through the tenant models and creates them in the schema. 32 | """ 33 | 34 | for name, model in tenant_models.items(): 35 | table = table_schema(model, schema) 36 | 37 | logger.info(f"Creating table '{name}' for schema: '{schema}'") 38 | try: 39 | async with registry.engine.begin() as connection: 40 | await connection.run_sync(table.create) 41 | await registry.engine.dispose() 42 | except Exception as e: 43 | logger.error(str(e)) 44 | ... 45 | -------------------------------------------------------------------------------- /saffier/contrib/sqlalchemy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/saffier/contrib/sqlalchemy/__init__.py -------------------------------------------------------------------------------- /saffier/contrib/sqlalchemy/protocols.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from typing import Any 3 | 4 | import sqlalchemy 5 | 6 | DIALECTS = {"postgres": "postgres"} 7 | 8 | 9 | class BaseFieldProtocol(sqlalchemy.TypeDecorator): 10 | """ 11 | When implementing a field representation from SQLAlchemy, the protocol will be enforced 12 | """ 13 | 14 | impl: Any 15 | cache_ok: bool 16 | 17 | @abstractmethod 18 | def load_dialect_impl(self, dialect: Any) -> Any: 19 | raise NotImplementedError("load_dialect_impl must be implemented") 20 | 21 | @abstractmethod 22 | def process_bind_param(self, value: Any, dialect: Any) -> Any: 23 | raise NotImplementedError("process_bind_param must be implemented") 24 | 25 | @abstractmethod 26 | def process_result_value(self, value: Any, dialect: Any) -> Any: 27 | """ 28 | Processes the value coming from the database in a column-row style. 29 | """ 30 | raise NotImplementedError("process_result_value must be implemented") 31 | -------------------------------------------------------------------------------- /saffier/contrib/sqlalchemy/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | class SubList(list): 5 | def __init__(self, delimiter: str, *args: Any) -> None: 6 | self.delimiter = delimiter 7 | super().__init__(*args) 8 | 9 | def __str__(self) -> str: 10 | return self.delimiter.join(self) 11 | -------------------------------------------------------------------------------- /saffier/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/saffier/core/__init__.py -------------------------------------------------------------------------------- /saffier/core/connection/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/saffier/core/connection/__init__.py -------------------------------------------------------------------------------- /saffier/core/connection/database.py: -------------------------------------------------------------------------------- 1 | from databasez import Database, DatabaseURL 2 | 3 | __all__ = ["Database", "DatabaseURL"] 4 | -------------------------------------------------------------------------------- /saffier/core/datastructures.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from pydantic import BaseModel, ConfigDict 4 | 5 | object_setattr = object.__setattr__ 6 | 7 | 8 | class HashableBaseModel(BaseModel): 9 | """ 10 | Pydantic BaseModel by default doesn't handle with hashable types the same way 11 | a python object would and therefore there are types that are mutable (list, set) 12 | not hashable and those need to be handled properly. 13 | 14 | HashableBaseModel handles those corner cases. 15 | """ 16 | 17 | __slots__ = ["__weakref__"] 18 | 19 | def __hash__(self) -> Any: 20 | values: Any = {} 21 | for key, value in self.__dict__.items(): 22 | values[key] = None 23 | if isinstance(value, (list, set)): 24 | values[key] = tuple(value) 25 | else: 26 | values[key] = value 27 | return hash((type(self),) + tuple(values)) 28 | 29 | 30 | class ArbitraryHashableBaseModel(HashableBaseModel): 31 | """ 32 | Same as HashableBaseModel but allowing arbitrary values 33 | """ 34 | 35 | model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True) 36 | -------------------------------------------------------------------------------- /saffier/core/db/__init__.py: -------------------------------------------------------------------------------- 1 | from .context_vars import set_tenant as set_tenant 2 | -------------------------------------------------------------------------------- /saffier/core/db/constants.py: -------------------------------------------------------------------------------- 1 | CASCADE = "CASCADE" 2 | RESTRICT = "RESTRICT" 3 | SET_NULL = "SET NULL" 4 | -------------------------------------------------------------------------------- /saffier/core/db/fields/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import ( 2 | BigIntegerField, 3 | BooleanField, 4 | CharField, 5 | ChoiceField, 6 | DateField, 7 | DateTimeField, 8 | DecimalField, 9 | EmailField, 10 | Field, 11 | FloatField, 12 | ForeignKey, 13 | IntegerField, 14 | IPAddressField, 15 | JSONField, 16 | ManyToMany, 17 | ManyToManyField, 18 | OneToOne, 19 | OneToOneField, 20 | PasswordField, 21 | TextField, 22 | TimeField, 23 | URLField, 24 | UUIDField, 25 | ) 26 | 27 | __all__ = [ 28 | "BigIntegerField", 29 | "BooleanField", 30 | "CharField", 31 | "ChoiceField", 32 | "DateField", 33 | "DateTimeField", 34 | "DecimalField", 35 | "EmailField", 36 | "Field", 37 | "FloatField", 38 | "ForeignKey", 39 | "IntegerField", 40 | "IPAddressField", 41 | "JSONField", 42 | "ManyToMany", 43 | "ManyToManyField", 44 | "OneToOne", 45 | "OneToOneField", 46 | "PasswordField", 47 | "TextField", 48 | "TimeField", 49 | "URLField", 50 | "UUIDField", 51 | ] 52 | -------------------------------------------------------------------------------- /saffier/core/db/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import Model, ReflectModel 2 | 3 | __all__ = ["Model", "ReflectModel"] 4 | -------------------------------------------------------------------------------- /saffier/core/db/models/mixins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/saffier/core/db/models/mixins/__init__.py -------------------------------------------------------------------------------- /saffier/core/db/models/mixins/generics.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import typing 3 | 4 | from sqlalchemy.orm import Mapped, relationship 5 | 6 | 7 | class DeclarativeMixin: 8 | """ 9 | Exposes all the declarative operations 10 | for a given Saffier model object. 11 | """ 12 | 13 | @classmethod 14 | def declarative(cls) -> typing.Any: 15 | return cls.generate_model_declarative() 16 | 17 | @classmethod 18 | def generate_model_declarative(cls) -> typing.Any: 19 | """ 20 | Transforms a core Saffier table into a Declarative model table. 21 | """ 22 | Base = cls.meta.registry.declarative_base 23 | 24 | # Build the original table 25 | fields = {"__table__": cls.table} 26 | 27 | # Generate base 28 | model_table = type(cls.__name__, (Base,), fields) 29 | 30 | # Make sure if there are foreignkeys, builds the relationships 31 | for column in cls.table.columns: 32 | if not column.foreign_keys: 33 | continue 34 | 35 | # Maps the relationships with the foreign keys and related names 36 | field = cls.fields.get(column.name) 37 | to = field.to.__name__ if inspect.isclass(field.to) else field.to 38 | mapped_model: Mapped[to] = relationship(to) # type: ignore 39 | 40 | # Adds to the current model 41 | model_table.__mapper__.add_property(f"{column.name}_relation", mapped_model) 42 | 43 | return model_table 44 | -------------------------------------------------------------------------------- /saffier/core/db/models/utils.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Type, cast 2 | 3 | from saffier.core.connection.registry import Registry 4 | 5 | if TYPE_CHECKING: 6 | from saffier.core.db.models import Model 7 | 8 | 9 | def get_model(registry: Registry, model_name: str) -> Type["Model"]: 10 | """ 11 | Return the model with capitalize model_name. 12 | 13 | Raise lookup error if no model is found. 14 | """ 15 | try: 16 | return cast("Type[Model]", registry.models[model_name]) 17 | except KeyError: 18 | raise LookupError(f"Registry doesn't have a {model_name} model.") from None 19 | -------------------------------------------------------------------------------- /saffier/core/db/querysets/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import QuerySet 2 | from .clauses import and_, not_, or_ 3 | from .prefetch import Prefetch 4 | 5 | __all__ = ["QuerySet", "Prefetch"] 6 | __all__ = ["QuerySet", "Prefetch", "and_", "not_", "or_"] 7 | -------------------------------------------------------------------------------- /saffier/core/db/querysets/clauses.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import sqlalchemy 4 | 5 | 6 | def or_(*args: Any, **kwargs: Any) -> Any: 7 | """ 8 | Creates a SQL Alchemy OR clause for the expressions being passed. 9 | """ 10 | return sqlalchemy.or_(*args, **kwargs) 11 | 12 | 13 | def and_(*args: Any, **kwargs: Any) -> Any: 14 | """ 15 | Creates a SQL Alchemy AND clause for the expressions being passed. 16 | """ 17 | return sqlalchemy.and_(*args, **kwargs) 18 | 19 | 20 | def not_(*args: Any, **kwargs: Any) -> Any: 21 | """ 22 | Creates a SQL Alchemy NOT clause for the expressions being passed. 23 | """ 24 | return sqlalchemy.not_(*args, **kwargs) 25 | -------------------------------------------------------------------------------- /saffier/core/db/querysets/protocols.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | if typing.TYPE_CHECKING: 4 | from saffier.core.db.models.model import Model 5 | 6 | # Create a var type for the Saffier Model 7 | SaffierModel = typing.TypeVar("SaffierModel", bound="Model") 8 | 9 | 10 | class AwaitableQuery(typing.Generic[SaffierModel]): 11 | __slots__ = ("model_class",) 12 | 13 | def __init__(self, model_class: typing.Type[SaffierModel]) -> None: 14 | self.model_class: typing.Type[SaffierModel] = model_class 15 | 16 | async def execute(self) -> typing.Any: 17 | raise NotImplementedError() 18 | -------------------------------------------------------------------------------- /saffier/core/db/relationships/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/saffier/core/db/relationships/__init__.py -------------------------------------------------------------------------------- /saffier/core/extras/__init__.py: -------------------------------------------------------------------------------- 1 | from .extra import SaffierExtra 2 | 3 | __all__ = ["SaffierExtra"] 4 | -------------------------------------------------------------------------------- /saffier/core/extras/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any 3 | 4 | 5 | class BaseExtra(ABC): 6 | @abstractmethod 7 | def set_saffier_extension(self, app: Any) -> None: 8 | raise NotImplementedError( 9 | "Any class implementing the extra must implement set_saffier_extension() ." 10 | ) 11 | -------------------------------------------------------------------------------- /saffier/core/extras/extra.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import TYPE_CHECKING, Any 3 | 4 | from saffier.cli.constants import SAFFIER_DB, SAFFIER_EXTRA 5 | from saffier.core.extras.base import BaseExtra 6 | from saffier.core.terminal import Print, Terminal 7 | 8 | if TYPE_CHECKING: 9 | from saffier.core.connection.registry import Registry 10 | 11 | object_setattr = object.__setattr__ 12 | terminal = Terminal() 13 | printer = Print() 14 | 15 | 16 | @dataclass 17 | class Config: 18 | app: Any 19 | registry: "Registry" 20 | 21 | 22 | class SaffierExtra(BaseExtra): 23 | def __init__(self, app: Any, registry: "Registry", **kwargs: Any) -> None: 24 | super().__init__(**kwargs) 25 | self.app = app 26 | self.registry = registry 27 | 28 | self.set_saffier_extension(self.app, self.registry) 29 | 30 | def set_saffier_extension(self, app: Any, registry: "Registry") -> None: 31 | """ 32 | Sets a saffier dictionary for the app object. 33 | """ 34 | if hasattr(app, SAFFIER_DB): 35 | printer.write_warning( 36 | "The application already has a Migrate related configuration with the needed information. SaffierExtra will be ignored and it can be removed." 37 | ) 38 | return 39 | 40 | config = Config(app=app, registry=registry) 41 | object_setattr(app, SAFFIER_EXTRA, {}) 42 | app._saffier_extra["extra"] = config 43 | -------------------------------------------------------------------------------- /saffier/core/signals/__init__.py: -------------------------------------------------------------------------------- 1 | from .handlers import post_delete, post_save, post_update, pre_delete, pre_save, pre_update 2 | from .signal import Broadcaster, Signal 3 | 4 | __all__ = [ 5 | "Broadcaster", 6 | "Signal", 7 | "post_delete", 8 | "post_save", 9 | "post_update", 10 | "pre_delete", 11 | "pre_save", 12 | "pre_update", 13 | ] 14 | -------------------------------------------------------------------------------- /saffier/core/sync.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from typing import Any 3 | 4 | import anyio 5 | from anyio._core._eventloop import threadlocals 6 | 7 | 8 | def execsync(async_function: Any, raise_error: bool = True) -> Any: 9 | """ 10 | Runs any async function inside a blocking function (sync). 11 | """ 12 | 13 | @functools.wraps(async_function) 14 | def wrapper(*args: Any, **kwargs: Any) -> Any: 15 | current_async_module = getattr(threadlocals, "current_async_module", None) 16 | partial_func = functools.partial(async_function, *args, **kwargs) 17 | if current_async_module is not None and raise_error is True: 18 | return anyio.from_thread.run(partial_func) 19 | return anyio.run(partial_func) 20 | 21 | return wrapper 22 | -------------------------------------------------------------------------------- /saffier/core/terminal/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import OutputColour 2 | from .print import Print 3 | from .terminal import Terminal 4 | 5 | __all__ = ["OutputColour", "Print", "Terminal"] 6 | -------------------------------------------------------------------------------- /saffier/core/terminal/print.py: -------------------------------------------------------------------------------- 1 | from saffier.core.terminal.base import Base, OutputColour 2 | 3 | 4 | class Print(Base): 5 | """Base output class for the terminal""" 6 | 7 | def write_success( 8 | self, 9 | message: str, 10 | colour: str = OutputColour.SUCCESS, 11 | ) -> None: 12 | """Outputs the successes to the console""" 13 | message = self.message(message, colour) 14 | self.print(message) 15 | 16 | def write_info(self, message: str, colour: str = OutputColour.INFO) -> None: 17 | """Outputs the info to the console""" 18 | message = self.message(message, colour) 19 | self.print(message) 20 | 21 | def write_warning( 22 | self, 23 | message: str, 24 | colour: str = OutputColour.WARNING, 25 | ) -> None: 26 | """Outputs the warnings to the console""" 27 | message = self.message(message, colour) 28 | self.print(message) 29 | 30 | def write_plain(self, message: str, colour: str = OutputColour.WHITE) -> None: 31 | message = self.message(message, colour) 32 | self.print(message) 33 | 34 | def write_error( 35 | self, 36 | message: str, 37 | colour: str = OutputColour.ERROR, 38 | ) -> None: 39 | """Outputs the errors to the console""" 40 | message = self.message(message, colour) 41 | self.print(message) 42 | -------------------------------------------------------------------------------- /saffier/core/terminal/terminal.py: -------------------------------------------------------------------------------- 1 | from saffier.core.terminal.base import Base, OutputColour 2 | 3 | 4 | class Terminal(Base): 5 | """Base output class for the terminal""" 6 | 7 | def write_success( 8 | self, 9 | message: str, 10 | colour: str = OutputColour.SUCCESS, 11 | ) -> str: 12 | """Outputs the successes to the console""" 13 | message = self.message(message, colour) 14 | return message 15 | 16 | def write_info( 17 | self, 18 | message: str, 19 | colour: str = OutputColour.INFO, 20 | ) -> str: 21 | """Outputs the info to the console""" 22 | message = self.message(message, colour) 23 | return message 24 | 25 | def write_warning( 26 | self, 27 | message: str, 28 | colour: str = OutputColour.WARNING, 29 | ) -> str: 30 | """Outputs the warnings to the console""" 31 | message = self.message(message, colour) 32 | return message 33 | 34 | def write_error( 35 | self, 36 | message: str, 37 | colour: str = OutputColour.ERROR, 38 | ) -> str: 39 | """Outputs the errors to the console""" 40 | message = self.message(message, colour) 41 | return message 42 | 43 | def write_plain(self, message: str, colour: str = OutputColour.WHITE) -> str: 44 | message = self.message(message, colour) 45 | return message 46 | -------------------------------------------------------------------------------- /saffier/core/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/saffier/core/utils/__init__.py -------------------------------------------------------------------------------- /saffier/core/utils/sync.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from concurrent import futures 3 | from concurrent.futures import Future 4 | from typing import Any, Awaitable 5 | 6 | import nest_asyncio 7 | 8 | nest_asyncio.apply() 9 | 10 | 11 | def run_sync(async_function: Awaitable) -> Any: 12 | """ 13 | Runs the queries in sync mode 14 | """ 15 | try: 16 | return asyncio.run(async_function) 17 | except RuntimeError: 18 | with futures.ThreadPoolExecutor(max_workers=1) as executor: 19 | future: Future = executor.submit(asyncio.run, async_function) 20 | return future.result() 21 | -------------------------------------------------------------------------------- /saffier/core/utils/unique.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from saffier.types import Empty 4 | 5 | 6 | class Uniqueness: 7 | TRUE = Empty() 8 | FALSE = Empty() 9 | HASHABLE_TYPES = (int, bool, str, float, list, dict) 10 | 11 | def __init__(self, items: typing.Optional[typing.List[typing.Any]] = None) -> None: 12 | self._set: set = set() 13 | for item in items or []: 14 | self.add(item) 15 | 16 | def __contains__(self, item: typing.Any) -> bool: 17 | item = self.make_hashable(item) 18 | return item in self._set 19 | 20 | def add(self, item: typing.Any) -> None: 21 | item = self.make_hashable(item) 22 | self._set.add(item) 23 | 24 | def make_hashable(self, element: typing.Any) -> typing.Any: 25 | """ 26 | Coerce a primitive into a uniquely hashable type, for uniqueness checks. 27 | """ 28 | assert (element is None) or isinstance(element, (int, bool, str, float, list, dict)) 29 | 30 | if element is True: 31 | return self.TRUE 32 | elif element is False: 33 | return self.FALSE 34 | elif isinstance(element, list): 35 | return ("list", tuple(self.make_hashable(item) for item in element)) 36 | elif isinstance(element, dict): 37 | return ( 38 | "dict", 39 | tuple( 40 | (self.make_hashable(key), self.make_hashable(value)) 41 | for key, value in element.items() 42 | ), 43 | ) 44 | return element 45 | -------------------------------------------------------------------------------- /saffier/exceptions.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from saffier.core.utils.base import BaseError 4 | 5 | 6 | class SaffierException(Exception): 7 | def __init__( 8 | self, 9 | *args: typing.Any, 10 | detail: str = "", 11 | ): 12 | self.detail = detail 13 | super().__init__(*(str(arg) for arg in args if arg), self.detail) 14 | 15 | def __repr__(self) -> str: 16 | if self.detail: 17 | return f"{self.__class__.__name__} - {self.detail}" 18 | return self.__class__.__name__ 19 | 20 | def __str__(self) -> str: 21 | return "".join(self.args).strip() 22 | 23 | 24 | class ObjectNotFound(SaffierException): ... 25 | 26 | 27 | DoesNotFound = ObjectNotFound 28 | 29 | 30 | class MultipleObjectsReturned(SaffierException): ... 31 | 32 | 33 | class ValidationError(BaseError): ... 34 | 35 | 36 | class ImproperlyConfigured(SaffierException): ... 37 | 38 | 39 | class ForeignKeyBadConfigured(SaffierException): ... 40 | 41 | 42 | class RelationshipIncompatible(SaffierException): ... 43 | 44 | 45 | class DuplicateRecordError(SaffierException): ... 46 | 47 | 48 | class RelationshipNotFound(SaffierException): ... 49 | 50 | 51 | class QuerySetError(SaffierException): ... 52 | 53 | 54 | class ModelReferenceError(SaffierException): ... 55 | 56 | 57 | class SchemaError(SaffierException): ... 58 | 59 | 60 | class SignalError(SaffierException): ... 61 | 62 | 63 | class CommandEnvironmentError(SaffierException): ... 64 | -------------------------------------------------------------------------------- /saffier/protocols/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/saffier/protocols/__init__.py -------------------------------------------------------------------------------- /saffier/protocols/many_relationship.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, runtime_checkable 2 | 3 | try: 4 | from typing import Protocol 5 | except ImportError: # pragma: nocover 6 | from typing_extensions import Protocol # type: ignore 7 | 8 | 9 | if TYPE_CHECKING: # pragma: nocover 10 | from saffier import Model 11 | 12 | 13 | @runtime_checkable 14 | class ManyRelationProtocol(Protocol): 15 | """Defines the what needs to be implemented when using the ManyRelationProtocol""" 16 | 17 | async def add(self, child: "Model") -> None: ... 18 | 19 | async def remove(self, child: "Model") -> None: ... 20 | -------------------------------------------------------------------------------- /saffier/py.typed: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /saffier/types.py: -------------------------------------------------------------------------------- 1 | class Empty: 2 | """ 3 | A placeholder class object. 4 | """ 5 | 6 | ... 7 | -------------------------------------------------------------------------------- /saffier/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/saffier/utils/__init__.py -------------------------------------------------------------------------------- /saffier/utils/compat.py: -------------------------------------------------------------------------------- 1 | from inspect import isclass 2 | from typing import Any 3 | 4 | from typing_extensions import get_origin 5 | 6 | 7 | def is_class_and_subclass(value: Any, _type: Any) -> bool: 8 | """ 9 | Checks if a `value` is of type class and subclass. 10 | by checking the origin of the value against the type being 11 | verified. 12 | """ 13 | original = get_origin(value) 14 | if not original and not isclass(value): 15 | return False 16 | 17 | try: 18 | if original: 19 | return original and issubclass(original, _type) 20 | return issubclass(value, _type) 21 | except TypeError: 22 | return False 23 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | if [ "$VIRTUAL_ENV" != '' ]; then 4 | export PREFIX="$VIRTUAL_ENV/bin/" 5 | elif [ -d 'venv' ] ; then 6 | export PREFIX="venv/bin/" 7 | else 8 | PREFIX="" 9 | fi 10 | 11 | set -x 12 | 13 | ${PREFIX}python -m build 14 | ${PREFIX}twine check dist/* 15 | -------------------------------------------------------------------------------- /scripts/check: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | export PREFIX="" 4 | if [ "$VIRTUAL_ENV" != '' ]; then 5 | export PREFIX="$VIRTUAL_ENV/bin/" 6 | elif [ -d 'venv' ] ; then 7 | export PREFIX="venv/bin/" 8 | fi 9 | export SOURCE_FILES="saffier tests" 10 | export EXCLUDE=__init__.py 11 | export MAIN="saffier" 12 | 13 | ${PREFIX}mypy $MAIN 14 | ${PREFIX}ruff check $SOURCE_FILES --line-length 99 15 | ${PREFIX}black $SOURCE_FILES --check --diff --check --line-length 99 16 | -------------------------------------------------------------------------------- /scripts/clean: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | if [ -d 'dist' ] ; then 4 | rm -r dist 5 | fi 6 | if [ -d 'site' ] ; then 7 | rm -r site 8 | fi 9 | if [ -d 'htmlcov' ] ; then 10 | rm -r htmlcov 11 | fi 12 | if [ -d 'saffier.egg-info' ] ; then 13 | rm -r saffier.egg-info 14 | fi 15 | if [ -d '.hypothesis' ] ; then 16 | rm -r .hypothesis 17 | fi 18 | if [ -d '.mypy_cache' ] ; then 19 | rm -r .mypy_cache 20 | fi 21 | if [ -d '.pytest_cache' ] ; then 22 | rm -r .pytest_cache 23 | fi 24 | if [ -d '.ruff_cache' ] ; then 25 | rm -r .ruff_cache 26 | fi 27 | -------------------------------------------------------------------------------- /scripts/coverage: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | export PREFIX="" 4 | if [ "$VIRTUAL_ENV" != '' ]; then 5 | export PREFIX="$VIRTUAL_ENV/bin/" 6 | elif [ -d 'venv' ] ; then 7 | export PREFIX="venv/bin/" 8 | fi 9 | set -x 10 | 11 | ${PREFIX}coverage report --show-missing --skip-covered --fail-under=100 12 | -------------------------------------------------------------------------------- /scripts/docs: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | export PREFIX="" 4 | 5 | if [ "$VIRTUAL_ENV" != '' ]; then 6 | export PREFIX="$VIRTUAL_ENV/bin/" 7 | elif [ -d 'venv' ] ; then 8 | export PREFIX="venv/bin/" 9 | fi 10 | 11 | set -x 12 | 13 | ${PREFIX}mkdocs serve 14 | -------------------------------------------------------------------------------- /scripts/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Use the Python executable provided from the `-p` option, or a default. 4 | [ "$1" = "-p" ] && PYTHON=$2 || PYTHON="python3" 5 | 6 | VENV="venv" 7 | 8 | set -x 9 | 10 | if [ "$VIRTUAL_ENV" != '' ]; then 11 | PIP="$VIRTUAL_ENV/bin/pip" 12 | elif [ -z "$GITHUB_ACTIONS" ]; then 13 | "$PYTHON" -m venv "$VENV" 14 | PIP="$VENV/bin/pip" 15 | else 16 | PIP="pip" 17 | fi 18 | 19 | "$PIP" install -U pip 20 | "$PIP" install -e .[all,dev,test,doc,postgres,mysql,sqlite,testing] 21 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | export PREFIX="" 4 | if [ "$VIRTUAL_ENV" != '' ]; then 5 | export PREFIX="$VIRTUAL_ENV/bin/" 6 | elif [ -d 'venv' ] ; then 7 | export PREFIX="venv/bin/" 8 | fi 9 | export SOURCE_FILES="saffier tests docs_src" 10 | export EXCLUDE=__init__.py 11 | 12 | set -x 13 | 14 | ${PREFIX}mypy saffier 15 | ${PREFIX}ruff check $SOURCE_FILES --fix --line-length 99 16 | ${PREFIX}black $SOURCE_FILES --line-length 99 17 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | VERSION_FILE="saffier/__init__.py" 4 | 5 | if [ "$VIRTUAL_ENV" != '' ]; then 6 | export PREFIX="$VIRTUAL_ENV/bin/" 7 | elif [ -d 'venv' ] ; then 8 | PREFIX="venv/bin/" 9 | else 10 | PREFIX="" 11 | fi 12 | 13 | if [ ! -z "$GITHUB_ACTIONS" ]; then 14 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 15 | git config --local user.name "GitHub Action" 16 | 17 | VERSION=`grep __version__ ${VERSION_FILE} | grep -o '[0-9][^"]*'` 18 | 19 | if [ "refs/tags/${VERSION}" != "${GITHUB_REF}" ] ; then 20 | echo "GitHub Ref '${GITHUB_REF}' did not match package version '${VERSION}'" 21 | exit 1 22 | fi 23 | fi 24 | 25 | set -x 26 | 27 | ${PREFIX}twine upload dist/* 28 | -------------------------------------------------------------------------------- /scripts/sync-version: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | SEMVER_REGEX="([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?" 4 | CHANGELOG_VERSION=$(grep -o -E $SEMVER_REGEX docs/release-notes.md | head -1) 5 | VERSION=$(grep -o -E $SEMVER_REGEX saffier/__init__.py | head -1) 6 | if [ "$CHANGELOG_VERSION" != "$VERSION" ]; then 7 | echo "Version in changelog does not match version in saffier/__init__.py!" 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PREFIX="" 4 | if [ "$VIRTUAL_ENV" != '' ]; then 5 | export PREFIX="$VIRTUAL_ENV/bin/" 6 | elif [ -d 'venv' ] ; then 7 | export PREFIX="venv/bin/" 8 | fi 9 | export SAFFIER_TESTCLIENT_TEST_PREFIX="" 10 | 11 | set -ex 12 | 13 | # if [ -z $GITHUB_ACTIONS ]; then 14 | # scripts/check 15 | # fi 16 | 17 | ${PREFIX}coverage run -m pytest $@ 18 | -------------------------------------------------------------------------------- /tests/clauses/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/tests/clauses/__init__.py -------------------------------------------------------------------------------- /tests/cli/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/tests/cli/conftest.py -------------------------------------------------------------------------------- /tests/cli/custom/README: -------------------------------------------------------------------------------- 1 | Custom template 2 | -------------------------------------------------------------------------------- /tests/cli/custom/alembic.ini.mako: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,saffier 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_saffier] 38 | level = INFO 39 | handlers = 40 | qualname = saffier 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /tests/cli/custom/script.py.mako: -------------------------------------------------------------------------------- 1 | # Custom mako template 2 | """${message} 3 | 4 | Revision ID: ${up_revision} 5 | Revises: ${down_revision | comma,n} 6 | Create Date: ${create_date} 7 | 8 | """ 9 | from alembic import op 10 | import sqlalchemy as sa 11 | ${imports if imports else ""} 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = ${repr(up_revision)} 15 | down_revision = ${repr(down_revision)} 16 | branch_labels = ${repr(branch_labels)} 17 | depends_on = ${repr(depends_on)} 18 | 19 | 20 | def upgrade(): 21 | ${upgrades if upgrades else "pass"} 22 | 23 | 24 | def downgrade(): 25 | ${downgrades if downgrades else "pass"} 26 | -------------------------------------------------------------------------------- /tests/cli/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from esmerald import Esmerald 5 | 6 | import saffier 7 | from saffier import Migrate 8 | from saffier.testclient import DatabaseTestClient 9 | from tests.settings import DATABASE_URL 10 | 11 | pytestmark = pytest.mark.anyio 12 | database = DatabaseTestClient(DATABASE_URL, drop_database=True) 13 | models = saffier.Registry(database=database) 14 | 15 | basedir = os.path.abspath(os.path.dirname(__file__)) 16 | 17 | 18 | class AppUser(saffier.Model): 19 | name = saffier.CharField(max_length=255) 20 | 21 | class Meta: 22 | registry = models 23 | 24 | 25 | app = Esmerald(routes=[]) 26 | Migrate(app, registry=models) 27 | -------------------------------------------------------------------------------- /tests/cli/main_extra.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from esmerald import Esmerald 5 | 6 | import saffier 7 | from saffier import SaffierExtra 8 | from saffier.testclient import DatabaseTestClient 9 | from tests.settings import DATABASE_URL 10 | 11 | pytestmark = pytest.mark.anyio 12 | database = DatabaseTestClient(DATABASE_URL, drop_database=True) 13 | models = saffier.Registry(database=database) 14 | 15 | basedir = os.path.abspath(os.path.dirname(__file__)) 16 | 17 | 18 | class AppUser(saffier.Model): 19 | name = saffier.CharField(max_length=255) 20 | 21 | class Meta: 22 | registry = models 23 | 24 | 25 | app = Esmerald(routes=[]) 26 | SaffierExtra(app, registry=models) 27 | -------------------------------------------------------------------------------- /tests/cli/test_saffier_extra.py: -------------------------------------------------------------------------------- 1 | from esmerald import Esmerald 2 | 3 | from saffier import Registry 4 | from saffier.cli.constants import SAFFIER_DB, SAFFIER_EXTRA 5 | from tests.cli.main import app as main_app 6 | from tests.cli.main_extra import app as extra_app 7 | 8 | 9 | def test_has_saffier_extra(): 10 | assert hasattr(extra_app, SAFFIER_EXTRA) 11 | 12 | 13 | def test_extra_esmerald(): 14 | extra = getattr(extra_app, SAFFIER_EXTRA)["extra"] 15 | assert isinstance(extra.app, Esmerald) 16 | 17 | 18 | def test_has_saffier_migration(): 19 | assert hasattr(main_app, SAFFIER_DB) 20 | 21 | 22 | def test_migration_registry(): 23 | extra = getattr(main_app, SAFFIER_DB)["migrate"] 24 | assert isinstance(extra.registry, Registry) 25 | -------------------------------------------------------------------------------- /tests/cli/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | 5 | def run_cmd(app, cmd, is_app=True): 6 | env = dict(os.environ) 7 | if is_app: 8 | env["SAFFIER_DEFAULT_APP"] = app 9 | # CI uses something different as workdir and we aren't hatch test yet. 10 | if "VIRTUAL_ENV" not in env: 11 | basedir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 12 | if os.path.isdir(f"{basedir}/venv/bin/"): 13 | cmd = f"{basedir}/venv/bin/{cmd}" 14 | result = subprocess.run(cmd, capture_output=True, env=env, shell=True) 15 | print("\n$ " + cmd) 16 | print(result.stdout.decode("utf-8")) 17 | print(result.stderr.decode("utf-8")) 18 | return result.stdout, result.stderr, result.returncode 19 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | os.environ.setdefault("SAFFIER_SETTINGS_MODULE", "tests.settings.TestSettings") 6 | 7 | 8 | @pytest.fixture(scope="module") 9 | def anyio_backend(): 10 | return ("asyncio", {"debug": True}) 11 | -------------------------------------------------------------------------------- /tests/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/tests/contrib/__init__.py -------------------------------------------------------------------------------- /tests/foreign_keys/m2m_string/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/tests/foreign_keys/m2m_string/__init__.py -------------------------------------------------------------------------------- /tests/indexes/test_indexes_errors.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | import pytest 5 | 6 | import saffier 7 | from saffier.core.db.datastructures import Index 8 | from saffier.testclient import DatabaseTestClient 9 | from tests.settings import DATABASE_URL 10 | 11 | pytestmark = pytest.mark.anyio 12 | 13 | database = DatabaseTestClient(DATABASE_URL) 14 | models = saffier.Registry(database=database) 15 | 16 | 17 | def get_random_string(length): 18 | letters = string.ascii_lowercase 19 | result_str = "".join(random.choice(letters) for i in range(length)) 20 | return result_str 21 | 22 | 23 | def test_raises_value_error_on_wrong_max_length(): 24 | with pytest.raises(ValueError): 25 | 26 | class User(saffier.Model): 27 | name = saffier.CharField(max_length=255) 28 | title = saffier.CharField(max_length=255) 29 | 30 | class Meta: 31 | registry = models 32 | indexes = [Index(fields=["name", "title"], name=get_random_string(64))] 33 | 34 | 35 | def test_raises_value_error_on_wrong_type_passed_fields(): 36 | with pytest.raises(ValueError): 37 | 38 | class User(saffier.Model): 39 | name = saffier.CharField(max_length=255) 40 | title = saffier.CharField(max_length=255) 41 | 42 | class Meta: 43 | registry = models 44 | indexes = [Index(fields=2)] 45 | -------------------------------------------------------------------------------- /tests/managers/test_managers_abstract_multiple.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier import Manager 5 | from saffier.core.db.querysets.base import QuerySet 6 | from saffier.exceptions import ImproperlyConfigured 7 | from saffier.testclient import DatabaseTestClient as Database 8 | from tests.settings import DATABASE_URL 9 | 10 | database = Database(url=DATABASE_URL) 11 | models = saffier.Registry(database=database) 12 | 13 | pytestmark = pytest.mark.anyio 14 | 15 | 16 | class ObjectsManager(Manager): 17 | def get_queryset(self) -> QuerySet: 18 | queryset = super().get_queryset().filter(is_active=True) 19 | return queryset 20 | 21 | 22 | class LanguageManager(Manager): 23 | def get_queryset(self) -> QuerySet: 24 | queryset = super().get_queryset().filter(language="EN") 25 | return queryset 26 | 27 | 28 | async def test_inherited_abstract_base_model_managers_raises_error_on_multiple(): 29 | with pytest.raises(ImproperlyConfigured) as raised: 30 | 31 | class BaseModel(saffier.Model): 32 | query = ObjectsManager() 33 | languages = LanguageManager() 34 | 35 | class Meta: 36 | abstract = True 37 | registry = models 38 | 39 | assert raised.value.args[0] == "Multiple managers are not allowed in abstract classes." 40 | -------------------------------------------------------------------------------- /tests/metaclass/test_meta.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | pytestmark = pytest.mark.anyio 8 | 9 | database = Database(DATABASE_URL) 10 | models = saffier.Registry(database=database) 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | 17 | class Meta: 18 | registry = models 19 | 20 | 21 | @pytest.fixture(autouse=True, scope="module") 22 | async def create_test_database(): 23 | await models.create_all() 24 | yield 25 | await models.drop_all() 26 | 27 | 28 | @pytest.fixture(autouse=True) 29 | async def rollback_connections(): 30 | with database.force_rollback(): 31 | async with database: 32 | yield 33 | 34 | 35 | async def test_meta_tablename(): 36 | await User.query.create(name="Saffier") 37 | users = await User.query.all() 38 | 39 | assert len(users) == 1 40 | 41 | user = await User.query.get(name="Saffier") 42 | 43 | assert user.meta.tablename == "users" 44 | 45 | 46 | async def test_meta_registry(): 47 | await User.query.create(name="Saffier") 48 | users = await User.query.all() 49 | 50 | assert len(users) == 1 51 | 52 | user = await User.query.get(name="Saffier") 53 | 54 | assert user.meta.registry == models 55 | -------------------------------------------------------------------------------- /tests/models/run_sync/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/saffier/8b41bc990b4e9f8805a7610074b30ea68a01d752/tests/models/run_sync/__init__.py -------------------------------------------------------------------------------- /tests/models/run_sync/test_model_count.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_count(): 37 | saffier.run_sync(User.query.create(name="Test")) 38 | saffier.run_sync(User.query.create(name="Jane")) 39 | saffier.run_sync(User.query.create(name="Lucy")) 40 | 41 | assert saffier.run_sync(User.query.count()) == 3 42 | assert saffier.run_sync(User.query.filter(name__icontains="T").count()) == 1 43 | -------------------------------------------------------------------------------- /tests/models/run_sync/test_model_exists.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_exists(): 37 | saffier.run_sync(User.query.create(name="Test")) 38 | assert saffier.run_sync(User.query.filter(name="Test").exists()) is True 39 | assert saffier.run_sync(User.query.filter(name="Jane").exists()) is False 40 | -------------------------------------------------------------------------------- /tests/models/run_sync/test_model_first.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_first(): 37 | Test = saffier.run_sync(User.query.create(name="Test")) 38 | jane = saffier.run_sync(User.query.create(name="Jane")) 39 | 40 | assert saffier.run_sync(User.query.first()) == Test 41 | assert saffier.run_sync(User.query.first(name="Jane")) == jane 42 | assert saffier.run_sync(User.query.filter(name="Jane").first()) == jane 43 | assert saffier.run_sync(User.query.filter(name="Lucy").first()) is None 44 | -------------------------------------------------------------------------------- /tests/models/run_sync/test_model_get_or_create.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_get_or_create(): 37 | user, created = saffier.run_sync( 38 | User.query.get_or_create(name="Test", defaults={"language": "Portuguese"}) 39 | ) 40 | assert created is True 41 | assert user.name == "Test" 42 | assert user.language == "Portuguese" 43 | 44 | user, created = saffier.run_sync( 45 | User.query.get_or_create(name="Test", defaults={"language": "English"}) 46 | ) 47 | assert created is False 48 | assert user.name == "Test" 49 | assert user.language == "Portuguese" 50 | -------------------------------------------------------------------------------- /tests/models/run_sync/test_model_last.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_last(): 37 | Test = saffier.run_sync(User.query.create(name="Test")) 38 | jane = saffier.run_sync(User.query.create(name="Jane")) 39 | 40 | assert saffier.run_sync(User.query.last()) == jane 41 | assert saffier.run_sync(User.query.last(name="Jane")) == jane 42 | assert saffier.run_sync(User.query.filter(name="Test").last()) == Test 43 | assert saffier.run_sync(User.query.filter(name="Lucy").last()) is None 44 | -------------------------------------------------------------------------------- /tests/models/run_sync/test_model_offset.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_offset(): 37 | saffier.run_sync(User.query.create(name="Test")) 38 | saffier.run_sync(User.query.create(name="Jane")) 39 | 40 | users = saffier.run_sync(User.query.offset(1).limit(1).all()) 41 | assert users[0].name == "Jane" 42 | -------------------------------------------------------------------------------- /tests/models/run_sync/test_model_queryset_delete.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class Product(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | rating = saffier.IntegerField(minimum=1, maximum=5) 17 | in_stock = saffier.BooleanField(default=False) 18 | 19 | class Meta: 20 | registry = models 21 | name = "products" 22 | 23 | 24 | @pytest.fixture(autouse=True, scope="function") 25 | async def create_test_database(): 26 | await models.create_all() 27 | yield 28 | await models.drop_all() 29 | 30 | 31 | @pytest.fixture(autouse=True) 32 | async def rollback_connections(): 33 | with database.force_rollback(): 34 | async with database: 35 | yield 36 | 37 | 38 | async def test_queryset_delete(): 39 | shirt = saffier.run_sync(Product.query.create(name="Shirt", rating=5)) 40 | saffier.run_sync(Product.query.create(name="Belt", rating=5)) 41 | saffier.run_sync(Product.query.create(name="Tie", rating=5)) 42 | 43 | saffier.run_sync(Product.query.filter(pk=shirt.id).delete()) 44 | assert saffier.run_sync(Product.query.count()) == 2 45 | 46 | saffier.run_sync(Product.query.delete()) 47 | assert saffier.run_sync(Product.query.count()) == 0 48 | -------------------------------------------------------------------------------- /tests/models/run_sync/test_model_sync.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier import run_sync 5 | from saffier.testclient import DatabaseTestClient as Database 6 | from tests.settings import DATABASE_URL 7 | 8 | database = Database(url=DATABASE_URL) 9 | models = saffier.Registry(database=database) 10 | 11 | pytestmark = pytest.mark.anyio 12 | 13 | 14 | class User(saffier.Model): 15 | id = saffier.IntegerField(primary_key=True) 16 | name = saffier.CharField(max_length=100) 17 | language = saffier.CharField(max_length=200, null=True) 18 | 19 | class Meta: 20 | registry = models 21 | 22 | 23 | @pytest.fixture(autouse=True, scope="function") 24 | async def create_test_database(): 25 | await models.create_all() 26 | yield 27 | await models.drop_all() 28 | 29 | 30 | @pytest.fixture(autouse=True) 31 | async def rollback_connections(): 32 | with database.force_rollback(): 33 | async with database: 34 | yield 35 | 36 | 37 | async def test_model_first(): 38 | run_sync(User.query.create(name="Test")) 39 | run_sync(User.query.create(name="Jane")) 40 | 41 | users = run_sync(User.query.all()) 42 | 43 | assert len(users) == 2 44 | 45 | users = run_sync(User.query.all()) 46 | 47 | assert len(users) == 2 48 | -------------------------------------------------------------------------------- /tests/models/run_sync/test_model_update_or_create.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_update_or_create(): 37 | user, created = saffier.run_sync( 38 | User.query.update_or_create(name="Test", language="English", defaults={"name": "Jane"}) 39 | ) 40 | assert created is True 41 | assert user.name == "Jane" 42 | assert user.language == "English" 43 | 44 | user, created = saffier.run_sync( 45 | User.query.update_or_create(name="Jane", language="English", defaults={"name": "Test"}) 46 | ) 47 | assert created is False 48 | assert user.name == "Test" 49 | assert user.language == "English" 50 | -------------------------------------------------------------------------------- /tests/models/test_model_count.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_count(): 37 | await User.query.create(name="Test") 38 | await User.query.create(name="Jane") 39 | await User.query.create(name="Lucy") 40 | 41 | assert await User.query.count() == 3 42 | assert await User.query.filter(name__icontains="T").count() == 1 43 | -------------------------------------------------------------------------------- /tests/models/test_model_exists.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_exists(): 37 | await User.query.create(name="Test") 38 | assert await User.query.filter(name="Test").exists() is True 39 | assert await User.query.filter(name="Jane").exists() is False 40 | -------------------------------------------------------------------------------- /tests/models/test_model_first.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_first(): 37 | Test = await User.query.create(name="Test") 38 | jane = await User.query.create(name="Jane") 39 | 40 | assert await User.query.first() == Test 41 | assert await User.query.first(name="Jane") == jane 42 | assert await User.query.filter(name="Jane").first() == jane 43 | assert await User.query.filter(name="Lucy").first() is None 44 | -------------------------------------------------------------------------------- /tests/models/test_model_get_or_create.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_get_or_create(): 37 | user, created = await User.query.get_or_create( 38 | name="Test", defaults={"language": "Portuguese"} 39 | ) 40 | assert created is True 41 | assert user.name == "Test" 42 | assert user.language == "Portuguese" 43 | 44 | user, created = await User.query.get_or_create(name="Test", defaults={"language": "English"}) 45 | assert created is False 46 | assert user.name == "Test" 47 | assert user.language == "Portuguese" 48 | -------------------------------------------------------------------------------- /tests/models/test_model_last.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_last(): 37 | Test = await User.query.create(name="Test") 38 | jane = await User.query.create(name="Jane") 39 | 40 | assert await User.query.last() == jane 41 | assert await User.query.last(name="Jane") == jane 42 | assert await User.query.filter(name="Test").last() == Test 43 | assert await User.query.filter(name="Lucy").last() is None 44 | -------------------------------------------------------------------------------- /tests/models/test_model_limit.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_limit(): 37 | await User.query.create(name="Test") 38 | await User.query.create(name="Jane") 39 | await User.query.create(name="Lucy") 40 | 41 | assert len(await User.query.limit(2).all()) == 2 42 | 43 | 44 | async def test_model_limit_with_filter(): 45 | await User.query.create(name="Test") 46 | await User.query.create(name="Test") 47 | await User.query.create(name="Test") 48 | 49 | assert len(await User.query.limit(2).filter(name__iexact="Test").all()) == 2 50 | -------------------------------------------------------------------------------- /tests/models/test_model_offset.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_offset(): 37 | await User.query.create(name="Test") 38 | await User.query.create(name="Jane") 39 | 40 | users = await User.query.offset(1).limit(1).all() 41 | assert users[0].name == "Jane" 42 | -------------------------------------------------------------------------------- /tests/models/test_model_queryset_delete.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class Product(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | rating = saffier.IntegerField(minimum=1, maximum=5) 17 | in_stock = saffier.BooleanField(default=False) 18 | 19 | class Meta: 20 | registry = models 21 | name = "products" 22 | 23 | 24 | @pytest.fixture(autouse=True, scope="function") 25 | async def create_test_database(): 26 | await models.create_all() 27 | yield 28 | await models.drop_all() 29 | 30 | 31 | @pytest.fixture(autouse=True) 32 | async def rollback_connections(): 33 | with database.force_rollback(): 34 | async with database: 35 | yield 36 | 37 | 38 | async def test_queryset_delete(): 39 | shirt = await Product.query.create(name="Shirt", rating=5) 40 | await Product.query.create(name="Belt", rating=5) 41 | await Product.query.create(name="Tie", rating=5) 42 | 43 | await Product.query.filter(pk=shirt.id).delete() 44 | assert await Product.query.count() == 2 45 | 46 | await Product.query.delete() 47 | assert await Product.query.count() == 0 48 | -------------------------------------------------------------------------------- /tests/models/test_model_update_or_create.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | models = saffier.Registry(database=database) 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | class User(saffier.Model): 14 | id = saffier.IntegerField(primary_key=True) 15 | name = saffier.CharField(max_length=100) 16 | language = saffier.CharField(max_length=200, null=True) 17 | 18 | class Meta: 19 | registry = models 20 | 21 | 22 | @pytest.fixture(autouse=True, scope="function") 23 | async def create_test_database(): 24 | await models.create_all() 25 | yield 26 | await models.drop_all() 27 | 28 | 29 | @pytest.fixture(autouse=True) 30 | async def rollback_connections(): 31 | with database.force_rollback(): 32 | async with database: 33 | yield 34 | 35 | 36 | async def test_model_update_or_create(): 37 | user, created = await User.query.update_or_create( 38 | name="Test", language="English", defaults={"name": "Jane"} 39 | ) 40 | assert created is True 41 | assert user.name == "Jane" 42 | assert user.language == "English" 43 | 44 | user, created = await User.query.update_or_create( 45 | name="Jane", language="English", defaults={"name": "Test"} 46 | ) 47 | assert created is False 48 | assert user.name == "Test" 49 | assert user.language == "English" 50 | -------------------------------------------------------------------------------- /tests/prefetch/test_prefetch_object.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.exceptions import QuerySetError 5 | from saffier.testclient import DatabaseTestClient as Database 6 | from tests.settings import DATABASE_URL 7 | 8 | pytestmark = pytest.mark.anyio 9 | 10 | database = Database(DATABASE_URL) 11 | models = saffier.Registry(database=database) 12 | 13 | 14 | class User(saffier.Model): 15 | name = saffier.CharField(max_length=100) 16 | 17 | class Meta: 18 | registry = models 19 | 20 | 21 | class Post(saffier.Model): 22 | user = saffier.ForeignKey(User, related_name="posts") 23 | comment = saffier.CharField(max_length=255) 24 | 25 | class Meta: 26 | registry = models 27 | 28 | 29 | class Article(saffier.Model): 30 | user = saffier.ForeignKey(User, related_name="articles") 31 | content = saffier.CharField(max_length=255) 32 | 33 | class Meta: 34 | registry = models 35 | 36 | 37 | @pytest.fixture(autouse=True, scope="function") 38 | async def create_test_database(): 39 | await models.create_all() 40 | yield 41 | await models.drop_all() 42 | 43 | 44 | @pytest.fixture(autouse=True) 45 | async def rollback_connections(): 46 | with database.force_rollback(): 47 | async with database: 48 | yield 49 | 50 | 51 | class Test: ... 52 | 53 | 54 | async def test_raise_prefetch_related_error(): 55 | await User.query.create(name="Saffier") 56 | 57 | with pytest.raises(QuerySetError): 58 | await User.query.prefetch_related( 59 | Test(), 60 | ).all() 61 | -------------------------------------------------------------------------------- /tests/registry/test_different_registry_default.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from enum import Enum 3 | 4 | import pytest 5 | 6 | import saffier 7 | from saffier.core.db import fields 8 | from saffier.testclient import DatabaseTestClient as Database 9 | from tests.settings import DATABASE_URL 10 | 11 | pytestmark = pytest.mark.anyio 12 | 13 | database = Database(DATABASE_URL) 14 | models = saffier.Registry(database=database, schema="another") 15 | 16 | 17 | def time(): 18 | return datetime.now().time() 19 | 20 | 21 | class StatusEnum(Enum): 22 | DRAFT = "Draft" 23 | RELEASED = "Released" 24 | 25 | 26 | class Product(saffier.Model): 27 | id = fields.IntegerField(primary_key=True) 28 | name = fields.CharField(max_length=255) 29 | 30 | class Meta: 31 | registry = models 32 | 33 | 34 | @pytest.fixture(autouse=True, scope="module") 35 | async def create_test_database(): 36 | await models.create_all() 37 | yield 38 | await models.drop_all() 39 | 40 | 41 | @pytest.fixture(autouse=True) 42 | async def rollback_transactions(): 43 | with database.force_rollback(): 44 | async with database: 45 | yield 46 | 47 | 48 | async def test_bulk_create(): 49 | await Product.query.bulk_create( 50 | [ 51 | {"name": "product-1"}, 52 | {"name": "product-2"}, 53 | ] 54 | ) 55 | 56 | total = await Product.query.all() 57 | 58 | assert len(total) == 2 59 | assert Product.table.schema == models.db_schema 60 | -------------------------------------------------------------------------------- /tests/registry/test_registries.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import saffier 4 | from saffier.testclient import DatabaseTestClient as Database 5 | from tests.settings import DATABASE_ALTERNATIVE_URL, DATABASE_URL 6 | 7 | database = Database(url=DATABASE_URL) 8 | another_db = Database(url=DATABASE_ALTERNATIVE_URL) 9 | 10 | registry = saffier.Registry(database=database, extra={"alternative": another_db}) 11 | pytestmark = pytest.mark.anyio 12 | 13 | 14 | @pytest.fixture(autouse=True, scope="module") 15 | async def create_test_database(): 16 | try: 17 | await registry.create_all() 18 | yield 19 | await registry.drop_all() 20 | except Exception: 21 | pytest.skip("No database available") 22 | 23 | 24 | @pytest.fixture(autouse=True) 25 | async def rollback_connections(): 26 | with database.force_rollback(): 27 | async with database: 28 | yield 29 | 30 | with another_db.force_rollback(): 31 | async with another_db: 32 | yield 33 | 34 | 35 | class User(saffier.Model): 36 | name = saffier.CharField(max_length=255, null=True) 37 | 38 | class Meta: 39 | registry = registry 40 | 41 | 42 | def test_has_multiple_connections(): 43 | assert "alternative" in User.meta.registry.extra 44 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass 3 | 4 | from saffier.contrib.multi_tenancy.settings import TenancySettings 5 | 6 | DATABASE_URL = os.environ.get( 7 | "TEST_DATABASE_URL", "postgresql+asyncpg://postgres:postgres@localhost:5432/saffier" 8 | ) 9 | DATABASE_ALTERNATIVE_URL = os.environ.get( 10 | "TEST_DATABASE_ALTERNATIVE_URL", 11 | "postgresql+asyncpg://postgres:postgres@localhost:5433/saffier_alt", 12 | ) 13 | TEST_DATABASE = "postgresql+asyncpg://postgres:postgres@localhost:5432/test_saffier" 14 | 15 | 16 | @dataclass 17 | class TestSettings(TenancySettings): 18 | tenant_model: str = "Tenant" 19 | auth_user_model: str = "User" 20 | -------------------------------------------------------------------------------- /tests/uniques/test_unique.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from enum import Enum 3 | 4 | import pytest 5 | from sqlalchemy.exc import IntegrityError 6 | 7 | import saffier 8 | from saffier.testclient import DatabaseTestClient as Database 9 | from tests.settings import DATABASE_URL 10 | 11 | pytestmark = pytest.mark.anyio 12 | 13 | database = Database(DATABASE_URL) 14 | models = saffier.Registry(database=database) 15 | 16 | 17 | def time(): 18 | return datetime.datetime.now().time() 19 | 20 | 21 | class StatusEnum(Enum): 22 | DRAFT = "Draft" 23 | RELEASED = "Released" 24 | 25 | 26 | class BaseModel(saffier.Model): 27 | class Meta: 28 | registry = models 29 | 30 | 31 | class User(BaseModel): 32 | name = saffier.CharField(max_length=255, unique=True) 33 | email = saffier.CharField(max_length=60) 34 | 35 | 36 | @pytest.fixture(autouse=True, scope="module") 37 | async def create_test_database(): 38 | await models.create_all() 39 | yield 40 | await models.drop_all() 41 | 42 | 43 | @pytest.fixture(autouse=True) 44 | async def rollback_transactions(): 45 | with database.force_rollback(): 46 | async with database: 47 | yield 48 | 49 | 50 | @pytest.mark.skipif(database.url.dialect == "mysql", reason="Not supported on MySQL") 51 | async def test_unique(): 52 | await User.query.create(name="Tiago", email="test@example.com") 53 | 54 | with pytest.raises(IntegrityError): 55 | await User.query.create(name="Tiago", email="test2@example.come") 56 | --------------------------------------------------------------------------------