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