├── piccolo ├── py.typed ├── apps │ ├── __init__.py │ ├── app │ │ ├── __init__.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ ├── templates │ │ │ │ ├── tables.py.jinja │ │ │ │ └── piccolo_app.py.jinja │ │ │ └── show_all.py │ │ └── piccolo_app.py │ ├── asgi │ │ ├── __init__.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ └── templates │ │ │ │ └── app │ │ │ │ ├── home │ │ │ │ ├── __init__.py.jinja │ │ │ │ ├── piccolo_migrations │ │ │ │ │ └── README.md │ │ │ │ ├── tables.py.jinja │ │ │ │ ├── _quart_endpoints.py.jinja │ │ │ │ ├── _sanic_endpoints.py.jinja │ │ │ │ ├── templates │ │ │ │ │ └── base.html.jinja_raw │ │ │ │ ├── _falcon_endpoints.py.jinja │ │ │ │ ├── _lilya_endpoints.py.jinja │ │ │ │ ├── _esmerald_endpoints.py.jinja │ │ │ │ ├── _starlette_endpoints.py.jinja │ │ │ │ ├── piccolo_app.py.jinja │ │ │ │ ├── _blacksheep_endpoints.py.jinja │ │ │ │ ├── _litestar_endpoints.py.jinja │ │ │ │ └── endpoints.py.jinja │ │ │ │ ├── static │ │ │ │ ├── favicon.ico │ │ │ │ └── main.css │ │ │ │ ├── requirements.txt.jinja │ │ │ │ ├── piccolo_conf_test.py.jinja │ │ │ │ ├── README.md.jinja │ │ │ │ ├── piccolo_conf.py.jinja │ │ │ │ ├── conftest.py.jinja │ │ │ │ ├── main.py.jinja │ │ │ │ ├── app.py.jinja │ │ │ │ ├── _lilya_app.py.jinja │ │ │ │ └── _starlette_app.py.jinja │ │ └── piccolo_app.py │ ├── fixtures │ │ ├── __init__.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ └── shared.py │ │ └── piccolo_app.py │ ├── meta │ │ ├── __init__.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ └── version.py │ │ └── piccolo_app.py │ ├── project │ │ ├── __init__.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ └── templates │ │ │ │ └── piccolo_conf.py.jinja │ │ └── piccolo_app.py │ ├── schema │ │ ├── __init__.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ ├── exceptions.py │ │ │ └── templates │ │ │ │ └── graphviz.dot.jinja │ │ └── piccolo_app.py │ ├── shell │ │ ├── __init__.py │ │ ├── commands │ │ │ └── __init__.py │ │ └── piccolo_app.py │ ├── tester │ │ ├── __init__.py │ │ ├── commands │ │ │ └── __init__.py │ │ └── piccolo_app.py │ ├── user │ │ ├── __init__.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ ├── change_password.py │ │ │ └── change_permissions.py │ │ ├── piccolo_migrations │ │ │ ├── __init__.py │ │ │ ├── 2020-06-11T21-38-55.py │ │ │ └── 2021-04-30T16-14-15.py │ │ └── piccolo_app.py │ ├── migrations │ │ ├── __init__.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ └── templates │ │ │ │ └── migration.py.jinja │ │ ├── auto │ │ │ ├── __init__.py │ │ │ └── operations.py │ │ ├── piccolo_app.py │ │ └── tables.py │ ├── playground │ │ ├── __init__.py │ │ ├── commands │ │ │ └── __init__.py │ │ └── piccolo_app.py │ └── sql_shell │ │ ├── __init__.py │ │ ├── commands │ │ └── __init__.py │ │ └── piccolo_app.py ├── conf │ └── __init__.py ├── query │ ├── operators │ │ └── __init__.py │ ├── functions │ │ ├── base.py │ │ ├── math.py │ │ └── __init__.py │ ├── methods │ │ ├── __init__.py │ │ ├── indexes.py │ │ ├── raw.py │ │ ├── drop_index.py │ │ ├── exists.py │ │ └── table_exists.py │ └── __init__.py ├── __init__.py ├── utils │ ├── __init__.py │ ├── naming.py │ ├── repr.py │ ├── sync.py │ └── warnings.py ├── columns │ ├── operators │ │ ├── base.py │ │ ├── string.py │ │ ├── __init__.py │ │ ├── math.py │ │ └── comparison.py │ ├── defaults │ │ ├── __init__.py │ │ └── uuid.py │ ├── indexes.py │ ├── choices.py │ └── __init__.py ├── engine │ ├── exceptions.py │ ├── __init__.py │ └── finder.py ├── testing │ └── __init__.py └── custom_types.py ├── tests ├── __init__.py ├── apps │ ├── __init__.py │ ├── app │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── test_show_all.py │ ├── asgi │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── files │ │ │ └── dummy_server.py │ ├── meta │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── test_version.py │ ├── shell │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── test_run.py │ ├── user │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── test_change_password.py │ ├── fixtures │ │ ├── __init__.py │ │ └── commands │ │ │ └── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── auto │ │ │ ├── __init__.py │ │ │ └── integration │ │ │ │ ├── __init__.py │ │ │ │ └── README.md │ │ ├── commands │ │ │ ├── __init__.py │ │ │ ├── test_migrations │ │ │ │ ├── __init__.py │ │ │ │ └── 2020-03-31T20-38-22.py │ │ │ ├── test_check.py │ │ │ └── test_clean.py │ │ └── test_migration.py │ ├── project │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── test_new.py │ ├── schema │ │ ├── __init__.py │ │ └── commands │ │ │ └── test_graph.py │ ├── sql_shell │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── test_run.py │ └── tester │ │ └── __init__.py ├── conf │ ├── __init__.py │ └── example.py ├── query │ ├── __init__.py │ ├── functions │ │ ├── __init__.py │ │ ├── base.py │ │ └── test_math.py │ ├── mixins │ │ ├── __init__.py │ │ └── test_order_by_delegate.py │ ├── operators │ │ ├── __init__.py │ │ └── test_json.py │ ├── test_await.py │ ├── test_gather.py │ └── test_slots.py ├── table │ ├── __init__.py │ ├── instance │ │ ├── __init__.py │ │ ├── test_remove.py │ │ ├── test_instantiate.py │ │ └── test_create.py │ ├── test_ref.py │ ├── test_exists.py │ ├── test_repr.py │ ├── test_all_columns.py │ ├── test_constructor.py │ ├── test_from_dict.py │ ├── test_update_self.py │ ├── test_create_db_tables.py │ ├── test_drop_db_tables.py │ └── test_table_exists.py ├── utils │ ├── __init__.py │ ├── test_warnings.py │ ├── test_encoding.py │ ├── test_naming.py │ ├── test_printing.py │ ├── test_list.py │ ├── test_sync.py │ ├── test_lazy_loader.py │ └── test_dictionary.py ├── columns │ ├── __init__.py │ ├── m2m │ │ ├── __init__.py │ │ └── test_m2m_schema.py │ ├── test_uuid.py │ ├── test_real.py │ ├── test_double_precision.py │ ├── test_varchar.py │ ├── test_numeric.py │ ├── test_readable.py │ ├── foreign_key │ │ ├── test_value_type.py │ │ ├── test_foreign_key_references.py │ │ ├── test_foreign_key_meta.py │ │ ├── test_on_delete_on_update.py │ │ ├── test_foreign_key_self.py │ │ └── test_reverse.py │ ├── test_integer.py │ ├── test_smallint.py │ ├── test_date.py │ ├── test_bigint.py │ ├── test_bytea.py │ └── test_reserved_column_names.py ├── engine │ ├── __init__.py │ ├── test_version_parsing.py │ ├── test_logging.py │ └── test_extra_nodes.py ├── testing │ └── __init__.py ├── example_apps │ ├── __init__.py │ ├── mega │ │ ├── __init__.py │ │ ├── piccolo_migrations │ │ │ └── __init__.py │ │ └── piccolo_app.py │ └── music │ │ ├── __init__.py │ │ ├── piccolo_app.py │ │ └── piccolo_migrations │ │ ├── music_2024_06_19t18_11_05_793132.py │ │ ├── 2021-11-13T14-01-46-114725.py │ │ ├── 2020-12-17T18-44-44.py │ │ └── 2021-09-06T13-58-23-024723.py ├── README.md ├── sqlite_conf.py ├── test_main.py ├── postgres_conf.py ├── cockroach_conf.py └── conftest.py ├── profiling ├── __init__.py ├── README.md └── run_profile.py ├── requirements ├── extras │ ├── pytest.txt │ ├── orjson.txt │ ├── playground.txt │ ├── postgres.txt │ ├── sqlite.txt │ └── uvloop.txt ├── profile-requirements.txt ├── readthedocs-requirements.txt ├── doc-requirements.txt ├── requirements.txt ├── test-requirements.txt ├── dev-requirements.txt └── README.md ├── docs ├── src │ ├── piccolo │ │ ├── changes │ │ │ └── index.rst │ │ ├── query_clauses │ │ │ ├── on_conflict │ │ │ │ └── bands.csv │ │ │ ├── distinct │ │ │ │ └── albums.csv │ │ │ ├── freeze.rst │ │ │ ├── limit.rst │ │ │ ├── first.rst │ │ │ ├── offset.rst │ │ │ ├── index.rst │ │ │ ├── as_of.rst │ │ │ ├── group_by.rst │ │ │ └── batch.rst │ │ ├── schema │ │ │ ├── images │ │ │ │ └── m2m.png │ │ │ └── index.rst │ │ ├── migrations │ │ │ └── index.rst │ │ ├── projects_and_apps │ │ │ ├── images │ │ │ │ └── schema_graph_output.png │ │ │ └── index.rst │ │ ├── features │ │ │ ├── index.rst │ │ │ ├── security.rst │ │ │ ├── syntax.rst │ │ │ └── types_and_tab_completion.rst │ │ ├── help │ │ │ └── index.rst │ │ ├── playground │ │ │ └── index.rst │ │ ├── getting_started │ │ │ ├── index.rst │ │ │ ├── setup_sqlite.rst │ │ │ ├── database_support.rst │ │ │ ├── example_schema.rst │ │ │ └── installing_piccolo.rst │ │ ├── tutorials │ │ │ ├── index.rst │ │ │ ├── avoiding_circular_imports_src │ │ │ │ └── tables.py │ │ │ └── fastapi_src │ │ │ │ └── app.py │ │ ├── query_types │ │ │ ├── exists.rst │ │ │ ├── raw.rst │ │ │ ├── delete.rst │ │ │ ├── insert.rst │ │ │ ├── index.rst │ │ │ └── create_table.rst │ │ ├── functions │ │ │ ├── index.rst │ │ │ ├── math.rst │ │ │ ├── array.rst │ │ │ ├── aggregate.rst │ │ │ ├── string.rst │ │ │ ├── custom.rst │ │ │ ├── type_conversion.rst │ │ │ ├── datetime.rst │ │ │ └── basic_usage.rst │ │ ├── engines │ │ │ ├── sqlite_engine.rst │ │ │ ├── postgres_engine.rst │ │ │ └── cockroach_engine.rst │ │ ├── authentication │ │ │ └── index.rst │ │ └── ecosystem │ │ │ └── index.rst │ └── logo.png ├── logo_hero.png └── Makefile ├── .flake8 ├── scripts ├── profile.sh ├── run-docs.sh ├── release.sh ├── format.sh ├── piccolo.sh ├── pyright.sh ├── test-integration.sh ├── test-postgres.sh ├── test-sqlite.sh ├── test-cockroach.sh ├── test-strict.sh ├── lint.sh └── README.md ├── .gitignore ├── .readthedocs.yaml ├── piccolo_conf.py ├── .github ├── ISSUE_TEMPLATES │ ├── support.yml │ ├── issues.yml │ └── features.yml └── workflows │ └── release.yaml ├── SECURITY.md ├── pyproject.toml └── LICENSE /piccolo/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /profiling/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/conf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/query/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/table/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/conf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/asgi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/meta/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/shell/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/columns/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/engine/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/meta/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/schema/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/shell/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/tester/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/schema/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/sql_shell/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/tester/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/columns/m2m/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/example_apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/query/functions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/query/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/query/operators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/table/instance/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/app/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/meta/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/playground/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/sql_shell/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/user/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/query/operators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/app/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/asgi/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/meta/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/migrations/auto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/shell/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/user/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/example_apps/mega/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/example_apps/music/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/fixtures/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/project/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/schema/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/shell/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/sql_shell/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/tester/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/extras/pytest.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | -------------------------------------------------------------------------------- /tests/apps/fixtures/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/migrations/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/project/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/sql_shell/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/__init__.py: -------------------------------------------------------------------------------- 1 | __VERSION__ = "1.30.0" 2 | -------------------------------------------------------------------------------- /piccolo/apps/migrations/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/playground/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/user/piccolo_migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/extras/orjson.txt: -------------------------------------------------------------------------------- 1 | orjson>=3.5.1 2 | -------------------------------------------------------------------------------- /requirements/extras/playground.txt: -------------------------------------------------------------------------------- 1 | ipython 2 | -------------------------------------------------------------------------------- /requirements/extras/postgres.txt: -------------------------------------------------------------------------------- 1 | asyncpg>=0.30.0 2 | -------------------------------------------------------------------------------- /requirements/extras/sqlite.txt: -------------------------------------------------------------------------------- 1 | aiosqlite>=0.16.0 2 | -------------------------------------------------------------------------------- /tests/apps/migrations/auto/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/migrations/commands/test_migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/example_apps/mega/piccolo_migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/__init__.py.jinja: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/profile-requirements.txt: -------------------------------------------------------------------------------- 1 | viztracer==0.15.0 2 | -------------------------------------------------------------------------------- /piccolo/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .naming import _camel_to_snake 2 | -------------------------------------------------------------------------------- /docs/src/piccolo/changes/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../../../CHANGES.rst 2 | -------------------------------------------------------------------------------- /piccolo/columns/operators/base.py: -------------------------------------------------------------------------------- 1 | class Operator: 2 | template = "" 3 | -------------------------------------------------------------------------------- /requirements/extras/uvloop.txt: -------------------------------------------------------------------------------- 1 | uvloop>=0.12.0; sys_platform != "win32" 2 | -------------------------------------------------------------------------------- /piccolo/engine/exceptions.py: -------------------------------------------------------------------------------- 1 | class TransactionError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | per-file-ignores = __init__.py:F401 3 | max-line-length = 79 4 | -------------------------------------------------------------------------------- /piccolo/apps/app/commands/templates/tables.py.jinja: -------------------------------------------------------------------------------- 1 | from piccolo.table import Table 2 | -------------------------------------------------------------------------------- /docs/logo_hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piccolo-orm/piccolo/HEAD/docs/logo_hero.png -------------------------------------------------------------------------------- /docs/src/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piccolo-orm/piccolo/HEAD/docs/src/logo.png -------------------------------------------------------------------------------- /requirements/readthedocs-requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | -r doc-requirements.txt 3 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_clauses/on_conflict/bands.csv: -------------------------------------------------------------------------------- 1 | id,name,popularity 2 | 1,Pythonistas,1000 3 | -------------------------------------------------------------------------------- /scripts/profile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python -m profiling.run_profile && vizviewer result.json 3 | -------------------------------------------------------------------------------- /scripts/run-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sphinx-autobuild -a docs/src docs/build/html --watch piccolo 3 | -------------------------------------------------------------------------------- /requirements/doc-requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==8.3.0 2 | piccolo-theme==0.24.0 3 | sphinx-autobuild==2025.8.25 4 | -------------------------------------------------------------------------------- /piccolo/testing/__init__.py: -------------------------------------------------------------------------------- 1 | from piccolo.testing.model_builder import ModelBuilder 2 | 3 | __all__ = ["ModelBuilder"] 4 | -------------------------------------------------------------------------------- /docs/src/piccolo/schema/images/m2m.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piccolo-orm/piccolo/HEAD/docs/src/piccolo/schema/images/m2m.png -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/piccolo_migrations/README.md: -------------------------------------------------------------------------------- 1 | Add migrations using `piccolo migrations new home --auto`. 2 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -rf ./build/* 3 | rm -rf ./dist/* 4 | python setup.py sdist bdist_wheel 5 | twine upload dist/* 6 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piccolo-orm/piccolo/HEAD/piccolo/apps/asgi/commands/templates/app/static/favicon.ico -------------------------------------------------------------------------------- /profiling/README.md: -------------------------------------------------------------------------------- 1 | # Profiling 2 | 3 | Tests we run to evaluate Piccolo performance. 4 | 5 | You need to setup a local Postgres database called 'piccolo_profile'. 6 | -------------------------------------------------------------------------------- /requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | black 2 | colorama>=0.4.0 3 | Jinja2>=2.11.0 4 | targ>=0.3.7 5 | inflection>=0.5.1 6 | typing-extensions>=4.3.0 7 | pydantic[email]==2.* 8 | -------------------------------------------------------------------------------- /requirements/test-requirements.txt: -------------------------------------------------------------------------------- 1 | coveralls==3.3.1 2 | httpx==0.28.0 3 | pytest-cov==3.0.0 4 | pytest==8.3.5 5 | python-dateutil==2.8.2 6 | typing-extensions>=4.3.0 7 | -------------------------------------------------------------------------------- /docs/src/piccolo/migrations/index.rst: -------------------------------------------------------------------------------- 1 | .. _migrations: 2 | 3 | Migrations 4 | ========== 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | ./create 10 | ./running 11 | -------------------------------------------------------------------------------- /docs/src/piccolo/projects_and_apps/images/schema_graph_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piccolo-orm/piccolo/HEAD/docs/src/piccolo/projects_and_apps/images/schema_graph_output.png -------------------------------------------------------------------------------- /piccolo/apps/meta/commands/version.py: -------------------------------------------------------------------------------- 1 | import piccolo 2 | 3 | 4 | def version(): 5 | """ 6 | Prints out the Piccolo version. 7 | """ 8 | print(piccolo.__VERSION__) 9 | -------------------------------------------------------------------------------- /docs/src/piccolo/features/index.rst: -------------------------------------------------------------------------------- 1 | Features 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | ./types_and_tab_completion 8 | ./security 9 | ./syntax 10 | -------------------------------------------------------------------------------- /scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SOURCES="piccolo tests" 3 | 4 | echo "Running isort..." 5 | isort $SOURCES 6 | echo "-----" 7 | 8 | echo "Running black..." 9 | black $SOURCES 10 | -------------------------------------------------------------------------------- /piccolo/columns/defaults/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | from .date import * # noqa 3 | from .time import * # noqa 4 | from .timestamp import * # noqa 5 | from .uuid import * # noqa 6 | -------------------------------------------------------------------------------- /docs/src/piccolo/help/index.rst: -------------------------------------------------------------------------------- 1 | Help 2 | ==== 3 | 4 | If you have any questions then the best place to ask them is the 5 | `discussions section on our GitHub page `_. 6 | -------------------------------------------------------------------------------- /piccolo/columns/operators/string.py: -------------------------------------------------------------------------------- 1 | from .base import Operator 2 | 3 | 4 | class StringOperator(Operator): 5 | pass 6 | 7 | 8 | class Concat(StringOperator): 9 | template = "{value_1} || {value_2}" 10 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/requirements.txt.jinja: -------------------------------------------------------------------------------- 1 | {%- for router_dependency in router_dependencies -%} 2 | {{ router_dependency }} 3 | {% endfor -%} 4 | {{ server }} 5 | piccolo[postgres]>=1.0.0 6 | piccolo_admin>=1.0.0 -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | ## Database 4 | 5 | A local database needs to be running with the following credentials: 6 | 7 | ``` 8 | host = localhost:5432 9 | database = piccolo 10 | user = piccolo 11 | password = piccolo 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_clauses/distinct/albums.csv: -------------------------------------------------------------------------------- 1 | id,band,title,release_date 2 | 1,Pythonistas,Py album 2021,2021-12-01 3 | 2,Pythonistas,Py album 2022,2022-12-01 4 | 3,Rustaceans,Rusty album 2021,2021-12-01 5 | 4,Rustaceans,Rusty album 2022,2022-12-01 6 | -------------------------------------------------------------------------------- /requirements/dev-requirements.txt: -------------------------------------------------------------------------------- 1 | black==24.3.0 2 | ipdb==0.13.9 3 | ipython>=7.31.1 4 | flake8==6.1.0 5 | isort==5.10.1 6 | slotscheck==0.17.1 7 | twine==3.8.0 8 | mypy==1.18.1 9 | pip-upgrader==1.4.15 10 | pyright==1.1.367 11 | wheel==0.38.1 12 | -------------------------------------------------------------------------------- /docs/src/piccolo/playground/index.rst: -------------------------------------------------------------------------------- 1 | Playground 2 | ========== 3 | 4 | Piccolo ships with a handy command to help learn the different queries. For 5 | simple usage see :ref:`Playground`. 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | 10 | ./advanced 11 | -------------------------------------------------------------------------------- /scripts/piccolo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is used for running Piccolo commands on the example project within 3 | # the tests folder. For example, if we need to add a new auto migration to it. 4 | export PICCOLO_CONF="tests.postgres_conf" 5 | python -m piccolo.main $@ 6 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_clauses/freeze.rst: -------------------------------------------------------------------------------- 1 | .. _freeze: 2 | 3 | freeze 4 | ====== 5 | 6 | You can use the ``freeze`` clause with any query type. 7 | 8 | Source 9 | ------ 10 | 11 | .. currentmodule:: piccolo.query.base 12 | 13 | .. automethod:: Query.freeze 14 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/piccolo_app.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppConfig, Command 2 | 3 | from .commands.new import new 4 | 5 | APP_CONFIG = AppConfig( 6 | app_name="asgi", 7 | migrations_folder_path="", 8 | commands=[Command(callable=new, aliases=["create"])], 9 | ) 10 | -------------------------------------------------------------------------------- /piccolo/apps/shell/piccolo_app.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppConfig, Command 2 | 3 | from .commands.run import run 4 | 5 | APP_CONFIG = AppConfig( 6 | app_name="shell", 7 | migrations_folder_path="", 8 | commands=[Command(callable=run, aliases=["start"])], 9 | ) 10 | -------------------------------------------------------------------------------- /docs/src/piccolo/schema/index.rst: -------------------------------------------------------------------------------- 1 | Schema 2 | ====== 3 | 4 | The schema is how you define your database tables, columns and relationships. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | ./defining 10 | ./column_types 11 | ./m2m 12 | ./one_to_one 13 | ./advanced 14 | -------------------------------------------------------------------------------- /piccolo/apps/meta/piccolo_app.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppConfig, Command 2 | 3 | from .commands.version import version 4 | 5 | APP_CONFIG = AppConfig( 6 | app_name="meta", 7 | migrations_folder_path="", 8 | commands=[Command(callable=version, aliases=["v"])], 9 | ) 10 | -------------------------------------------------------------------------------- /piccolo/apps/project/piccolo_app.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppConfig, Command 2 | 3 | from .commands.new import new 4 | 5 | APP_CONFIG = AppConfig( 6 | app_name="project", 7 | migrations_folder_path="", 8 | commands=[Command(callable=new, aliases=["create"])], 9 | ) 10 | -------------------------------------------------------------------------------- /piccolo/apps/playground/piccolo_app.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppConfig, Command 2 | 3 | from .commands.run import run 4 | 5 | APP_CONFIG = AppConfig( 6 | app_name="playground", 7 | migrations_folder_path="", 8 | commands=[Command(callable=run, aliases=["start"])], 9 | ) 10 | -------------------------------------------------------------------------------- /piccolo/apps/sql_shell/piccolo_app.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppConfig, Command 2 | 3 | from .commands.run import run 4 | 5 | APP_CONFIG = AppConfig( 6 | app_name="sql_shell", 7 | migrations_folder_path="", 8 | commands=[Command(callable=run, aliases=["start"])], 9 | ) 10 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/tables.py.jinja: -------------------------------------------------------------------------------- 1 | from piccolo.table import Table 2 | from piccolo.columns import Varchar, Boolean 3 | 4 | 5 | class Task(Table): 6 | """ 7 | An example table. 8 | """ 9 | 10 | name = Varchar() 11 | completed = Boolean(default=False) 12 | -------------------------------------------------------------------------------- /piccolo/apps/tester/piccolo_app.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppConfig 2 | 3 | from .commands.run import run 4 | 5 | APP_CONFIG = AppConfig( 6 | app_name="tester", 7 | migrations_folder_path="", 8 | table_classes=[], 9 | migration_dependencies=[], 10 | commands=[run], 11 | ) 12 | -------------------------------------------------------------------------------- /tests/table/test_ref.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.columns.column_types import Varchar 4 | from tests.example_apps.music.tables import Band 5 | 6 | 7 | class TestRef(TestCase): 8 | def test_ref(self): 9 | column = Band.ref("manager.name") 10 | self.assertIsInstance(column, Varchar) 11 | -------------------------------------------------------------------------------- /piccolo/apps/schema/commands/exceptions.py: -------------------------------------------------------------------------------- 1 | class SchemaCommandError(Exception): 2 | """ 3 | Base class for all schema command errors. 4 | """ 5 | 6 | pass 7 | 8 | 9 | class GenerateError(SchemaCommandError): 10 | """ 11 | Raised when an error occurs during schema generation. 12 | """ 13 | 14 | pass 15 | -------------------------------------------------------------------------------- /tests/sqlite_conf.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppRegistry 2 | from piccolo.engine.sqlite import SQLiteEngine 3 | 4 | DB = SQLiteEngine(path="test.sqlite") 5 | 6 | 7 | APP_REGISTRY = AppRegistry( 8 | apps=[ 9 | "tests.example_apps.music.piccolo_app", 10 | "tests.example_apps.mega.piccolo_app", 11 | ] 12 | ) 13 | -------------------------------------------------------------------------------- /tests/utils/test_warnings.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.utils.warnings import colored_warning 4 | 5 | 6 | class TestColoredWarning(TestCase): 7 | def test_colored_warning(self): 8 | """ 9 | Just make sure no errors are raised. 10 | """ 11 | colored_warning(message="TESTING!") 12 | -------------------------------------------------------------------------------- /piccolo/apps/app/commands/show_all.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import Finder 2 | 3 | 4 | def show_all(): 5 | """ 6 | Lists all registered Piccolo apps. 7 | """ 8 | app_registry = Finder().get_app_registry() 9 | 10 | print("Registered apps:") 11 | 12 | for app_path in app_registry.apps: 13 | print(app_path) 14 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/piccolo_conf_test.py.jinja: -------------------------------------------------------------------------------- 1 | from piccolo_conf import * # noqa 2 | 3 | 4 | DB = PostgresEngine( 5 | config={ 6 | "database": "{{ project_identifier }}_test", 7 | "user": "postgres", 8 | "password": "", 9 | "host": "localhost", 10 | "port": 5432, 11 | } 12 | ) 13 | -------------------------------------------------------------------------------- /piccolo/apps/fixtures/piccolo_app.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppConfig 2 | 3 | from .commands.dump import dump 4 | from .commands.load import load 5 | 6 | APP_CONFIG = AppConfig( 7 | app_name="fixtures", 8 | migrations_folder_path="", 9 | table_classes=[], 10 | migration_dependencies=[], 11 | commands=[dump, load], 12 | ) 13 | -------------------------------------------------------------------------------- /scripts/pyright.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # We have a separate script for pyright vs lint.sh, as it's hard to get 100% 3 | # success in pyright. In the future we might merge them. 4 | 5 | set -e 6 | 7 | MODULES="piccolo" 8 | SOURCES="$MODULES tests" 9 | 10 | echo "Running pyright..." 11 | pyright $sources 12 | echo "-----" 13 | 14 | echo "All passed!" 15 | -------------------------------------------------------------------------------- /scripts/test-integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # To run all in a folder tests/ 3 | # To run all in a file tests/test_foo.py 4 | # To run all in a class tests/test_foo.py::TestFoo 5 | # To run a single test tests/test_foo.py::TestFoo::test_foo 6 | 7 | export PICCOLO_CONF="tests.postgres_conf" 8 | python -m pytest \ 9 | -m integration \ 10 | -s $@ 11 | -------------------------------------------------------------------------------- /tests/table/test_exists.py: -------------------------------------------------------------------------------- 1 | from tests.base import DBTestCase 2 | from tests.example_apps.music.tables import Band 3 | 4 | 5 | class TestExists(DBTestCase): 6 | def test_exists(self): 7 | self.insert_rows() 8 | 9 | response = Band.exists().where(Band.name == "Pythonistas").run_sync() 10 | 11 | self.assertTrue(response) 12 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/README.md.jinja: -------------------------------------------------------------------------------- 1 | # {{ project_identifier }} 2 | 3 | ## Setup 4 | 5 | ### Install requirements 6 | 7 | ```bash 8 | pip install -r requirements.txt 9 | ``` 10 | 11 | ### Getting started guide 12 | 13 | ```bash 14 | python main.py 15 | ``` 16 | 17 | ### Running tests 18 | 19 | ```bash 20 | piccolo tester run 21 | ``` 22 | -------------------------------------------------------------------------------- /piccolo/engine/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Engine 2 | from .cockroach import CockroachEngine 3 | from .finder import engine_finder 4 | from .postgres import PostgresEngine 5 | from .sqlite import SQLiteEngine 6 | 7 | __all__ = [ 8 | "Engine", 9 | "PostgresEngine", 10 | "SQLiteEngine", 11 | "CockroachEngine", 12 | "engine_finder", 13 | ] 14 | -------------------------------------------------------------------------------- /piccolo/utils/naming.py: -------------------------------------------------------------------------------- 1 | import inflection 2 | 3 | 4 | def _camel_to_snake(string: str): 5 | """ 6 | Convert CamelCase to snake_case. 7 | """ 8 | return inflection.underscore(string) 9 | 10 | 11 | def _snake_to_camel(string: str): 12 | """ 13 | Convert snake_case to CamelCase. 14 | """ 15 | return inflection.camelize(string) 16 | -------------------------------------------------------------------------------- /tests/apps/migrations/test_migration.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.apps.migrations.tables import Migration 4 | 5 | 6 | class TestMigrationTable(TestCase): 7 | def test_migration_table(self): 8 | Migration.create_table(if_not_exists=True).run_sync() 9 | Migration.select().run_sync() 10 | Migration.alter().drop_table().run_sync() 11 | -------------------------------------------------------------------------------- /docs/src/piccolo/getting_started/index.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | .. toctree:: 5 | :caption: Contents: 6 | :maxdepth: 1 7 | 8 | ./what_is_piccolo 9 | ./database_support 10 | ./installing_piccolo 11 | ./playground 12 | ./setup_postgres 13 | ./setup_cockroach 14 | ./setup_sqlite 15 | ./example_schema 16 | ./sync_and_async 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .pytest_cache/ 3 | .mypy_cache/ 4 | .DS_Store 5 | docs/_build/ 6 | build/ 7 | .doctrees/ 8 | .vscode/ 9 | piccolo.egg-info/ 10 | .idea/ 11 | dist/ 12 | piccolo.sqlite 13 | _build/ 14 | .coverage 15 | coverage.xml 16 | *.sqlite 17 | htmlcov/ 18 | prof/ 19 | .env/ 20 | .venv/ 21 | result.json 22 | 23 | # CockroachDB 24 | cockroach-data/ 25 | heap_profiler/ 26 | goroutine_dump/ 27 | -------------------------------------------------------------------------------- /tests/apps/meta/commands/test_version.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock, patch 3 | 4 | from piccolo.apps.meta.commands.version import version 5 | 6 | 7 | class TestVersion(TestCase): 8 | @patch("piccolo.apps.meta.commands.version.print") 9 | def test_version(self, print_: MagicMock): 10 | version() 11 | print_.assert_called_once() 12 | -------------------------------------------------------------------------------- /piccolo/apps/migrations/auto/__init__.py: -------------------------------------------------------------------------------- 1 | from .diffable_table import DiffableTable 2 | from .migration_manager import MigrationManager 3 | from .schema_differ import AlterStatements, SchemaDiffer 4 | from .schema_snapshot import SchemaSnapshot 5 | 6 | __all__ = [ 7 | "DiffableTable", 8 | "MigrationManager", 9 | "AlterStatements", 10 | "SchemaDiffer", 11 | "SchemaSnapshot", 12 | ] 13 | -------------------------------------------------------------------------------- /requirements/README.md: -------------------------------------------------------------------------------- 1 | # Requirement files 2 | 3 | - `extras` - optional dependencies of `Piccolo`. 4 | - `dev-requirements.txt` - needed to develop `Piccolo`. 5 | - `requirements.txt` - default requirements of `Piccolo`. 6 | - `test-requirements.txt` - needed to run `Piccolo` tests. 7 | - `doc-requirements.txt` - needed to run the `Piccolo` docs 8 | - `readthedocs-requirements.txt` - just used by ReadTheDocs. 9 | -------------------------------------------------------------------------------- /tests/utils/test_encoding.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.utils.encoding import dump_json, load_json 4 | 5 | 6 | class TestEncodingDecoding(TestCase): 7 | def test_dump_load(self): 8 | """ 9 | Test dumping then loading an object. 10 | """ 11 | payload = {"a": [1, 2, 3]} 12 | self.assertEqual(load_json(dump_json(payload)), payload) 13 | -------------------------------------------------------------------------------- /tests/apps/migrations/auto/integration/README.md: -------------------------------------------------------------------------------- 1 | # Integration tests 2 | 3 | The integration tests are for testing the migrations end to end - from auto generating a migration file, to running it. 4 | 5 | Migrations are complex - columns can be added, deleted, renamed, and modified (likewise with tables). Migrations can also be run backwards. To properly test all of the possible options, we need a lot of test cases. 6 | -------------------------------------------------------------------------------- /docs/src/piccolo/features/security.rst: -------------------------------------------------------------------------------- 1 | .. _Security: 2 | 3 | Security 4 | ======== 5 | 6 | SQL Injection protection 7 | ------------------------ 8 | 9 | If you look under the hood, Piccolo uses a custom class called ``QueryString`` 10 | for composing queries. It keeps query parameters separate from the query 11 | string, so we can pass parameterised queries to the engine. This helps 12 | prevent SQL Injection attacks. 13 | -------------------------------------------------------------------------------- /piccolo/apps/app/piccolo_app.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppConfig, Command 2 | 3 | from .commands.new import new 4 | from .commands.show_all import show_all 5 | 6 | APP_CONFIG = AppConfig( 7 | app_name="app", 8 | migrations_folder_path="", 9 | commands=[ 10 | Command(callable=new, aliases=["create"]), 11 | Command(callable=show_all, aliases=["show", "all", "list"]), 12 | ], 13 | ) 14 | -------------------------------------------------------------------------------- /tests/conf/example.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is used by test_apps.py to make sure we can exclude imported 3 | ``Table`` subclasses when using ``table_finder``. 4 | """ 5 | 6 | from piccolo.apps.user.tables import BaseUser 7 | from piccolo.columns.column_types import ForeignKey, Varchar 8 | from piccolo.table import Table 9 | 10 | 11 | class Musician(Table): 12 | name = Varchar() 13 | user = ForeignKey(BaseUser) 14 | -------------------------------------------------------------------------------- /docs/src/piccolo/tutorials/index.rst: -------------------------------------------------------------------------------- 1 | Tutorials 2 | ========= 3 | 4 | These tutorials bring together information from across the documentation, to 5 | help you solve common problems: 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | 10 | ./migrate_existing_project 11 | ./using_sqlite_and_asyncio_effectively 12 | ./deployment 13 | ./fastapi 14 | ./avoiding_circular_imports 15 | ./moving_table_between_apps 16 | -------------------------------------------------------------------------------- /piccolo/columns/operators/__init__.py: -------------------------------------------------------------------------------- 1 | from .comparison import ( 2 | ArrayAll, 3 | ArrayAny, 4 | Equal, 5 | GreaterEqualThan, 6 | GreaterThan, 7 | ILike, 8 | In, 9 | IsNotNull, 10 | IsNull, 11 | LessEqualThan, 12 | LessThan, 13 | Like, 14 | NotEqual, 15 | NotIn, 16 | NotLike, 17 | ) 18 | from .math import Add, Divide, Multiply, Subtract 19 | from .string import Concat 20 | -------------------------------------------------------------------------------- /tests/columns/m2m/test_m2m_schema.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from tests.base import engines_skip 4 | 5 | from .base import M2MBase 6 | 7 | 8 | @engines_skip("sqlite") 9 | class TestM2MWithSchema(M2MBase, TestCase): 10 | """ 11 | Make sure that when the tables exist in a non-public schema, that M2M still 12 | works. 13 | """ 14 | 15 | def setUp(self): 16 | return self._setUp(schema="schema_1") 17 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | from unittest import IsolatedAsyncioTestCase 2 | 3 | from piccolo.apps.migrations.tables import Migration 4 | from piccolo.main import main 5 | 6 | 7 | class TestMain(IsolatedAsyncioTestCase): 8 | 9 | async def asyncTearDown(self): 10 | await Migration.alter().drop_table(if_exists=True) 11 | 12 | def test_main(self): 13 | # Just make sure it runs without raising any errors. 14 | main() 15 | -------------------------------------------------------------------------------- /docs/src/piccolo/projects_and_apps/index.rst: -------------------------------------------------------------------------------- 1 | .. _ProjectsAndApps: 2 | 3 | Projects and Apps 4 | ================= 5 | 6 | By using Piccolo projects and apps, you can build a larger, more modular, 7 | application. 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | ./piccolo_projects 13 | ./piccolo_apps 14 | ./included_apps 15 | 16 | .. note:: 17 | 18 | There is a `video tutorial on YouTube `_. 19 | -------------------------------------------------------------------------------- /piccolo/columns/operators/math.py: -------------------------------------------------------------------------------- 1 | from .base import Operator 2 | 3 | 4 | class MathOperator(Operator): 5 | pass 6 | 7 | 8 | class Add(MathOperator): 9 | template = "{name} + {value}" 10 | 11 | 12 | class Subtract(MathOperator): 13 | template = "{name} - {value}" 14 | 15 | 16 | class Multiply(MathOperator): 17 | template = "{name} * {value}" 18 | 19 | 20 | class Divide(MathOperator): 21 | template = "{name} * {value}" 22 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_types/exists.rst: -------------------------------------------------------------------------------- 1 | .. _Exists: 2 | 3 | Exists 4 | ====== 5 | 6 | This checks whether any rows exist which match the criteria. 7 | 8 | .. code-block:: python 9 | 10 | >>> await Band.exists().where(Band.name == 'Pythonistas') 11 | True 12 | 13 | ------------------------------------------------------------------------------- 14 | 15 | Query clauses 16 | ------------- 17 | 18 | where 19 | ~~~~~ 20 | 21 | See :ref:`where`. 22 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | 7 | build: 8 | os: ubuntu-22.04 9 | tools: 10 | python: "3.11" 11 | 12 | sphinx: 13 | configuration: docs/src/conf.py 14 | 15 | formats: 16 | - pdf 17 | - epub 18 | 19 | python: 20 | install: 21 | - requirements: requirements/readthedocs-requirements.txt 22 | -------------------------------------------------------------------------------- /tests/example_apps/music/piccolo_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from piccolo.conf.apps import AppConfig, table_finder 4 | 5 | CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) 6 | 7 | 8 | APP_CONFIG = AppConfig( 9 | app_name="music", 10 | table_classes=table_finder(["tests.example_apps.music.tables"]), 11 | migrations_folder_path=os.path.join( 12 | CURRENT_DIRECTORY, "piccolo_migrations" 13 | ), 14 | commands=[], 15 | ) 16 | -------------------------------------------------------------------------------- /scripts/test-postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # To run all in a folder tests/ 3 | # To run all in a file tests/test_foo.py 4 | # To run all in a class tests/test_foo.py::TestFoo 5 | # To run a single test tests/test_foo.py::TestFoo::test_foo 6 | 7 | export PICCOLO_CONF="tests.postgres_conf" 8 | python -m pytest \ 9 | --cov=piccolo \ 10 | --cov-report=xml \ 11 | --cov-report=html \ 12 | --cov-fail-under=85 \ 13 | -m "not integration" \ 14 | -s $@ 15 | -------------------------------------------------------------------------------- /scripts/test-sqlite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # To run all in a folder tests/ 3 | # To run all in a file tests/test_foo.py 4 | # To run all in a class tests/test_foo.py::TestFoo 5 | # To run a single test tests/test_foo.py::TestFoo::test_foo 6 | 7 | export PICCOLO_CONF="tests.sqlite_conf" 8 | python -m pytest \ 9 | --cov=piccolo \ 10 | --cov-report=xml \ 11 | --cov-report=html \ 12 | --cov-fail-under=75 \ 13 | -m "not integration" \ 14 | -s $@ 15 | -------------------------------------------------------------------------------- /docs/src/piccolo/functions/index.rst: -------------------------------------------------------------------------------- 1 | Functions 2 | ========= 3 | 4 | .. hint:: 5 | This is an advanced topic - if you're new to Piccolo you can skip this for 6 | now. 7 | 8 | Functions can be used to modify how queries are run, and what is returned. 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | ./basic_usage 14 | ./aggregate 15 | ./array 16 | ./datetime 17 | ./math 18 | ./string 19 | ./type_conversion 20 | ./custom 21 | -------------------------------------------------------------------------------- /tests/example_apps/mega/piccolo_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from piccolo.conf.apps import AppConfig 4 | 5 | from .tables import MegaTable, SmallTable 6 | 7 | CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) 8 | 9 | 10 | APP_CONFIG = AppConfig( 11 | app_name="mega", 12 | table_classes=[MegaTable, SmallTable], 13 | migrations_folder_path=os.path.join( 14 | CURRENT_DIRECTORY, "piccolo_migrations" 15 | ), 16 | commands=[], 17 | ) 18 | -------------------------------------------------------------------------------- /docs/src/piccolo/functions/math.rst: -------------------------------------------------------------------------------- 1 | Math functions 2 | ============== 3 | 4 | .. currentmodule:: piccolo.query.functions.math 5 | 6 | Abs 7 | --- 8 | 9 | .. autoclass:: Abs 10 | :class-doc-from: class 11 | 12 | Ceil 13 | ---- 14 | 15 | .. autoclass:: Ceil 16 | :class-doc-from: class 17 | 18 | Floor 19 | ----- 20 | 21 | .. autoclass:: Floor 22 | :class-doc-from: class 23 | 24 | Round 25 | ----- 26 | 27 | .. autoclass:: Round 28 | :class-doc-from: class 29 | -------------------------------------------------------------------------------- /piccolo/columns/indexes.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class IndexMethod(str, Enum): 5 | """ 6 | Used to specify the index method for a 7 | :class:`Column `. 8 | """ 9 | 10 | btree = "btree" 11 | hash = "hash" 12 | gist = "gist" 13 | gin = "gin" 14 | 15 | def __str__(self): 16 | return f"{self.__class__.__name__}.{self.name}" 17 | 18 | def __repr__(self): 19 | return self.__str__() 20 | -------------------------------------------------------------------------------- /tests/columns/test_uuid.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from piccolo.columns.column_types import UUID 4 | from piccolo.table import Table 5 | from piccolo.testing.test_case import TableTest 6 | 7 | 8 | class MyTable(Table): 9 | uuid = UUID() 10 | 11 | 12 | class TestUUID(TableTest): 13 | tables = [MyTable] 14 | 15 | def test_return_type(self): 16 | row = MyTable() 17 | row.save().run_sync() 18 | 19 | self.assertIsInstance(row.uuid, uuid.UUID) 20 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_clauses/limit.rst: -------------------------------------------------------------------------------- 1 | .. _limit: 2 | 3 | limit 4 | ===== 5 | 6 | You can use ``limit`` clauses with the following queries: 7 | 8 | * :ref:`Objects` 9 | * :ref:`Select` 10 | 11 | Rather than returning all of the matching results, it will only return the 12 | number you ask for. 13 | 14 | .. code-block:: python 15 | 16 | await Band.select().limit(2) 17 | 18 | Likewise, with objects: 19 | 20 | .. code-block:: python 21 | 22 | await Band.objects().limit(2) 23 | -------------------------------------------------------------------------------- /scripts/test-cockroach.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # To run all in a folder tests/ 3 | # To run all in a file tests/test_foo.py 4 | # To run all in a class tests/test_foo.py::TestFoo 5 | # To run a single test tests/test_foo.py::TestFoo::test_foo 6 | 7 | export PICCOLO_CONF="tests.cockroach_conf" 8 | python -m pytest \ 9 | --cov=piccolo \ 10 | --cov-report=xml \ 11 | --cov-report=html \ 12 | --cov-fail-under=80 \ 13 | -m "not integration and not cockroach_array_slow" \ 14 | -s $@ 15 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/_quart_endpoints.py.jinja: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import jinja2 4 | 5 | from quart import Response 6 | 7 | ENVIRONMENT = jinja2.Environment( 8 | loader=jinja2.FileSystemLoader( 9 | searchpath=os.path.join(os.path.dirname(__file__), "templates") 10 | ) 11 | ) 12 | 13 | 14 | def index(): 15 | template = ENVIRONMENT.get_template("home.html.jinja") 16 | content = template.render(title="Piccolo + ASGI") 17 | return Response(content) 18 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/_sanic_endpoints.py.jinja: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import jinja2 4 | 5 | from sanic import HTTPResponse 6 | 7 | ENVIRONMENT = jinja2.Environment( 8 | loader=jinja2.FileSystemLoader( 9 | searchpath=os.path.join(os.path.dirname(__file__), "templates") 10 | ) 11 | ) 12 | 13 | 14 | def index(): 15 | template = ENVIRONMENT.get_template("home.html.jinja") 16 | content = template.render(title="Piccolo + ASGI") 17 | return HTTPResponse(content) -------------------------------------------------------------------------------- /docs/src/piccolo/functions/array.rst: -------------------------------------------------------------------------------- 1 | Array functions 2 | =============== 3 | 4 | .. currentmodule:: piccolo.query.functions.array 5 | 6 | ArrayCat 7 | -------- 8 | 9 | .. autoclass:: ArrayCat 10 | 11 | ArrayAppend 12 | ----------- 13 | 14 | .. autoclass:: ArrayAppend 15 | 16 | ArrayPrepend 17 | ------------ 18 | 19 | .. autoclass:: ArrayPrepend 20 | 21 | ArrayRemove 22 | ----------- 23 | 24 | .. autoclass:: ArrayRemove 25 | 26 | ArrayReplace 27 | ------------ 28 | 29 | .. autoclass:: ArrayReplace 30 | -------------------------------------------------------------------------------- /tests/query/test_await.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from tests.base import DBTestCase 4 | from tests.example_apps.music.tables import Band 5 | 6 | 7 | class TestAwait(DBTestCase): 8 | def test_await(self): 9 | """ 10 | Test awaiting a query directly - it should proxy to Query.run(). 11 | """ 12 | 13 | async def get_all(): 14 | return await Band.select() 15 | 16 | response = asyncio.run(get_all()) 17 | 18 | self.assertIsInstance(response, list) 19 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/piccolo_conf.py.jinja: -------------------------------------------------------------------------------- 1 | from piccolo.engine.postgres import PostgresEngine 2 | 3 | from piccolo.conf.apps import AppRegistry 4 | 5 | 6 | DB = PostgresEngine( 7 | config={ 8 | "database": "{{ project_identifier }}", 9 | "user": "postgres", 10 | "password": "", 11 | "host": "localhost", 12 | "port": 5432, 13 | } 14 | ) 15 | 16 | APP_REGISTRY = AppRegistry( 17 | apps=["home.piccolo_app", "piccolo_admin.piccolo_app"] 18 | ) 19 | -------------------------------------------------------------------------------- /scripts/test-strict.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This runs the tests in Python's development mode: 3 | # https://docs.python.org/3/library/devmode.html 4 | # It shows us deprecation warnings, and asyncio warnings. 5 | 6 | # To run all in a folder tests/ 7 | # To run all in a file tests/test_foo.py 8 | # To run all in a class tests/test_foo.py::TestFoo 9 | # To run a single test tests/test_foo.py::TestFoo::test_foo 10 | 11 | export PICCOLO_CONF="tests.postgres_conf" 12 | python -X dev -m pytest -m "not integration" -s $@ 13 | -------------------------------------------------------------------------------- /piccolo_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | This piccolo_conf file is just here so migrations can be made for Piccolo's own 3 | internal apps. 4 | 5 | For example: 6 | 7 | python -m piccolo.main migration new user --auto 8 | 9 | """ 10 | 11 | from piccolo.conf.apps import AppRegistry 12 | from piccolo.engine.postgres import PostgresEngine 13 | 14 | DB = PostgresEngine(config={}) 15 | 16 | 17 | # A list of paths to piccolo apps 18 | # e.g. ['blog.piccolo_app'] 19 | APP_REGISTRY = AppRegistry(apps=["piccolo.apps.user.piccolo_app"]) 20 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | MODULES="piccolo" 5 | SOURCES="$MODULES tests" 6 | 7 | echo "Running isort..." 8 | isort --check $SOURCES 9 | echo "-----" 10 | 11 | echo "Running black..." 12 | black --check $SOURCES 13 | echo "-----" 14 | 15 | echo "Running flake8..." 16 | flake8 $SOURCES 17 | echo "-----" 18 | 19 | echo "Running mypy..." 20 | mypy $SOURCES 21 | echo "-----" 22 | 23 | echo "Running slotscheck..." 24 | python -m slotscheck $MODULES 25 | echo "-----" 26 | 27 | echo "All passed!" 28 | -------------------------------------------------------------------------------- /piccolo/apps/schema/piccolo_app.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppConfig, Command 2 | 3 | from .commands.generate import generate 4 | from .commands.graph import graph 5 | 6 | APP_CONFIG = AppConfig( 7 | app_name="schema", 8 | migrations_folder_path="", 9 | commands=[ 10 | Command(callable=generate, aliases=["gen", "create", "new", "mirror"]), 11 | Command( 12 | callable=graph, 13 | aliases=["map", "visualise", "vizualise", "viz", "vis"], 14 | ), 15 | ], 16 | ) 17 | -------------------------------------------------------------------------------- /tests/apps/project/commands/test_new.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from unittest import TestCase 4 | 5 | from piccolo.apps.project.commands.new import new 6 | 7 | 8 | class TestNewProject(TestCase): 9 | def test_new(self): 10 | root = tempfile.gettempdir() 11 | 12 | file_path = os.path.join(root, "piccolo_conf.py") 13 | 14 | if os.path.exists(file_path): 15 | os.unlink(file_path) 16 | 17 | new(root=root) 18 | 19 | self.assertTrue(os.path.exists(file_path)) 20 | -------------------------------------------------------------------------------- /tests/table/test_repr.py: -------------------------------------------------------------------------------- 1 | from tests.base import DBTestCase 2 | from tests.example_apps.music.tables import Manager 3 | 4 | 5 | class TestTableRepr(DBTestCase): 6 | def test_repr_postgres(self): 7 | self.assertEqual( 8 | Manager().__repr__(), 9 | "", 10 | ) 11 | 12 | self.insert_row() 13 | manager = Manager.objects().first().run_sync() 14 | assert manager is not None 15 | self.assertEqual(manager.__repr__(), f"") 16 | -------------------------------------------------------------------------------- /piccolo/apps/project/commands/templates/piccolo_conf.py.jinja: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppRegistry 2 | {% if engine_name == 'postgres' %} 3 | from piccolo.engine.postgres import PostgresEngine 4 | 5 | 6 | DB = PostgresEngine(config={}) 7 | {% endif %} 8 | {% if engine_name == 'sqlite' %} 9 | from piccolo.engine.sqlite import SQLiteEngine 10 | 11 | 12 | DB = SQLiteEngine(path='project.sqlite') 13 | {% endif %} 14 | 15 | 16 | # A list of paths to piccolo apps 17 | # e.g. ['blog.piccolo_app'] 18 | APP_REGISTRY = AppRegistry(apps=[]) 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/support.yml: -------------------------------------------------------------------------------- 1 | name: Support 💁‍♂️ 2 | description: Raise a support ticket 3 | title: "[Support]: Describe your issue / support requirement here" 4 | labels: [support] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for reaching out! 10 | - type: input 11 | attributes: 12 | label: Support ticket information 13 | description: Please elaborate on your support requirement. 14 | placeholder: I would like to see... 15 | validations: 16 | required: true 17 | -------------------------------------------------------------------------------- /docs/src/piccolo/functions/aggregate.rst: -------------------------------------------------------------------------------- 1 | Aggregate functions 2 | =================== 3 | 4 | .. currentmodule:: piccolo.query.functions.aggregate 5 | 6 | Avg 7 | --- 8 | 9 | .. autoclass:: Avg 10 | :class-doc-from: class 11 | 12 | Count 13 | ----- 14 | 15 | .. autoclass:: Count 16 | :class-doc-from: class 17 | 18 | Min 19 | --- 20 | 21 | .. autoclass:: Min 22 | :class-doc-from: class 23 | 24 | Max 25 | --- 26 | 27 | .. autoclass:: Max 28 | :class-doc-from: class 29 | 30 | Sum 31 | --- 32 | 33 | .. autoclass:: Sum 34 | :class-doc-from: class 35 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/templates/base.html.jinja_raw: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ASGI 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% block content %}{% endblock %} 14 | 15 | 16 | -------------------------------------------------------------------------------- /piccolo/query/functions/base.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from piccolo.columns.base import Column 4 | from piccolo.querystring import QueryString 5 | 6 | 7 | class Function(QueryString): 8 | function_name: str 9 | 10 | def __init__( 11 | self, 12 | identifier: Union[Column, QueryString, str], 13 | alias: Optional[str] = None, 14 | ): 15 | alias = alias or self.__class__.__name__.lower() 16 | 17 | super().__init__( 18 | f"{self.function_name}({{}})", 19 | identifier, 20 | alias=alias, 21 | ) 22 | -------------------------------------------------------------------------------- /piccolo/engine/finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional 4 | 5 | from piccolo.engine.base import Engine 6 | 7 | 8 | def engine_finder(module_name: Optional[str] = None) -> Optional[Engine]: 9 | """ 10 | An example module name is `my_piccolo_conf`. 11 | 12 | The value used is determined by: 13 | module_name argument > environment variable > default. 14 | 15 | The module must be available on the path, so Python can import it. 16 | """ 17 | from piccolo.conf.apps import Finder 18 | 19 | return Finder().get_engine(module_name=module_name) 20 | -------------------------------------------------------------------------------- /tests/columns/test_real.py: -------------------------------------------------------------------------------- 1 | from piccolo.columns.column_types import Real 2 | from piccolo.table import Table 3 | from piccolo.testing.test_case import TableTest 4 | 5 | 6 | class MyTable(Table): 7 | column_a = Real() 8 | 9 | 10 | class TestReal(TableTest): 11 | tables = [MyTable] 12 | 13 | def test_creation(self): 14 | row = MyTable(column_a=1.23) 15 | row.save().run_sync() 16 | 17 | _row = MyTable.objects().first().run_sync() 18 | assert _row is not None 19 | self.assertEqual(type(_row.column_a), float) 20 | self.assertAlmostEqual(_row.column_a, 1.23) 21 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/conftest.py.jinja: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from piccolo.utils.warnings import colored_warning 5 | 6 | 7 | def pytest_configure(*args): 8 | if os.environ.get("PICCOLO_TEST_RUNNER") != "True": 9 | colored_warning( 10 | "\n\n" 11 | "We recommend running Piccolo tests using the " 12 | "`piccolo tester run` command, which wraps Pytest, and makes " 13 | "sure the test database is being used. " 14 | "To stop this warning, modify conftest.py." 15 | "\n\n" 16 | ) 17 | sys.exit(1) 18 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/_falcon_endpoints.py.jinja: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import falcon 4 | import jinja2 5 | 6 | ENVIRONMENT = jinja2.Environment( 7 | loader=jinja2.FileSystemLoader( 8 | searchpath=os.path.join(os.path.dirname(__file__), "templates") 9 | ) 10 | ) 11 | 12 | 13 | class HomeEndpoint: 14 | async def on_get(self, req, resp): 15 | template = ENVIRONMENT.get_template("home.html.jinja") 16 | content = template.render(title="Piccolo + ASGI",) 17 | resp.status = falcon.HTTP_200 18 | resp.content_type = "text/html" 19 | resp.text = content 20 | -------------------------------------------------------------------------------- /tests/query/mixins/test_order_by_delegate.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.query.mixins import OrderByDelegate 4 | 5 | 6 | class TestOrderByDelegate(TestCase): 7 | def test_no_columns(self): 8 | """ 9 | An exception should be raised if no columns are passed in. 10 | """ 11 | delegate = OrderByDelegate() 12 | 13 | with self.assertRaises(ValueError) as manager: 14 | delegate.order_by() 15 | 16 | self.assertEqual( 17 | manager.exception.__str__(), 18 | "At least one column must be passed to order_by.", 19 | ) 20 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_clauses/first.rst: -------------------------------------------------------------------------------- 1 | .. _first: 2 | 3 | first 4 | ===== 5 | 6 | You can use ``first`` clauses with the following queries: 7 | 8 | * :ref:`Objects` 9 | * :ref:`Select` 10 | 11 | Rather than returning a list of results, just the first result is returned. 12 | 13 | .. code-block:: python 14 | 15 | >>> await Band.select().first() 16 | {'name': 'Pythonistas', 'manager': 1, 'popularity': 1000, 'id': 1} 17 | 18 | Likewise, with objects: 19 | 20 | .. code-block:: python 21 | 22 | >>> await Band.objects().first() 23 | 24 | 25 | If no match is found, then ``None`` is returned instead. 26 | -------------------------------------------------------------------------------- /tests/query/functions/base.py: -------------------------------------------------------------------------------- 1 | from piccolo.testing.test_case import TableTest 2 | from tests.example_apps.music.tables import Band, Manager 3 | 4 | 5 | class BandTest(TableTest): 6 | tables = [Band, Manager] 7 | 8 | def setUp(self) -> None: 9 | super().setUp() 10 | 11 | manager = Manager({Manager.name: "Guido"}) 12 | manager.save().run_sync() 13 | 14 | band = Band( 15 | { 16 | Band.name: "Pythonistas", 17 | Band.manager: manager, 18 | Band.popularity: 1000, 19 | } 20 | ) 21 | band.save().run_sync() 22 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/_lilya_endpoints.py.jinja: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import jinja2 4 | from lilya.controllers import Controller 5 | from lilya.responses import HTMLResponse 6 | 7 | 8 | ENVIRONMENT = jinja2.Environment( 9 | loader=jinja2.FileSystemLoader( 10 | searchpath=os.path.join(os.path.dirname(__file__), "templates") 11 | ) 12 | ) 13 | 14 | 15 | class HomeController(Controller): 16 | async def get(self, request): 17 | template = ENVIRONMENT.get_template("home.html.jinja") 18 | 19 | content = template.render(title="Piccolo + ASGI",) 20 | 21 | return HTMLResponse(content) 22 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/_esmerald_endpoints.py.jinja: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import jinja2 4 | from esmerald import Request, Response, get 5 | from esmerald.responses import HTMLResponse 6 | 7 | ENVIRONMENT = jinja2.Environment( 8 | loader=jinja2.FileSystemLoader( 9 | searchpath=os.path.join(os.path.dirname(__file__), "templates") 10 | ) 11 | ) 12 | 13 | 14 | @get(path="/", include_in_schema=False) 15 | def home(request: Request) -> HTMLResponse: 16 | template = ENVIRONMENT.get_template("home.html.jinja") 17 | 18 | content = template.render(title="Piccolo + ASGI",) 19 | 20 | return HTMLResponse(content) 21 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/_starlette_endpoints.py.jinja: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import jinja2 4 | from starlette.endpoints import HTTPEndpoint 5 | from starlette.responses import HTMLResponse 6 | 7 | 8 | ENVIRONMENT = jinja2.Environment( 9 | loader=jinja2.FileSystemLoader( 10 | searchpath=os.path.join(os.path.dirname(__file__), "templates") 11 | ) 12 | ) 13 | 14 | 15 | class HomeEndpoint(HTTPEndpoint): 16 | async def get(self, request): 17 | template = ENVIRONMENT.get_template("home.html.jinja") 18 | 19 | content = template.render(title="Piccolo + ASGI",) 20 | 21 | return HTMLResponse(content) 22 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/piccolo_app.py.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Import all of the Tables subclasses in your app here, and register them with 3 | the APP_CONFIG. 4 | """ 5 | 6 | import os 7 | 8 | from piccolo.conf.apps import AppConfig, table_finder 9 | 10 | 11 | CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) 12 | 13 | 14 | APP_CONFIG = AppConfig( 15 | app_name="home", 16 | migrations_folder_path=os.path.join( 17 | CURRENT_DIRECTORY, "piccolo_migrations" 18 | ), 19 | table_classes=table_finder(modules=["home.tables"], exclude_imported=True), 20 | migration_dependencies=[], 21 | commands=[], 22 | ) 23 | -------------------------------------------------------------------------------- /tests/columns/test_double_precision.py: -------------------------------------------------------------------------------- 1 | from piccolo.columns.column_types import DoublePrecision 2 | from piccolo.table import Table 3 | from piccolo.testing.test_case import TableTest 4 | 5 | 6 | class MyTable(Table): 7 | column_a = DoublePrecision() 8 | 9 | 10 | class TestDoublePrecision(TableTest): 11 | tables = [MyTable] 12 | 13 | def test_creation(self): 14 | row = MyTable(column_a=1.23) 15 | row.save().run_sync() 16 | 17 | _row = MyTable.objects().first().run_sync() 18 | assert _row is not None 19 | self.assertEqual(type(_row.column_a), float) 20 | self.assertAlmostEqual(_row.column_a, 1.23) 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | release: 10 | name: "Publish release" 11 | runs-on: "ubuntu-latest" 12 | 13 | steps: 14 | - uses: "actions/checkout@v3" 15 | - uses: "actions/setup-python@v5" 16 | with: 17 | python-version: 3.13 18 | - name: "Install dependencies" 19 | run: "pip install -r requirements/dev-requirements.txt" 20 | - name: "Publish to PyPI" 21 | run: "./scripts/release.sh" 22 | env: 23 | TWINE_USERNAME: __token__ 24 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = src 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /tests/apps/app/commands/test_show_all.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock, call, patch 3 | 4 | from piccolo.apps.app.commands.show_all import show_all 5 | 6 | 7 | class TestShowAll(TestCase): 8 | @patch("piccolo.apps.app.commands.show_all.print") 9 | def test_show_all(self, print_: MagicMock): 10 | show_all() 11 | 12 | self.assertEqual( 13 | print_.mock_calls, 14 | [ 15 | call("Registered apps:"), 16 | call("tests.example_apps.music.piccolo_app"), 17 | call("tests.example_apps.mega.piccolo_app"), 18 | ], 19 | ) 20 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/_blacksheep_endpoints.py.jinja: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from blacksheep.contents import Content 4 | from blacksheep.messages import Response 5 | import jinja2 6 | 7 | 8 | ENVIRONMENT = jinja2.Environment( 9 | loader=jinja2.FileSystemLoader( 10 | searchpath=os.path.join(os.path.dirname(__file__), "templates") 11 | ) 12 | ) 13 | 14 | 15 | async def home(request): 16 | template = ENVIRONMENT.get_template("home.html.jinja") 17 | content = template.render(title="Piccolo + ASGI",) 18 | return Response( 19 | 200, 20 | content=Content(b"text/html", bytes(content, encoding='utf8')) 21 | ) 22 | -------------------------------------------------------------------------------- /tests/postgres_conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from piccolo.conf.apps import AppRegistry 4 | from piccolo.engine.postgres import PostgresEngine 5 | 6 | DB = PostgresEngine( 7 | config={ 8 | "host": os.environ.get("PG_HOST", "localhost"), 9 | "port": os.environ.get("PG_PORT", "5432"), 10 | "user": os.environ.get("PG_USER", "postgres"), 11 | "password": os.environ.get("PG_PASSWORD", ""), 12 | "database": os.environ.get("PG_DATABASE", "piccolo"), 13 | } 14 | ) 15 | 16 | 17 | APP_REGISTRY = AppRegistry( 18 | apps=[ 19 | "tests.example_apps.music.piccolo_app", 20 | "tests.example_apps.mega.piccolo_app", 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /tests/cockroach_conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from piccolo.conf.apps import AppRegistry 4 | from piccolo.engine.cockroach import CockroachEngine 5 | 6 | DB = CockroachEngine( 7 | config={ 8 | "host": os.environ.get("PG_HOST", "localhost"), 9 | "port": os.environ.get("PG_PORT", "26257"), 10 | "user": os.environ.get("PG_USER", "root"), 11 | "password": os.environ.get("PG_PASSWORD", ""), 12 | "database": os.environ.get("PG_DATABASE", "piccolo"), 13 | } 14 | ) 15 | 16 | 17 | APP_REGISTRY = AppRegistry( 18 | apps=[ 19 | "tests.example_apps.music.piccolo_app", 20 | "tests.example_apps.mega.piccolo_app", 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/main.py.jinja: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | {% if server == 'uvicorn' %} 3 | import uvicorn 4 | 5 | uvicorn.run('app:app', reload=True) 6 | {% elif server == 'Hypercorn' %} 7 | import asyncio 8 | 9 | from hypercorn.asyncio import serve 10 | from hypercorn.config import Config 11 | 12 | from app import app 13 | 14 | 15 | class CustomConfig(Config): 16 | use_reloader = True 17 | 18 | asyncio.run(serve(app, CustomConfig())) 19 | 20 | serve(app) 21 | {% elif server == 'granian' %} 22 | import granian 23 | 24 | granian.Granian("app:app", interface="asgi").serve() 25 | {% endif %} 26 | -------------------------------------------------------------------------------- /piccolo/apps/migrations/piccolo_app.py: -------------------------------------------------------------------------------- 1 | from piccolo.conf.apps import AppConfig, Command 2 | 3 | from .commands.backwards import backwards 4 | from .commands.check import check 5 | from .commands.clean import clean 6 | from .commands.forwards import forwards 7 | from .commands.new import new 8 | 9 | APP_CONFIG = AppConfig( 10 | app_name="migrations", 11 | migrations_folder_path="", 12 | commands=[ 13 | Command(callable=backwards, aliases=["b", "back", "backward"]), 14 | Command(callable=check), 15 | Command(callable=clean), 16 | Command(callable=forwards, aliases=["f", "forward"]), 17 | Command(callable=new, aliases=["n", "create"]), 18 | ], 19 | ) 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | v1 is actively maintained, and any security vulnerabilities will be patched. 6 | 7 | v0.X will have any major security vulnerabilities patched. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | We recommend opening a security advisory on GitHub, as per the [documentation](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability). 12 | 13 | Alternatively, reach out to the maintainers via email (see [setup.py](https://github.com/piccolo-orm/piccolo/blob/bbd2e4ad6378b2080d58fb7c7ed392f0425f0f21/setup.py#L60) for contact details). 14 | -------------------------------------------------------------------------------- /docs/src/piccolo/getting_started/setup_sqlite.rst: -------------------------------------------------------------------------------- 1 | .. _set_up_sqlite: 2 | 3 | Setup SQLite 4 | ============ 5 | 6 | Installation 7 | ------------ 8 | 9 | The good news is SQLite is good to go out of the box with Python. 10 | 11 | Some Piccolo features are only available with newer SQLite versions. 12 | 13 | .. _check_sqlite_version: 14 | 15 | Check version 16 | ------------- 17 | 18 | To check which SQLite version you're using, simply open a Python terminal, and 19 | do the following: 20 | 21 | .. code-block:: python 22 | 23 | >>> import sqlite3 24 | >>> sqlite3.sqlite_version 25 | '3.39.0' 26 | 27 | The easiest way to upgrade your SQLite version is to install the latest version 28 | of Python. 29 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/_litestar_endpoints.py.jinja: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import jinja2 4 | from litestar import MediaType, Request, Response, get 5 | 6 | ENVIRONMENT = jinja2.Environment( 7 | loader=jinja2.FileSystemLoader( 8 | searchpath=os.path.join(os.path.dirname(__file__), "templates") 9 | ) 10 | ) 11 | 12 | 13 | @get(path="/", include_in_schema=False, sync_to_thread=False) 14 | def home(request: Request) -> Response: 15 | template = ENVIRONMENT.get_template("home.html.jinja") 16 | content = template.render(title="Piccolo + ASGI") 17 | return Response( 18 | content, 19 | media_type=MediaType.HTML, 20 | status_code=200, 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /tests/example_apps/music/piccolo_migrations/music_2024_06_19t18_11_05_793132.py: -------------------------------------------------------------------------------- 1 | from piccolo.apps.migrations.auto.migration_manager import MigrationManager 2 | 3 | ID = "2024-06-19T18:11:05:793132" 4 | VERSION = "1.11.0" 5 | DESCRIPTION = "An example fake migration" 6 | 7 | 8 | async def forwards(): 9 | manager = MigrationManager( 10 | migration_id=ID, app_name="", description=DESCRIPTION, fake=True 11 | ) 12 | 13 | def run(): 14 | # This should never run, as this migrations is `fake=True`. It's here 15 | # for testing purposes (to make sure it never gets triggered). 16 | print("Running fake migration") 17 | 18 | manager.add_raw(run) 19 | 20 | return manager 21 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_clauses/offset.rst: -------------------------------------------------------------------------------- 1 | .. _offset: 2 | 3 | offset 4 | ====== 5 | 6 | You can use ``offset`` clauses with the following queries: 7 | 8 | * :ref:`Objects` 9 | * :ref:`Select` 10 | 11 | This will omit the first X rows from the response. 12 | 13 | It's highly recommended to use it along with an :ref:`order_by` clause, 14 | otherwise the results returned could be different each time. 15 | 16 | .. code-block:: python 17 | 18 | >>> await Band.select(Band.name).offset(1).order_by(Band.name) 19 | [{'name': 'Pythonistas'}, {'name': 'Rustaceans'}] 20 | 21 | Likewise, with objects: 22 | 23 | .. code-block:: python 24 | 25 | >>> await Band.objects().offset(1).order_by(Band.name) 26 | [Band2, Band3] 27 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Development Scripts 2 | 3 | The scripts follow GitHub's ["Scripts to Rule Them All"](https://github.com/github/scripts-to-rule-them-all). 4 | 5 | Call them from the root of the project, e.g. `./scripts/lint.sh`. 6 | 7 | - `scripts/format.sh` - Format the code to the required standards. 8 | - `scripts/lint.sh` - Run the automated code linting/formatting tools. 9 | - `scripts/piccolo.sh` - Run the Piccolo CLI on the example project in the `tests` folder. 10 | - `scripts/profile.sh` - Run a profiler to test performance. 11 | - `scripts/release.sh` - Publish package to PyPI. 12 | - `scripts/test-postgres.sh` - Run the test suite with Postgres. 13 | - `scripts/test-sqlite.sh` - Run the test suite with SQLite. 14 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_clauses/index.rst: -------------------------------------------------------------------------------- 1 | .. _QueryClauses: 2 | 3 | Query Clauses 4 | ============= 5 | 6 | Query clauses are used to modify a query by making it more specific, or 7 | by modifying the return values. 8 | 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | :caption: Essential 13 | 14 | ./first 15 | ./limit 16 | ./order_by 17 | ./where 18 | 19 | .. toctree:: 20 | :maxdepth: 1 21 | :caption: Advanced 22 | 23 | ./batch 24 | ./callback 25 | ./distinct 26 | ./freeze 27 | ./group_by 28 | ./lock_rows 29 | ./offset 30 | ./on_conflict 31 | ./output 32 | ./returning 33 | 34 | .. toctree:: 35 | :maxdepth: 1 36 | :caption: CockroachDB 37 | 38 | ./as_of 39 | -------------------------------------------------------------------------------- /docs/src/piccolo/functions/string.rst: -------------------------------------------------------------------------------- 1 | String functions 2 | ================ 3 | 4 | .. currentmodule:: piccolo.query.functions.string 5 | 6 | Concat 7 | ------ 8 | 9 | .. autoclass:: Concat 10 | 11 | Length 12 | ------ 13 | 14 | .. autoclass:: Length 15 | :class-doc-from: class 16 | 17 | Lower 18 | ----- 19 | 20 | .. autoclass:: Lower 21 | :class-doc-from: class 22 | 23 | Ltrim 24 | ----- 25 | 26 | .. autoclass:: Ltrim 27 | :class-doc-from: class 28 | 29 | Reverse 30 | ------- 31 | 32 | .. autoclass:: Reverse 33 | :class-doc-from: class 34 | 35 | Rtrim 36 | ----- 37 | 38 | .. autoclass:: Rtrim 39 | :class-doc-from: class 40 | 41 | Upper 42 | ----- 43 | 44 | .. autoclass:: Upper 45 | :class-doc-from: class 46 | -------------------------------------------------------------------------------- /tests/utils/test_naming.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.utils.naming import _camel_to_snake 4 | 5 | 6 | class TestCamelToSnake(TestCase): 7 | def test_converting_tablenames(self): 8 | """ 9 | Make sure Table names are converted correctly. 10 | """ 11 | self.assertEqual(_camel_to_snake("HelloWorld"), "hello_world") 12 | self.assertEqual(_camel_to_snake("Manager1"), "manager1") 13 | self.assertEqual(_camel_to_snake("ManagerAbc"), "manager_abc") 14 | self.assertEqual(_camel_to_snake("ManagerABC"), "manager_abc") 15 | self.assertEqual(_camel_to_snake("ManagerABCFoo"), "manager_abc_foo") 16 | self.assertEqual(_camel_to_snake("ManagerA"), "manager_a") 17 | -------------------------------------------------------------------------------- /docs/src/piccolo/tutorials/avoiding_circular_imports_src/tables.py: -------------------------------------------------------------------------------- 1 | # tables.py 2 | 3 | from piccolo.columns import ForeignKey, Varchar 4 | from piccolo.table import Table 5 | from piccolo.utils.pydantic import create_pydantic_model 6 | 7 | 8 | class Band(Table): 9 | name = Varchar() 10 | # This automatically gets converted into a LazyTableReference, because a 11 | # string is passed in: 12 | manager = ForeignKey("Manager") 13 | 14 | 15 | # This is not recommended, as it will cause the LazyTableReference to be 16 | # evaluated before Manager has imported. 17 | # Instead, move this to a separate file, or below Manager. 18 | BandModel = create_pydantic_model(Band) 19 | 20 | 21 | class Manager(Table): 22 | name = Varchar() 23 | -------------------------------------------------------------------------------- /tests/apps/migrations/commands/test_migrations/2020-03-31T20-38-22.py: -------------------------------------------------------------------------------- 1 | from piccolo.apps.migrations.auto import MigrationManager 2 | 3 | ID = "2020-03-31T20:38:22" 4 | 5 | 6 | async def forwards(): 7 | manager = MigrationManager(migration_id=ID) 8 | manager.add_table("Band", tablename="band") 9 | manager.add_column( 10 | table_class_name="Band", 11 | tablename="band", 12 | column_name="name", 13 | column_class_name="Varchar", 14 | params={ 15 | "length": 150, 16 | "default": "", 17 | "null": True, 18 | "primary_key": False, 19 | "unique": False, 20 | "index": False, 21 | }, 22 | ) 23 | 24 | return manager 25 | -------------------------------------------------------------------------------- /tests/example_apps/music/piccolo_migrations/2021-11-13T14-01-46-114725.py: -------------------------------------------------------------------------------- 1 | from piccolo.apps.migrations.auto import MigrationManager 2 | from piccolo.columns.column_types import Integer 3 | 4 | ID = "2021-11-13T14:01:46:114725" 5 | VERSION = "0.59.0" 6 | DESCRIPTION = "" 7 | 8 | 9 | async def forwards(): 10 | manager = MigrationManager( 11 | migration_id=ID, app_name="music", description=DESCRIPTION 12 | ) 13 | 14 | manager.alter_column( 15 | table_class_name="Venue", 16 | tablename="venue", 17 | column_name="capacity", 18 | params={"secret": True}, 19 | old_params={"secret": False}, 20 | column_class=Integer, 21 | old_column_class=Integer, 22 | ) 23 | 24 | return manager 25 | -------------------------------------------------------------------------------- /piccolo/apps/app/commands/templates/piccolo_app.py.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Import all of the Tables subclasses in your app here, and register them with 3 | the APP_CONFIG. 4 | """ 5 | 6 | import os 7 | 8 | from piccolo.conf.apps import AppConfig, table_finder, get_package 9 | 10 | 11 | CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) 12 | 13 | 14 | APP_CONFIG = AppConfig( 15 | app_name='{{ app_name }}', 16 | migrations_folder_path=os.path.join( 17 | CURRENT_DIRECTORY, 18 | 'piccolo_migrations' 19 | ), 20 | table_classes=table_finder( 21 | modules=[".tables"], 22 | package=get_package(__name__), 23 | exclude_imported=True 24 | ), 25 | migration_dependencies=[], 26 | commands=[] 27 | ) 28 | -------------------------------------------------------------------------------- /docs/src/piccolo/functions/custom.rst: -------------------------------------------------------------------------------- 1 | Custom functions 2 | ================ 3 | 4 | If there's a database function which Piccolo doesn't provide out of the box, 5 | you can still easily access it by using :class:`QueryString ` 6 | directly. 7 | 8 | QueryString 9 | ----------- 10 | 11 | :class:`QueryString ` is the building block of 12 | queries in Piccolo. 13 | 14 | If we have a custom function defined in the database called ``slugify``, you 15 | can access it like this: 16 | 17 | .. code-block:: python 18 | 19 | from piccolo.querystring import QueryString 20 | 21 | await Band.select( 22 | Band.name, 23 | QueryString('slugify({})', Band.name, alias='name_slug') 24 | ) 25 | -------------------------------------------------------------------------------- /tests/example_apps/music/piccolo_migrations/2020-12-17T18-44-44.py: -------------------------------------------------------------------------------- 1 | from piccolo.apps.migrations.auto import MigrationManager 2 | 3 | ID = "2020-12-17T18:44:44" 4 | VERSION = "0.14.7" 5 | 6 | 7 | async def forwards(): 8 | manager = MigrationManager(migration_id=ID, app_name="music") 9 | 10 | manager.add_table("Poster", tablename="poster") 11 | 12 | manager.add_column( 13 | table_class_name="Poster", 14 | tablename="poster", 15 | column_name="content", 16 | column_class_name="Text", 17 | params={ 18 | "default": "", 19 | "null": False, 20 | "primary_key": False, 21 | "unique": False, 22 | "index": False, 23 | }, 24 | ) 25 | 26 | return manager 27 | -------------------------------------------------------------------------------- /tests/utils/test_printing.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.utils.printing import get_fixed_length_string 4 | 5 | 6 | class TestGetFixedLengthString(TestCase): 7 | def test_extra_padding(self): 8 | """ 9 | Make sure the additional padding is added. 10 | """ 11 | result = get_fixed_length_string(string="hello", length=10) 12 | self.assertEqual(result, "hello ") 13 | 14 | def test_truncation(self): 15 | """ 16 | Make sure the string is truncated to the fixed length if it's too long. 17 | """ 18 | result = get_fixed_length_string( 19 | string="this is a very, very long string", length=20 20 | ) 21 | self.assertEqual(result, "this is a very, v...") 22 | -------------------------------------------------------------------------------- /docs/src/piccolo/functions/type_conversion.rst: -------------------------------------------------------------------------------- 1 | Type conversion functions 2 | ========================= 3 | 4 | Cast 5 | ---- 6 | 7 | .. currentmodule:: piccolo.query.functions.type_conversion 8 | 9 | .. autoclass:: Cast 10 | 11 | Notes on databases 12 | ------------------ 13 | 14 | Postgres and CockroachDB have very rich type systems, and you can convert 15 | between most types. SQLite is more limited. 16 | 17 | The following query will work in Postgres / Cockroach, but you might get 18 | unexpected results in SQLite, because it doesn't have a native ``TIME`` column 19 | type: 20 | 21 | .. code-block:: python 22 | 23 | >>> from piccolo.columns import Time 24 | >>> from piccolo.query.functions import Cast 25 | >>> await Concert.select(Cast(Concert.starts, Time())) 26 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/home/endpoints.py.jinja: -------------------------------------------------------------------------------- 1 | {% if router in ['fastapi', 'starlette'] %} 2 | {% include '_starlette_endpoints.py.jinja' %} 3 | {% elif router == 'blacksheep' %} 4 | {% include '_blacksheep_endpoints.py.jinja' %} 5 | {% elif router == 'litestar' %} 6 | {% include '_litestar_endpoints.py.jinja' %} 7 | {% elif router == 'esmerald' %} 8 | {% include '_esmerald_endpoints.py.jinja' %} 9 | {% elif router == 'lilya' %} 10 | {% include '_lilya_endpoints.py.jinja' %} 11 | {% elif router == 'quart' %} 12 | {% include '_quart_endpoints.py.jinja' %} 13 | {% elif router == 'falcon' %} 14 | {% include '_falcon_endpoints.py.jinja' %} 15 | {% elif router == 'sanic' %} 16 | {% include '_sanic_endpoints.py.jinja' %} 17 | {% endif %} 18 | -------------------------------------------------------------------------------- /piccolo/query/methods/__init__.py: -------------------------------------------------------------------------------- 1 | from .alter import Alter 2 | from .count import Count 3 | from .create import Create 4 | from .create_index import CreateIndex 5 | from .delete import Delete 6 | from .drop_index import DropIndex 7 | from .exists import Exists 8 | from .insert import Insert 9 | from .objects import Objects 10 | from .raw import Raw 11 | from .refresh import Refresh 12 | from .select import Select 13 | from .table_exists import TableExists 14 | from .update import Update 15 | 16 | __all__ = ( 17 | "Alter", 18 | "Count", 19 | "Create", 20 | "CreateIndex", 21 | "Delete", 22 | "DropIndex", 23 | "Exists", 24 | "Insert", 25 | "Objects", 26 | "Raw", 27 | "Refresh", 28 | "Select", 29 | "TableExists", 30 | "Update", 31 | ) 32 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/app.py.jinja: -------------------------------------------------------------------------------- 1 | {% if router == 'fastapi' %} 2 | {% include '_fastapi_app.py.jinja' %} 3 | {% elif router == 'starlette' %} 4 | {% include '_starlette_app.py.jinja' %} 5 | {% elif router == 'blacksheep' %} 6 | {% include '_blacksheep_app.py.jinja' %} 7 | {% elif router == 'litestar' %} 8 | {% include '_litestar_app.py.jinja' %} 9 | {% elif router == 'esmerald' %} 10 | {% include '_esmerald_app.py.jinja' %} 11 | {% elif router == 'lilya' %} 12 | {% include '_lilya_app.py.jinja' %} 13 | {% elif router == 'quart' %} 14 | {% include '_quart_app.py.jinja' %} 15 | {% elif router == 'falcon' %} 16 | {% include '_falcon_app.py.jinja' %} 17 | {% elif router == 'sanic' %} 18 | {% include '_sanic_app.py.jinja' %} 19 | {% endif %} 20 | -------------------------------------------------------------------------------- /tests/table/test_all_columns.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from tests.example_apps.music.tables import Band 4 | 5 | 6 | class TestAllColumns(TestCase): 7 | def test_all_columns(self): 8 | self.assertEqual( 9 | Band.all_columns(), 10 | [Band.id, Band.name, Band.manager, Band.popularity], 11 | ) 12 | self.assertEqual(Band.all_columns(), Band._meta.columns) 13 | 14 | def test_all_columns_excluding(self): 15 | self.assertEqual( 16 | Band.all_columns(exclude=[Band.id]), 17 | [Band.name, Band.manager, Band.popularity], 18 | ) 19 | 20 | self.assertEqual( 21 | Band.all_columns(exclude=["id"]), 22 | [Band.name, Band.manager, Band.popularity], 23 | ) 24 | -------------------------------------------------------------------------------- /piccolo/apps/user/commands/change_password.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from piccolo.apps.user.commands.create import ( 4 | get_confirmed_password, 5 | get_password, 6 | get_username, 7 | ) 8 | from piccolo.apps.user.tables import BaseUser 9 | 10 | 11 | def change_password(): 12 | """ 13 | Change a user's password. 14 | """ 15 | username = get_username() 16 | password = get_password() 17 | confirmed_password = get_confirmed_password() 18 | 19 | if password != confirmed_password: 20 | sys.exit("Passwords don't match!") 21 | 22 | BaseUser.update_password_sync(user=username, password=password) 23 | 24 | print(f"Updated password for {username}") 25 | print( 26 | "If using session auth, we recommend invalidating this user's session." 27 | ) 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/issues.yml: -------------------------------------------------------------------------------- 1 | name: Bug 🐛 2 | description: Report a bug 3 | title: "[Issue]: Describe the bug here" 4 | labels: ["goal:fix, priority:medium"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: input 11 | attributes: 12 | label: Bug description 13 | description: Can you please describe the bug in more detail? 14 | placeholder: Tell us what you found. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: multimedia 19 | attributes: 20 | label: Relevant log output 21 | description: Please paste any relevant log output. 22 | placeholder: Please upload your multimedia (screenshots and video) here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/features.yml: -------------------------------------------------------------------------------- 1 | name: Features 💡 2 | description: Suggest a new feature 3 | title: "[Feature]: Describe your feature idea here" 4 | labels: ["goal:addition"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this suggestion form! 10 | - type: input 11 | attributes: 12 | label: Suggestion 13 | description: Can you please elaborate on your suggestion? 14 | placeholder: I would like to see... 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: multimedia 19 | attributes: 20 | label: Relevant media 21 | description: You can add any media which helps explain your idea. 22 | placeholder: Please upload your multimedia (screenshots and video) here. 23 | -------------------------------------------------------------------------------- /tests/query/test_gather.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from tests.base import DBTestCase 4 | from tests.example_apps.music.tables import Manager 5 | 6 | 7 | class TestAwait(DBTestCase): 8 | def test_await(self): 9 | """ 10 | Make sure that asyncio.gather works with the main query types. 11 | """ 12 | 13 | async def run_queries(): 14 | return await asyncio.gather( 15 | Manager.select(), 16 | Manager.insert(Manager(name="Golangs")), 17 | Manager.delete().where(Manager.name != "Golangs"), 18 | Manager.objects(), 19 | Manager.count(), 20 | Manager.raw("SELECT * FROM manager"), 21 | ) 22 | 23 | # No exceptions should be raised. 24 | self.assertIsInstance(asyncio.run(run_queries()), list) 25 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_clauses/as_of.rst: -------------------------------------------------------------------------------- 1 | .. _as_of: 2 | 3 | as_of 4 | ===== 5 | 6 | .. note:: Cockroach only. 7 | 8 | You can use ``as_of`` clause with the following queries: 9 | 10 | * :ref:`Select` 11 | * :ref:`Objects` 12 | 13 | To retrieve historical data from 5 minutes ago: 14 | 15 | .. code-block:: python 16 | 17 | await Band.select().where( 18 | Band.name == 'Pythonistas' 19 | ).as_of('-5min') 20 | 21 | This generates an ``AS OF SYSTEM TIME`` clause. See `documentation `_. 22 | 23 | This clause accepts a wide variety of time and interval `string formats `_. 24 | 25 | This is very useful for performance, as it will reduce transaction contention across a cluster. 26 | -------------------------------------------------------------------------------- /piccolo/apps/migrations/commands/templates/migration.py.jinja: -------------------------------------------------------------------------------- 1 | from piccolo.apps.migrations.auto.migration_manager import MigrationManager 2 | {% for extra_import in extra_imports -%} 3 | {{ extra_import }} 4 | {% endfor %} 5 | 6 | 7 | {% for extra_definition in extra_definitions -%} 8 | {{ extra_definition }} 9 | {% endfor %} 10 | 11 | 12 | ID = '{{ migration_id }}' 13 | VERSION = '{{ version }}' 14 | DESCRIPTION = '{{ description }}' 15 | 16 | 17 | async def forwards(): 18 | manager = MigrationManager(migration_id=ID, app_name="{{ app_name }}", description=DESCRIPTION) 19 | {% if auto %} 20 | {% for alter_statement in alter_statements %} 21 | {{ alter_statement }} 22 | {% endfor %} 23 | {% else %} 24 | def run(): 25 | print(f"running {ID}") 26 | 27 | manager.add_raw(run) 28 | {% endif %} 29 | return manager 30 | -------------------------------------------------------------------------------- /tests/columns/test_varchar.py: -------------------------------------------------------------------------------- 1 | from piccolo.columns.column_types import Varchar 2 | from piccolo.table import Table 3 | from piccolo.testing.test_case import TableTest 4 | from tests.base import engines_only 5 | 6 | 7 | class MyTable(Table): 8 | name = Varchar(length=10) 9 | 10 | 11 | @engines_only("postgres", "cockroach") 12 | class TestVarchar(TableTest): 13 | """ 14 | SQLite doesn't enforce any constraints on max character length. 15 | 16 | https://www.sqlite.org/faq.html#q9 17 | 18 | Might consider enforcing this at the ORM level instead in the future. 19 | """ 20 | 21 | tables = [MyTable] 22 | 23 | def test_length(self): 24 | row = MyTable(name="bob") 25 | row.save().run_sync() 26 | 27 | with self.assertRaises(Exception): 28 | row.name = "bob123456789" 29 | row.save().run_sync() 30 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_types/raw.rst: -------------------------------------------------------------------------------- 1 | .. _Raw: 2 | 3 | Raw 4 | === 5 | 6 | Should you need to, you can execute raw SQL. 7 | 8 | .. code-block:: python 9 | 10 | >>> await Band.raw('SELECT name FROM band') 11 | [{'name': 'Pythonistas'}] 12 | 13 | It's recommended that you parameterise any values. Use curly braces ``{}`` as 14 | placeholders: 15 | 16 | .. code-block:: python 17 | 18 | >>> await Band.raw('SELECT * FROM band WHERE name = {}', 'Pythonistas') 19 | [{'name': 'Pythonistas', 'manager': 1, 'popularity': 1000, 'id': 1}] 20 | 21 | .. warning:: Be careful to avoid SQL injection attacks. Don't add any user submitted data into your SQL strings, unless it's parameterised. 22 | 23 | 24 | ------------------------------------------------------------------------------- 25 | 26 | Query clauses 27 | ------------- 28 | 29 | batch 30 | ~~~~~ 31 | 32 | See :ref:`batch`. 33 | -------------------------------------------------------------------------------- /piccolo/columns/choices.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Any 5 | 6 | 7 | @dataclass 8 | class Choice: 9 | """ 10 | When defining enums for ``Column`` choices, they can either be defined 11 | like: 12 | 13 | .. code-block:: python 14 | 15 | class Title(Enum): 16 | mr = 1 17 | mrs = 2 18 | 19 | If using Piccolo Admin, the values shown will be ``Mr`` and ``Mrs``. If you 20 | want more control, you can use ``Choice`` for the value instead. 21 | 22 | .. code-block:: python 23 | 24 | class Title(Enum): 25 | mr = Choice(value=1, display_name="Mr.") 26 | mrs = Choice(value=1, display_name="Mrs.") 27 | 28 | Now the values shown will be ``Mr.`` and ``Mrs.``. 29 | 30 | """ 31 | 32 | value: Any 33 | display_name: str 34 | -------------------------------------------------------------------------------- /tests/columns/test_numeric.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from piccolo.columns.column_types import Numeric 4 | from piccolo.table import Table 5 | from piccolo.testing.test_case import TableTest 6 | 7 | 8 | class MyTable(Table): 9 | column_a = Numeric() 10 | column_b = Numeric(digits=(3, 2)) 11 | 12 | 13 | class TestNumeric(TableTest): 14 | tables = [MyTable] 15 | 16 | def test_creation(self): 17 | row = MyTable(column_a=Decimal(1.23), column_b=Decimal(1.23)) 18 | row.save().run_sync() 19 | 20 | _row = MyTable.objects().first().run_sync() 21 | assert _row is not None 22 | 23 | self.assertEqual(type(_row.column_a), Decimal) 24 | self.assertEqual(type(_row.column_b), Decimal) 25 | 26 | self.assertAlmostEqual(_row.column_a, Decimal(1.23)) 27 | self.assertAlmostEqual(_row.column_b, Decimal("1.23")) 28 | -------------------------------------------------------------------------------- /docs/src/piccolo/features/syntax.rst: -------------------------------------------------------------------------------- 1 | Syntax 2 | ====== 3 | 4 | As close as possible to SQL 5 | --------------------------- 6 | 7 | The classes / methods / functions in Piccolo mirror their SQL counterparts as 8 | closely as possible. 9 | 10 | For example: 11 | 12 | * In other ORMs, you define models - in Piccolo you define tables. 13 | * Rather than using a filter method, you use a `where` method like in SQL. 14 | 15 | ------------------------------------------------------------------------------- 16 | 17 | Get the SQL at any time 18 | ----------------------- 19 | 20 | At any time you can access the ``__str__`` method of a query, to see the 21 | underlying SQL - making the ORM feel less magic. 22 | 23 | .. code-block:: python 24 | 25 | >>> query = Band.select(Band.name).where(Band.popularity >= 100) 26 | >>> print(query) 27 | 'SELECT name from band where popularity > 100' 28 | -------------------------------------------------------------------------------- /tests/columns/test_readable.py: -------------------------------------------------------------------------------- 1 | from piccolo import columns 2 | from piccolo.columns.readable import Readable 3 | from piccolo.table import Table 4 | from piccolo.testing.test_case import TableTest 5 | 6 | 7 | class MyTable(Table): 8 | first_name = columns.Varchar() 9 | last_name = columns.Varchar() 10 | 11 | @classmethod 12 | def get_readable(cls) -> Readable: 13 | return Readable( 14 | template="%s %s", columns=[cls.first_name, cls.last_name] 15 | ) 16 | 17 | 18 | class TestReadable(TableTest): 19 | tables = [MyTable] 20 | 21 | def setUp(self): 22 | super().setUp() 23 | MyTable(first_name="Guido", last_name="van Rossum").save().run_sync() 24 | 25 | def test_readable(self): 26 | response = MyTable.select(MyTable.get_readable()).run_sync() 27 | self.assertEqual(response[0]["readable"], "Guido van Rossum") 28 | -------------------------------------------------------------------------------- /tests/table/instance/test_remove.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from tests.example_apps.music.tables import Manager 4 | 5 | 6 | class TestRemove(TestCase): 7 | def setUp(self): 8 | Manager.create_table().run_sync() 9 | 10 | def tearDown(self): 11 | Manager.alter().drop_table().run_sync() 12 | 13 | def test_remove(self): 14 | manager = Manager(name="Maz") 15 | manager.save().run_sync() 16 | self.assertTrue( 17 | "Maz" 18 | in Manager.select(Manager.name).output(as_list=True).run_sync() 19 | ) 20 | self.assertEqual(manager._exists_in_db, True) 21 | 22 | manager.remove().run_sync() 23 | self.assertTrue( 24 | "Maz" 25 | not in Manager.select(Manager.name).output(as_list=True).run_sync() 26 | ) 27 | self.assertEqual(manager._exists_in_db, False) 28 | -------------------------------------------------------------------------------- /piccolo/query/functions/math.py: -------------------------------------------------------------------------------- 1 | """ 2 | These functions mirror their counterparts in the Postgresql docs: 3 | 4 | https://www.postgresql.org/docs/current/functions-math.html 5 | 6 | """ 7 | 8 | from .base import Function 9 | 10 | 11 | class Abs(Function): 12 | """ 13 | Absolute value. 14 | """ 15 | 16 | function_name = "ABS" 17 | 18 | 19 | class Ceil(Function): 20 | """ 21 | Nearest integer greater than or equal to argument. 22 | """ 23 | 24 | function_name = "CEIL" 25 | 26 | 27 | class Floor(Function): 28 | """ 29 | Nearest integer less than or equal to argument. 30 | """ 31 | 32 | function_name = "FLOOR" 33 | 34 | 35 | class Round(Function): 36 | """ 37 | Rounds to nearest integer. 38 | """ 39 | 40 | function_name = "ROUND" 41 | 42 | 43 | __all__ = ( 44 | "Abs", 45 | "Ceil", 46 | "Floor", 47 | "Round", 48 | ) 49 | -------------------------------------------------------------------------------- /tests/utils/test_list.py: -------------------------------------------------------------------------------- 1 | import string 2 | from unittest import TestCase 3 | 4 | from piccolo.utils.list import batch, flatten 5 | 6 | 7 | class TestFlatten(TestCase): 8 | def test_flatten(self): 9 | self.assertListEqual(flatten(["a", ["b", "c"]]), ["a", "b", "c"]) 10 | 11 | 12 | class TestBatch(TestCase): 13 | def test_batch(self): 14 | self.assertListEqual( 15 | batch([i for i in string.ascii_lowercase], chunk_size=5), 16 | [ 17 | ["a", "b", "c", "d", "e"], 18 | ["f", "g", "h", "i", "j"], 19 | ["k", "l", "m", "n", "o"], 20 | ["p", "q", "r", "s", "t"], 21 | ["u", "v", "w", "x", "y"], 22 | ["z"], 23 | ], 24 | ) 25 | 26 | def test_zero(self): 27 | with self.assertRaises(ValueError): 28 | batch([1, 2, 3], chunk_size=0) 29 | -------------------------------------------------------------------------------- /piccolo/utils/repr.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | 4 | def repr_class_instance(class_instance: object) -> str: 5 | """ 6 | Piccolo uses code generation for creating migrations. This function takes 7 | a class instance, and generates a string representation for it, which can 8 | be used in a migration file. 9 | """ 10 | init_arg_names = [ 11 | i 12 | for i in inspect.signature( 13 | class_instance.__class__.__init__ 14 | ).parameters.keys() 15 | if i not in ("self", "args", "kwargs") 16 | ] 17 | args_dict = {} 18 | for arg_name in init_arg_names: 19 | value = class_instance.__dict__.get(arg_name) 20 | args_dict[arg_name] = value 21 | 22 | args_str = ", ".join( 23 | f"{key}={value.__repr__()}" for key, value in args_dict.items() 24 | ) 25 | 26 | return f"{class_instance.__class__.__name__}({args_str})" 27 | -------------------------------------------------------------------------------- /piccolo/query/__init__.py: -------------------------------------------------------------------------------- 1 | from piccolo.columns.combination import WhereRaw 2 | 3 | from .base import Query 4 | from .functions.aggregate import Avg, Max, Min, Sum 5 | from .methods import ( 6 | Alter, 7 | Count, 8 | Create, 9 | CreateIndex, 10 | Delete, 11 | DropIndex, 12 | Exists, 13 | Insert, 14 | Objects, 15 | Raw, 16 | Select, 17 | TableExists, 18 | Update, 19 | ) 20 | from .methods.select import SelectRaw 21 | from .mixins import OrderByRaw 22 | 23 | __all__ = [ 24 | "Alter", 25 | "Avg", 26 | "Count", 27 | "Create", 28 | "CreateIndex", 29 | "Delete", 30 | "DropIndex", 31 | "Exists", 32 | "Insert", 33 | "Max", 34 | "Min", 35 | "Objects", 36 | "OrderByRaw", 37 | "Query", 38 | "Raw", 39 | "Select", 40 | "SelectRaw", 41 | "Sum", 42 | "TableExists", 43 | "Update", 44 | "WhereRaw", 45 | ] 46 | -------------------------------------------------------------------------------- /tests/columns/foreign_key/test_value_type.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from unittest import TestCase 3 | 4 | from piccolo.columns import UUID, ForeignKey, Varchar 5 | from piccolo.table import Table 6 | 7 | 8 | class Manager(Table): 9 | name = Varchar() 10 | manager: ForeignKey["Manager"] = ForeignKey("self", null=True) 11 | 12 | 13 | class Band(Table): 14 | manager = ForeignKey(references=Manager) 15 | 16 | 17 | class ManagerUUID(Table): 18 | pk = UUID(primary_key=True) 19 | 20 | 21 | class BandUUID(Table): 22 | manager = ForeignKey(references=ManagerUUID) 23 | 24 | 25 | class TestValueType(TestCase): 26 | """ 27 | The `value_type` of the `ForeignKey` should depend on the `PrimaryKey` of 28 | the referenced table. 29 | """ 30 | 31 | def test_value_type(self): 32 | self.assertTrue(Band.manager.value_type is int) 33 | self.assertTrue(BandUUID.manager.value_type is uuid.UUID) 34 | -------------------------------------------------------------------------------- /tests/columns/test_integer.py: -------------------------------------------------------------------------------- 1 | from piccolo.columns.column_types import Integer 2 | from piccolo.table import Table 3 | from piccolo.testing.test_case import AsyncTableTest 4 | from tests.base import sqlite_only 5 | 6 | 7 | class MyTable(Table): 8 | integer = Integer() 9 | 10 | 11 | @sqlite_only 12 | class TestInteger(AsyncTableTest): 13 | tables = [MyTable] 14 | 15 | async def test_large_integer(self): 16 | """ 17 | Make sure large integers can be inserted and retrieved correctly. 18 | 19 | There was a bug with this in SQLite: 20 | 21 | https://github.com/piccolo-orm/piccolo/issues/1127 22 | 23 | """ 24 | integer = 625757527765811240 25 | 26 | row = MyTable(integer=integer) 27 | await row.save() 28 | 29 | _row = MyTable.objects().first().run_sync() 30 | assert _row is not None 31 | 32 | self.assertEqual(_row.integer, integer) 33 | -------------------------------------------------------------------------------- /piccolo/apps/migrations/tables.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional 4 | 5 | from piccolo.columns import Timestamp, Varchar 6 | from piccolo.columns.defaults.timestamp import TimestampNow 7 | from piccolo.table import Table 8 | 9 | 10 | class Migration(Table): 11 | name = Varchar(length=200) 12 | app_name = Varchar(length=200) 13 | ran_on = Timestamp(default=TimestampNow()) 14 | 15 | @classmethod 16 | async def get_migrations_which_ran( 17 | cls, app_name: Optional[str] = None 18 | ) -> list[str]: 19 | """ 20 | Returns the names of migrations which have already run, by inspecting 21 | the database. 22 | """ 23 | query = cls.select(cls.name, cls.ran_on).order_by(cls.ran_on) 24 | if app_name is not None: 25 | query = query.where(cls.app_name == app_name) 26 | return [i["name"] for i in await query.run()] 27 | -------------------------------------------------------------------------------- /tests/engine/test_version_parsing.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.engine.postgres import PostgresEngine 4 | 5 | from ..base import engines_only 6 | 7 | 8 | @engines_only("postgres", "cockroach") 9 | class TestVersionParsing(TestCase): 10 | def test_version_parsing(self): 11 | """ 12 | Make sure the version number can correctly be parsed from a range 13 | of known formats. 14 | """ 15 | self.assertEqual( 16 | PostgresEngine._parse_raw_version_string(version_string="9.4"), 9.4 17 | ) 18 | 19 | self.assertEqual( 20 | PostgresEngine._parse_raw_version_string(version_string="9.4.1"), 21 | 9.4, 22 | ) 23 | 24 | self.assertEqual( 25 | PostgresEngine._parse_raw_version_string( 26 | version_string="12.4 (Ubuntu 12.4-0ubuntu0.20.04.1)" 27 | ), 28 | 12.4, 29 | ) 30 | -------------------------------------------------------------------------------- /tests/apps/migrations/commands/test_check.py: -------------------------------------------------------------------------------- 1 | from unittest import IsolatedAsyncioTestCase 2 | from unittest.mock import MagicMock, patch 3 | 4 | from piccolo.apps.migrations.commands.check import CheckMigrationManager, check 5 | from piccolo.apps.migrations.tables import Migration 6 | from piccolo.conf.apps import AppRegistry 7 | from piccolo.utils.sync import run_sync 8 | 9 | 10 | class TestCheckMigrationCommand(IsolatedAsyncioTestCase): 11 | 12 | async def asyncTearDown(self): 13 | await Migration.alter().drop_table(if_exists=True) 14 | 15 | @patch.object( 16 | CheckMigrationManager, 17 | "get_app_registry", 18 | ) 19 | def test_check_migrations(self, get_app_registry: MagicMock): 20 | get_app_registry.return_value = AppRegistry( 21 | apps=["piccolo.apps.user.piccolo_app"] 22 | ) 23 | 24 | # Make sure it runs without raising an exception: 25 | run_sync(check()) 26 | -------------------------------------------------------------------------------- /docs/src/piccolo/engines/sqlite_engine.rst: -------------------------------------------------------------------------------- 1 | SQLiteEngine 2 | ============ 3 | 4 | Configuration 5 | ------------- 6 | 7 | The ``SQLiteEngine`` is very simple - just specify a file path. The database 8 | file will be created automatically if it doesn't exist. 9 | 10 | .. code-block:: python 11 | 12 | # piccolo_conf.py 13 | from piccolo.engine.sqlite import SQLiteEngine 14 | 15 | 16 | DB = SQLiteEngine(path='my_app.sqlite') 17 | 18 | ------------------------------------------------------------------------------- 19 | 20 | Source 21 | ------ 22 | 23 | .. currentmodule:: piccolo.engine.sqlite 24 | 25 | .. autoclass:: SQLiteEngine 26 | 27 | ------------------------------------------------------------------------------- 28 | 29 | Production tips 30 | --------------- 31 | 32 | If you're planning on using SQLite in production with Piccolo, with lots of 33 | concurrent queries, then here are some :ref:`useful tips `. 34 | -------------------------------------------------------------------------------- /tests/utils/test_sync.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest import TestCase 3 | 4 | from piccolo.utils.sync import run_sync 5 | 6 | 7 | class TestSync(TestCase): 8 | def test_sync_simple(self): 9 | """ 10 | Test calling a simple coroutine. 11 | """ 12 | run_sync(asyncio.sleep(0.1)) 13 | 14 | def test_sync_nested(self): 15 | """ 16 | Test calling a coroutine, which contains a call to `run_sync`. 17 | """ 18 | 19 | async def test(): 20 | run_sync(asyncio.sleep(0.1)) 21 | 22 | run_sync(test()) 23 | 24 | def test_sync_stopped_event_loop(self): 25 | """ 26 | Test calling a coroutine, when the current thread has an event loop, 27 | but it isn't running. 28 | """ 29 | loop = asyncio.new_event_loop() 30 | asyncio.set_event_loop(loop) 31 | 32 | run_sync(asyncio.sleep(0.1)) 33 | 34 | asyncio.set_event_loop(None) 35 | -------------------------------------------------------------------------------- /tests/columns/foreign_key/test_foreign_key_references.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.columns import ForeignKey, Varchar 4 | from piccolo.table import Table 5 | 6 | 7 | class Manager(Table, tablename="manager_fk_references_test"): 8 | name = Varchar() 9 | 10 | 11 | class BandA(Table): 12 | manager = ForeignKey(references=Manager) 13 | 14 | 15 | class BandB(Table): 16 | manager: ForeignKey["Manager"] = ForeignKey(references="Manager") 17 | 18 | 19 | class TestReferences(TestCase): 20 | def test_foreign_key_references(self): 21 | """ 22 | Make sure foreign key references are stored correctly on the table 23 | which is the target of the ForeignKey. 24 | """ 25 | self.assertEqual(len(Manager._meta.foreign_key_references), 2) 26 | 27 | self.assertTrue(BandA.manager in Manager._meta.foreign_key_references) 28 | self.assertTrue(BandB.manager in Manager._meta.foreign_key_references) 29 | -------------------------------------------------------------------------------- /tests/table/test_constructor.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from tests.example_apps.music.tables import Band 4 | 5 | 6 | class TestConstructor(TestCase): 7 | def test_data_parameter(self): 8 | """ 9 | Make sure the _data parameter works. 10 | """ 11 | band = Band({Band.name: "Pythonistas"}) 12 | self.assertEqual(band.name, "Pythonistas") 13 | 14 | def test_kwargs(self): 15 | """ 16 | Make sure kwargs works. 17 | """ 18 | band = Band(name="Pythonistas") 19 | self.assertEqual(band.name, "Pythonistas") 20 | 21 | def test_mix(self): 22 | """ 23 | Make sure the _data paramter and kwargs works together (it's unlikely 24 | people will do this, but just in case). 25 | """ 26 | band = Band({Band.name: "Pythonistas"}, popularity=1000) 27 | self.assertEqual(band.name, "Pythonistas") 28 | self.assertEqual(band.popularity, 1000) 29 | -------------------------------------------------------------------------------- /piccolo/apps/user/piccolo_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from piccolo.conf.apps import AppConfig, Command 4 | 5 | from .commands.change_password import change_password 6 | from .commands.change_permissions import change_permissions 7 | from .commands.create import create 8 | from .commands.list import list_users 9 | from .tables import BaseUser 10 | 11 | CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) 12 | 13 | 14 | APP_CONFIG = AppConfig( 15 | app_name="user", 16 | migrations_folder_path=os.path.join( 17 | CURRENT_DIRECTORY, "piccolo_migrations" 18 | ), 19 | table_classes=[BaseUser], 20 | migration_dependencies=[], 21 | commands=[ 22 | Command(callable=create, aliases=["new"]), 23 | Command(callable=list_users, command_name="list", aliases=["ls"]), 24 | Command(callable=change_password, aliases=["password", "pass"]), 25 | Command(callable=change_permissions, aliases=["perm", "perms"]), 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /tests/table/test_from_dict.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.columns import Varchar 4 | from piccolo.table import Table 5 | 6 | 7 | class BandMember(Table): 8 | name = Varchar(length=50, index=True) 9 | 10 | 11 | class TestCreateFromDict(TestCase): 12 | def setUp(self): 13 | BandMember.create_table().run_sync() 14 | 15 | def tearDown(self): 16 | BandMember.alter().drop_table().run_sync() 17 | 18 | def test_create_table_from_dict(self): 19 | BandMember.from_dict({"name": "John"}).save().run_sync() 20 | self.assertEqual( 21 | BandMember.select(BandMember.name).run_sync(), [{"name": "John"}] 22 | ) 23 | BandMember.from_dict({"name": "Town"}).save().run_sync() 24 | self.assertEqual(BandMember.count().run_sync(), 2) 25 | self.assertEqual( 26 | BandMember.select(BandMember.name).run_sync(), 27 | [{"name": "John"}, {"name": "Town"}], 28 | ) 29 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_types/delete.rst: -------------------------------------------------------------------------------- 1 | .. _Delete: 2 | 3 | Delete 4 | ====== 5 | 6 | This deletes any matching rows from the table. 7 | 8 | .. code-block:: python 9 | 10 | >>> await Band.delete().where(Band.name == 'Rustaceans') 11 | [] 12 | 13 | ------------------------------------------------------------------------------- 14 | 15 | force 16 | ----- 17 | 18 | Piccolo won't let you run a delete query without a where clause, unless you 19 | explicitly tell it to do so. This is to help prevent accidentally deleting all 20 | the data from a table. 21 | 22 | .. code-block:: python 23 | 24 | >>> await Band.delete() 25 | Raises: DeletionError 26 | 27 | # Works fine: 28 | >>> await Band.delete(force=True) 29 | [] 30 | 31 | ------------------------------------------------------------------------------- 32 | 33 | Query clauses 34 | ------------- 35 | 36 | returning 37 | ~~~~~~~~~ 38 | 39 | See :ref:`Returning`. 40 | 41 | where 42 | ~~~~~ 43 | 44 | See :ref:`Where` 45 | -------------------------------------------------------------------------------- /piccolo/columns/defaults/uuid.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from collections.abc import Callable 3 | from enum import Enum 4 | from typing import Union 5 | 6 | from .base import Default 7 | 8 | 9 | class UUID4(Default): 10 | """ 11 | This makes the default value for a 12 | :class:`UUID ` column a randomly 13 | generated UUID v4 value. The advantage over using :func:`uuid.uuid4` from 14 | the standard library, is the default is set on the column definition in the 15 | database too. 16 | """ 17 | 18 | @property 19 | def postgres(self): 20 | return "uuid_generate_v4()" 21 | 22 | @property 23 | def cockroach(self): 24 | return self.postgres 25 | 26 | @property 27 | def sqlite(self): 28 | return "''" 29 | 30 | def python(self): 31 | return uuid.uuid4() 32 | 33 | 34 | UUIDArg = Union[UUID4, uuid.UUID, str, Enum, None, Callable[[], uuid.UUID]] 35 | 36 | 37 | __all__ = ["UUIDArg", "UUID4"] 38 | -------------------------------------------------------------------------------- /profiling/run_profile.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from viztracer import VizTracer 4 | 5 | from piccolo.columns.column_types import Varchar 6 | from piccolo.engine.postgres import PostgresEngine 7 | from piccolo.table import Table 8 | 9 | DB = PostgresEngine(config={"database": "piccolo_profile"}) 10 | 11 | 12 | class Band(Table, db=DB): 13 | name = Varchar() 14 | 15 | 16 | async def setup(): 17 | await Band.alter().drop_table(if_exists=True) 18 | await Band.create_table(if_not_exists=True) 19 | await Band.insert(*[Band(name="test") for _ in range(1000)]) 20 | 21 | 22 | class Trace: 23 | def __enter__(self): 24 | self.tracer = VizTracer(log_async=True) 25 | self.tracer.start() 26 | 27 | def __exit__(self, *args): 28 | self.tracer.stop() 29 | self.tracer.save() 30 | 31 | 32 | async def run_queries(): 33 | await setup() 34 | 35 | with Trace(): 36 | await Band.select() 37 | 38 | 39 | if __name__ == "__main__": 40 | asyncio.run(run_queries()) 41 | -------------------------------------------------------------------------------- /piccolo/query/functions/__init__.py: -------------------------------------------------------------------------------- 1 | from .aggregate import Avg, Count, Max, Min, Sum 2 | from .array import ( 3 | ArrayAppend, 4 | ArrayCat, 5 | ArrayPrepend, 6 | ArrayRemove, 7 | ArrayReplace, 8 | ) 9 | from .datetime import Day, Extract, Hour, Month, Second, Strftime, Year 10 | from .math import Abs, Ceil, Floor, Round 11 | from .string import Concat, Length, Lower, Ltrim, Reverse, Rtrim, Upper 12 | from .type_conversion import Cast 13 | 14 | __all__ = ( 15 | "Abs", 16 | "Avg", 17 | "Cast", 18 | "Ceil", 19 | "Concat", 20 | "Count", 21 | "Day", 22 | "Extract", 23 | "Extract", 24 | "Floor", 25 | "Hour", 26 | "Length", 27 | "Lower", 28 | "Ltrim", 29 | "Max", 30 | "Min", 31 | "Month", 32 | "Reverse", 33 | "Round", 34 | "Rtrim", 35 | "Second", 36 | "Strftime", 37 | "Sum", 38 | "Upper", 39 | "Year", 40 | "ArrayAppend", 41 | "ArrayCat", 42 | "ArrayPrepend", 43 | "ArrayRemove", 44 | "ArrayReplace", 45 | ) 46 | -------------------------------------------------------------------------------- /tests/table/test_update_self.py: -------------------------------------------------------------------------------- 1 | from piccolo.testing.test_case import AsyncTableTest 2 | from tests.example_apps.music.tables import Band, Manager 3 | 4 | 5 | class TestUpdateSelf(AsyncTableTest): 6 | 7 | tables = [Band, Manager] 8 | 9 | async def test_update_self(self): 10 | band = Band({Band.name: "Pythonistas", Band.popularity: 1000}) 11 | 12 | # Make sure we get a ValueError if it's not in the database yet. 13 | with self.assertRaises(ValueError): 14 | await band.update_self({Band.popularity: Band.popularity + 1}) 15 | 16 | # Save it, so it's in the database 17 | await band.save() 18 | 19 | # Make sure we can successfully update the object 20 | await band.update_self({Band.popularity: Band.popularity + 1}) 21 | 22 | # Make sure the value was updated on the object 23 | assert band.popularity == 1001 24 | 25 | # Make sure the value was updated in the database 26 | await band.refresh() 27 | assert band.popularity == 1001 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 79 3 | target-version = ['py38', 'py39', 'py310'] 4 | 5 | [tool.isort] 6 | profile = "black" 7 | line_length = 79 8 | 9 | [tool.mypy] 10 | [[tool.mypy.overrides]] 11 | module = [ 12 | "asyncpg.*", 13 | "colorama", 14 | "dateutil", 15 | "IPython", 16 | "IPython.core.interactiveshell", 17 | "jinja2", 18 | "orjson", 19 | "aiosqlite", 20 | "uvicorn" 21 | ] 22 | ignore_missing_imports = true 23 | 24 | 25 | [tool.pytest.ini_options] 26 | markers = [ 27 | "integration", 28 | "speed", 29 | "cockroach_array_slow" 30 | ] 31 | 32 | [tool.coverage.run] 33 | omit = [ 34 | "*.jinja", 35 | "**/piccolo_migrations/*", 36 | "**/piccolo_app.py", 37 | "**/utils/graphlib/*", 38 | ] 39 | 40 | [tool.coverage.report] 41 | # Note, we have to re-specify "pragma: no cover" 42 | # https://coverage.readthedocs.io/en/6.3.3/excluding.html#advanced-exclusion 43 | exclude_lines = [ 44 | "raise NotImplementedError", 45 | "pragma: no cover", 46 | "pass" 47 | ] 48 | -------------------------------------------------------------------------------- /tests/table/test_create_db_tables.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.table import ( 4 | create_db_tables_sync, 5 | create_tables, 6 | drop_db_tables_sync, 7 | ) 8 | from tests.example_apps.music.tables import Band, Manager 9 | 10 | 11 | class TestCreateDBTables(TestCase): 12 | def tearDown(self) -> None: 13 | drop_db_tables_sync(Manager, Band) 14 | 15 | def test_create_db_tables(self): 16 | """ 17 | Make sure the tables are created in the database. 18 | """ 19 | create_db_tables_sync(Manager, Band, if_not_exists=False) 20 | self.assertTrue(Manager.table_exists().run_sync()) 21 | self.assertTrue(Band.table_exists().run_sync()) 22 | 23 | def test_create_tables(self): 24 | """ 25 | This is a deprecated function, which just acts as a proxy. 26 | """ 27 | create_tables(Manager, Band, if_not_exists=False) 28 | self.assertTrue(Manager.table_exists().run_sync()) 29 | self.assertTrue(Band.table_exists().run_sync()) 30 | -------------------------------------------------------------------------------- /docs/src/piccolo/engines/postgres_engine.rst: -------------------------------------------------------------------------------- 1 | PostgresEngine 2 | ============== 3 | 4 | Configuration 5 | ------------- 6 | 7 | .. code-block:: python 8 | 9 | # piccolo_conf.py 10 | from piccolo.engine.postgres import PostgresEngine 11 | 12 | 13 | DB = PostgresEngine(config={ 14 | 'host': 'localhost', 15 | 'database': 'my_app', 16 | 'user': 'postgres', 17 | 'password': '' 18 | }) 19 | 20 | config 21 | ~~~~~~ 22 | 23 | The config dictionary is passed directly to the underlying database adapter, 24 | asyncpg. See the `asyncpg docs `_ 25 | to learn more. 26 | 27 | ------------------------------------------------------------------------------- 28 | 29 | Connection Pool 30 | --------------- 31 | 32 | See :ref:`ConnectionPool`. 33 | 34 | ------------------------------------------------------------------------------- 35 | 36 | Source 37 | ------ 38 | 39 | .. currentmodule:: piccolo.engine.postgres 40 | 41 | .. autoclass:: PostgresEngine 42 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_types/insert.rst: -------------------------------------------------------------------------------- 1 | .. _Insert: 2 | 3 | Insert 4 | ====== 5 | 6 | This is used to bulk insert rows into the table: 7 | 8 | .. code-block:: python 9 | 10 | await Band.insert( 11 | Band(name="Pythonistas"), 12 | Band(name="Darts"), 13 | Band(name="Gophers") 14 | ) 15 | 16 | ------------------------------------------------------------------------------- 17 | 18 | ``add`` 19 | ------- 20 | 21 | If we later decide to insert additional rows, we can use the ``add`` method: 22 | 23 | .. code-block:: python 24 | 25 | query = Band.insert(Band(name="Pythonistas")) 26 | 27 | if other_bands: 28 | query = query.add( 29 | Band(name="Darts"), 30 | Band(name="Gophers") 31 | ) 32 | 33 | await query 34 | 35 | ------------------------------------------------------------------------------- 36 | 37 | Query clauses 38 | ------------- 39 | 40 | on_conflict 41 | ~~~~~~~~~~~ 42 | 43 | See :ref:`On_Conflict`. 44 | 45 | 46 | returning 47 | ~~~~~~~~~ 48 | 49 | See :ref:`Returning`. 50 | -------------------------------------------------------------------------------- /docs/src/piccolo/engines/cockroach_engine.rst: -------------------------------------------------------------------------------- 1 | CockroachEngine 2 | =============== 3 | 4 | Configuration 5 | ------------- 6 | 7 | .. code-block:: python 8 | 9 | # piccolo_conf.py 10 | from piccolo.engine.cockroach import CockroachEngine 11 | 12 | 13 | DB = CockroachEngine(config={ 14 | 'host': 'localhost', 15 | 'database': 'piccolo', 16 | 'user': 'root', 17 | 'password': '', 18 | 'port': '26257', 19 | }) 20 | 21 | config 22 | ~~~~~~ 23 | 24 | The config dictionary is passed directly to the underlying database adapter, 25 | asyncpg. See the `asyncpg docs `_ 26 | to learn more. 27 | 28 | ------------------------------------------------------------------------------- 29 | 30 | Connection Pool 31 | --------------- 32 | 33 | See :ref:`ConnectionPool`. 34 | 35 | ------------------------------------------------------------------------------- 36 | 37 | Source 38 | ------ 39 | 40 | .. currentmodule:: piccolo.engine.cockroach 41 | 42 | .. autoclass:: CockroachEngine 43 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_types/index.rst: -------------------------------------------------------------------------------- 1 | Query Types 2 | =========== 3 | 4 | There are many different queries you can perform using Piccolo. 5 | 6 | The main ways to query data are with :ref:`Select`, which returns data as 7 | dictionaries, and :ref:`Objects`, which returns data as class instances, like a 8 | typical ORM. 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | ./select 14 | ./objects 15 | ./count 16 | ./alter 17 | ./create_table 18 | ./delete 19 | ./exists 20 | ./insert 21 | ./raw 22 | ./update 23 | 24 | ------------------------------------------------------------------------------- 25 | 26 | Features 27 | -------- 28 | 29 | .. toctree:: 30 | :maxdepth: 1 31 | 32 | ./transactions 33 | ./joins 34 | 35 | ------------------------------------------------------------------------------- 36 | 37 | Comparisons 38 | ----------- 39 | 40 | If you're familiar with other ORMs, here are some guides which show the Piccolo 41 | equivalents of common queries. 42 | 43 | .. toctree:: 44 | :maxdepth: 1 45 | 46 | ./django_comparison 47 | -------------------------------------------------------------------------------- /piccolo/utils/sync.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from collections.abc import Coroutine 5 | from concurrent.futures import Future, ThreadPoolExecutor 6 | from typing import Any, TypeVar 7 | 8 | ReturnType = TypeVar("ReturnType") 9 | 10 | 11 | def run_sync( 12 | coroutine: Coroutine[Any, Any, ReturnType], 13 | ) -> ReturnType: 14 | """ 15 | Run the coroutine synchronously - trying to accommodate as many edge cases 16 | as possible. 17 | 1. When called within a coroutine. 18 | 2. When called from ``python -m asyncio``, or iPython with %autoawait 19 | enabled, which means an event loop may already be running in the 20 | current thread. 21 | """ 22 | try: 23 | # We try this first, as in most situations this will work. 24 | return asyncio.run(coroutine) 25 | except RuntimeError: 26 | # An event loop already exists. 27 | with ThreadPoolExecutor(max_workers=1) as executor: 28 | future: Future = executor.submit(asyncio.run, coroutine) 29 | return future.result() 30 | -------------------------------------------------------------------------------- /tests/columns/foreign_key/test_foreign_key_meta.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.columns import ForeignKey, Varchar 4 | from piccolo.columns.base import OnDelete, OnUpdate 5 | from piccolo.table import Table 6 | 7 | 8 | class Manager(Table): 9 | name = Varchar() 10 | 11 | 12 | class Band(Table): 13 | """ 14 | Contains a ForeignKey with non-default `on_delete` and `on_update` values. 15 | """ 16 | 17 | manager = ForeignKey( 18 | references=Manager, 19 | on_delete=OnDelete.set_null, 20 | on_update=OnUpdate.set_null, 21 | ) 22 | 23 | 24 | class TestForeignKeyMeta(TestCase): 25 | """ 26 | Make sure that `ForeignKeyMeta` is setup correctly. 27 | """ 28 | 29 | def test_foreignkeymeta(self): 30 | self.assertTrue( 31 | Band.manager._foreign_key_meta.on_update == OnUpdate.set_null 32 | ) 33 | self.assertTrue( 34 | Band.manager._foreign_key_meta.on_delete == OnDelete.set_null 35 | ) 36 | self.assertTrue(Band.manager._foreign_key_meta.references == Manager) 37 | -------------------------------------------------------------------------------- /piccolo/query/methods/indexes.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Sequence 4 | 5 | from piccolo.query.base import Query 6 | from piccolo.querystring import QueryString 7 | 8 | 9 | class Indexes(Query): 10 | """ 11 | Returns the indexes for the given table. 12 | """ 13 | 14 | @property 15 | def postgres_querystrings(self) -> Sequence[QueryString]: 16 | return [ 17 | QueryString( 18 | "SELECT indexname AS name FROM pg_indexes " 19 | "WHERE tablename = {}", 20 | self.table._meta.get_formatted_tablename(quoted=False), 21 | ) 22 | ] 23 | 24 | @property 25 | def cockroach_querystrings(self) -> Sequence[QueryString]: 26 | return self.postgres_querystrings 27 | 28 | @property 29 | def sqlite_querystrings(self) -> Sequence[QueryString]: 30 | tablename = self.table._meta.tablename 31 | return [QueryString(f"PRAGMA index_list({tablename})")] 32 | 33 | async def response_handler(self, response): 34 | return [i["name"] for i in response] 35 | -------------------------------------------------------------------------------- /tests/table/instance/test_instantiate.py: -------------------------------------------------------------------------------- 1 | from tests.base import DBTestCase, engines_only, sqlite_only 2 | from tests.example_apps.music.tables import Band 3 | 4 | 5 | class TestInstance(DBTestCase): 6 | """ 7 | Test instantiating Table instances 8 | """ 9 | 10 | @engines_only("postgres") 11 | def test_insert_postgres(self): 12 | Pythonistas = Band(name="Pythonistas") 13 | self.assertEqual( 14 | Pythonistas.__str__(), "(DEFAULT,'Pythonistas',null,0)" 15 | ) 16 | 17 | @engines_only("cockroach") 18 | def test_insert_postgres_alt(self): 19 | Pythonistas = Band(name="Pythonistas") 20 | self.assertEqual( 21 | Pythonistas.__str__(), "(unique_rowid(),'Pythonistas',null,0)" 22 | ) 23 | 24 | @sqlite_only 25 | def test_insert_sqlite(self): 26 | Pythonistas = Band(name="Pythonistas") 27 | self.assertEqual(Pythonistas.__str__(), "(null,'Pythonistas',null,0)") 28 | 29 | def test_non_existant_column(self): 30 | with self.assertRaises(ValueError): 31 | Band(name="Pythonistas", foo="bar") 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Daniel Townsend 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tests/utils/test_lazy_loader.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, mock 2 | 3 | from piccolo.utils.lazy_loader import LazyLoader 4 | from tests.base import engines_only, sqlite_only 5 | 6 | 7 | class TestLazyLoader(TestCase): 8 | def test_lazy_loading_database_driver(self): 9 | _ = LazyLoader("asyncpg", globals(), "asyncpg") 10 | 11 | @engines_only("postgres", "cockroach") 12 | def test_lazy_loader_asyncpg_exception(self): 13 | lazy_loader = LazyLoader("asyncpg", globals(), "asyncpg.connect") 14 | 15 | with mock.patch("asyncpg.connect") as module: 16 | module.side_effect = ModuleNotFoundError() 17 | with self.assertRaises(ModuleNotFoundError): 18 | lazy_loader._load() 19 | 20 | @sqlite_only 21 | def test_lazy_loader_aiosqlite_exception(self): 22 | lazy_loader = LazyLoader("aiosqlite", globals(), "aiosqlite.connect") 23 | 24 | with mock.patch("aiosqlite.connect") as module: 25 | module.side_effect = ModuleNotFoundError() 26 | with self.assertRaises(ModuleNotFoundError): 27 | lazy_loader._load() 28 | -------------------------------------------------------------------------------- /docs/src/piccolo/functions/datetime.rst: -------------------------------------------------------------------------------- 1 | Datetime functions 2 | ================== 3 | 4 | .. currentmodule:: piccolo.query.functions.datetime 5 | 6 | Postgres / Cockroach 7 | -------------------- 8 | 9 | Extract 10 | ~~~~~~~ 11 | 12 | .. autoclass:: Extract 13 | 14 | 15 | SQLite 16 | ------ 17 | 18 | Strftime 19 | ~~~~~~~~ 20 | 21 | .. autoclass:: Strftime 22 | 23 | 24 | Database agnostic 25 | ----------------- 26 | 27 | These convenience functions work consistently across database engines. 28 | 29 | They all work very similarly, for example: 30 | 31 | .. code-block:: python 32 | 33 | >>> from piccolo.query.functions import Year 34 | >>> await Concert.select( 35 | ... Year(Concert.starts, alias="start_year") 36 | ... ) 37 | [{"start_year": 2024}] 38 | 39 | Year 40 | ~~~~ 41 | 42 | .. autofunction:: Year 43 | 44 | Month 45 | ~~~~~ 46 | 47 | .. autofunction:: Month 48 | 49 | Day 50 | ~~~ 51 | 52 | .. autofunction:: Day 53 | 54 | Hour 55 | ~~~~ 56 | 57 | .. autofunction:: Hour 58 | 59 | Minute 60 | ~~~~~~ 61 | 62 | .. autofunction:: Minute 63 | 64 | Second 65 | ~~~~~~ 66 | 67 | .. autofunction:: Second 68 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_types/create_table.rst: -------------------------------------------------------------------------------- 1 | .. _Create: 2 | 3 | Create Table 4 | ============ 5 | 6 | This creates the table and columns in the database. 7 | 8 | .. hint:: You can use migrations instead of manually altering the schema - see :ref:`Migrations`. 9 | 10 | .. code-block:: python 11 | 12 | >>> await Band.create_table() 13 | [] 14 | 15 | 16 | To prevent an error from being raised if the table already exists: 17 | 18 | .. code-block:: python 19 | 20 | >>> await Band.create_table(if_not_exists=True) 21 | [] 22 | 23 | create_db_tables / create_db_tables_sync 24 | ---------------------------------------- 25 | 26 | You can create multiple tables at once. 27 | 28 | This function will automatically sort tables based on their foreign keys so 29 | they're created in the right order: 30 | 31 | .. code-block:: python 32 | 33 | # async version 34 | >>> from piccolo.table import create_db_tables 35 | >>> await create_db_tables(Band, Manager, if_not_exists=True) 36 | 37 | # sync version 38 | >>> from piccolo.table import create_db_tables_sync 39 | >>> create_db_tables_sync(Band, Manager, if_not_exists=True) 40 | -------------------------------------------------------------------------------- /piccolo/apps/user/piccolo_migrations/2020-06-11T21-38-55.py: -------------------------------------------------------------------------------- 1 | from piccolo.apps.migrations.auto import MigrationManager 2 | 3 | ID = "2020-06-11T21:38:55" 4 | 5 | 6 | async def forwards(): 7 | manager = MigrationManager(migration_id=ID, app_name="user") 8 | 9 | manager.add_column( 10 | table_class_name="BaseUser", 11 | tablename="piccolo_user", 12 | column_name="first_name", 13 | column_class_name="Varchar", 14 | params={ 15 | "length": 255, 16 | "default": "", 17 | "null": True, 18 | "primary_key": False, 19 | "unique": False, 20 | "index": False, 21 | }, 22 | ) 23 | 24 | manager.add_column( 25 | table_class_name="BaseUser", 26 | tablename="piccolo_user", 27 | column_name="last_name", 28 | column_class_name="Varchar", 29 | params={ 30 | "length": 255, 31 | "default": "", 32 | "null": True, 33 | "primary_key": False, 34 | "unique": False, 35 | "index": False, 36 | }, 37 | ) 38 | 39 | return manager 40 | -------------------------------------------------------------------------------- /docs/src/piccolo/authentication/index.rst: -------------------------------------------------------------------------------- 1 | .. _Authentication: 2 | 3 | Authentication 4 | ============== 5 | 6 | Piccolo ships with authentication support out of the box. 7 | 8 | ------------------------------------------------------------------------------- 9 | 10 | Registering the app 11 | ------------------- 12 | 13 | Make sure ``'piccolo.apps.user.piccolo_app'`` is in your ``AppRegistry`` (see 14 | :ref:`PiccoloProjects`). 15 | 16 | ------------------------------------------------------------------------------- 17 | 18 | Tables 19 | ------ 20 | 21 | .. toctree:: 22 | :maxdepth: 1 23 | 24 | ./baseuser 25 | 26 | ------------------------------------------------------------------------------- 27 | 28 | Web app integration 29 | ------------------- 30 | 31 | Our sister project, `Piccolo API `_, 32 | contains powerful endpoints and middleware for integrating 33 | `session auth `_ 34 | and `token auth `_ 35 | into your ASGI web application, using ``BaseUser``. 36 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_clauses/group_by.rst: -------------------------------------------------------------------------------- 1 | .. _group_by: 2 | 3 | group_by 4 | ======== 5 | 6 | You can use ``group_by`` clauses with the following queries: 7 | 8 | * :ref:`Select` 9 | 10 | It is used in combination with the :ref:`aggregate functions ` 11 | - for example, ``Count``. 12 | 13 | ------------------------------------------------------------------------------- 14 | 15 | Count 16 | ----- 17 | 18 | In the following query, we get a count of the number of bands per manager: 19 | 20 | .. code-block:: python 21 | 22 | >>> from piccolo.query.functions.aggregate import Count 23 | 24 | >>> await Band.select( 25 | ... Band.manager.name.as_alias('manager_name'), 26 | ... Count(alias='band_count') 27 | ... ).group_by( 28 | ... Band.manager.name 29 | ... ) 30 | 31 | [ 32 | {"manager_name": "Graydon", "band_count": 1}, 33 | {"manager_name": "Guido", "band_count": 1} 34 | ] 35 | 36 | ------------------------------------------------------------------------------- 37 | 38 | Other aggregate functions 39 | ------------------------- 40 | 41 | These work the same as ``Count``. See :ref:`aggregate functions `. 42 | -------------------------------------------------------------------------------- /tests/columns/test_smallint.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from piccolo.columns.column_types import SmallInt 4 | from piccolo.table import Table 5 | from piccolo.testing.test_case import TableTest 6 | from tests.base import engines_only 7 | 8 | 9 | class MyTable(Table): 10 | value = SmallInt() 11 | 12 | 13 | @engines_only("postgres", "cockroach") 14 | class TestSmallIntPostgres(TableTest): 15 | """ 16 | Make sure a SmallInt column in Postgres can only store small numbers. 17 | """ 18 | 19 | tables = [MyTable] 20 | 21 | def _test_length(self): 22 | # Can store 2 bytes, but split between positive and negative values. 23 | max_value = int(2**16 / 2) - 1 24 | min_value = max_value * -1 25 | 26 | print("Testing max value") 27 | row = MyTable(value=max_value) 28 | row.save().run_sync() 29 | 30 | print("Testing min value") 31 | row.value = min_value 32 | row.save().run_sync() 33 | 34 | if "TRAVIS" not in os.environ: 35 | # This stalls out on Travis - not sure why. 36 | with self.assertRaises(Exception): 37 | row.value = max_value + 100 38 | row.save().run_sync() 39 | -------------------------------------------------------------------------------- /tests/columns/test_date.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from piccolo.columns.column_types import Date 4 | from piccolo.columns.defaults.date import DateNow 5 | from piccolo.table import Table 6 | from piccolo.testing.test_case import TableTest 7 | 8 | 9 | class MyTable(Table): 10 | created_on = Date() 11 | 12 | 13 | class MyTableDefault(Table): 14 | created_on = Date(default=DateNow()) 15 | 16 | 17 | class TestDate(TableTest): 18 | tables = [MyTable] 19 | 20 | def test_timestamp(self): 21 | created_on = datetime.datetime.now().date() 22 | row = MyTable(created_on=created_on) 23 | row.save().run_sync() 24 | 25 | result = MyTable.objects().first().run_sync() 26 | assert result is not None 27 | self.assertEqual(result.created_on, created_on) 28 | 29 | 30 | class TestDateDefault(TableTest): 31 | tables = [MyTableDefault] 32 | 33 | def test_timestamp(self): 34 | created_on = datetime.datetime.now().date() 35 | row = MyTableDefault() 36 | row.save().run_sync() 37 | 38 | result = MyTableDefault.objects().first().run_sync() 39 | assert result is not None 40 | self.assertEqual(result.created_on, created_on) 41 | -------------------------------------------------------------------------------- /tests/query/test_slots.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.query.methods import ( 4 | Alter, 5 | Count, 6 | Create, 7 | Delete, 8 | Exists, 9 | Insert, 10 | Objects, 11 | Raw, 12 | Select, 13 | TableExists, 14 | Update, 15 | ) 16 | from tests.example_apps.music.tables import Manager 17 | 18 | 19 | class TestSlots(TestCase): 20 | def test_attributes(self): 21 | """ 22 | Make sure slots are working correctly - they improve performance, 23 | and help prevent subtle bugs. 24 | """ 25 | for query_class in ( 26 | Alter, 27 | Count, 28 | Create, 29 | Delete, 30 | Exists, 31 | Insert, 32 | Objects, 33 | Raw, 34 | Select, 35 | TableExists, 36 | Update, 37 | ): 38 | class_name = query_class.__name__ 39 | 40 | with self.assertRaises( 41 | AttributeError, msg=f"{class_name} didn't raised an error" 42 | ): 43 | print(f"Setting {class_name} attribute") 44 | query_class(table=Manager).abc = 123 # type: ignore 45 | -------------------------------------------------------------------------------- /piccolo/query/methods/raw.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Sequence 4 | from typing import TYPE_CHECKING, Optional 5 | 6 | from piccolo.engine.base import BaseBatch 7 | from piccolo.query.base import Query 8 | from piccolo.querystring import QueryString 9 | 10 | if TYPE_CHECKING: # pragma: no cover 11 | from piccolo.table import Table 12 | 13 | 14 | class Raw(Query): 15 | __slots__ = ("querystring",) 16 | 17 | def __init__( 18 | self, 19 | table: type[Table], 20 | querystring: QueryString = QueryString(""), 21 | **kwargs, 22 | ): 23 | super().__init__(table, **kwargs) 24 | self.querystring = querystring 25 | 26 | async def batch( 27 | self, 28 | batch_size: Optional[int] = None, 29 | node: Optional[str] = None, 30 | **kwargs, 31 | ) -> BaseBatch: 32 | if batch_size: 33 | kwargs.update(batch_size=batch_size) 34 | if node: 35 | kwargs.update(node=node) 36 | return await self.table._meta.db.batch(self, **kwargs) 37 | 38 | @property 39 | def default_querystrings(self) -> Sequence[QueryString]: 40 | return [self.querystring] 41 | -------------------------------------------------------------------------------- /tests/apps/sql_shell/commands/test_run.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock, patch 3 | 4 | from piccolo.apps.sql_shell.commands.run import run 5 | from tests.base import postgres_only, sqlite_only 6 | 7 | 8 | class TestRun(TestCase): 9 | @postgres_only 10 | @patch("piccolo.apps.sql_shell.commands.run.subprocess") 11 | def test_psql(self, subprocess: MagicMock): 12 | """ 13 | Make sure psql was called correctly. 14 | """ 15 | run() 16 | self.assertTrue(subprocess.run.called) 17 | 18 | assert subprocess.run.call_args.args[0] == [ 19 | "psql", 20 | "-U", 21 | "postgres", 22 | "-h", 23 | "localhost", 24 | "-p", 25 | "5432", 26 | "piccolo", 27 | ] 28 | 29 | @sqlite_only 30 | @patch("piccolo.apps.sql_shell.commands.run.subprocess") 31 | def test_sqlite3(self, subprocess: MagicMock): 32 | """ 33 | Make sure sqlite3 was called correctly. 34 | """ 35 | run() 36 | self.assertTrue(subprocess.run.called) 37 | 38 | assert subprocess.run.call_args.args[0] == ["sqlite3", "test.sqlite"] 39 | -------------------------------------------------------------------------------- /docs/src/piccolo/getting_started/database_support.rst: -------------------------------------------------------------------------------- 1 | .. _DatabaseSupport: 2 | 3 | Database Support 4 | ================ 5 | 6 | `Postgres `_ is the primary database which Piccolo 7 | was designed for. It's robust, feature rich, and a great choice for most projects. 8 | 9 | `CockroachDB `_ is also supported. It's designed 10 | to be scalable and fault tolerant, and is mostly compatible with Postgres. 11 | There may be some minor features not supported, but it's OK to use. 12 | 13 | `SQLite `_ support was originally added to 14 | enable tooling like the :ref:`playground `, but over time we've 15 | added more and more support. Many people successfully use SQLite and Piccolo 16 | together in production. The main missing feature is support for 17 | :ref:`automatic database migrations ` due to SQLite's limited 18 | support for ``ALTER TABLE`` ``DDL`` statements. 19 | 20 | What about other databases? 21 | --------------------------- 22 | 23 | Our focus is on providing great support for a limited number of databases 24 | (especially Postgres), however it's likely that we'll support more databases in 25 | the future. 26 | -------------------------------------------------------------------------------- /piccolo/custom_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import datetime 4 | import decimal 5 | import uuid 6 | from collections.abc import Iterable 7 | from typing import TYPE_CHECKING, Any, TypeVar, Union 8 | 9 | from typing_extensions import TypeAlias 10 | 11 | if TYPE_CHECKING: # pragma: no cover 12 | from piccolo.columns.combination import And, Or, Where, WhereRaw # noqa 13 | from piccolo.table import Table 14 | 15 | 16 | Combinable = Union["Where", "WhereRaw", "And", "Or"] 17 | CustomIterable = Iterable[Any] 18 | 19 | 20 | TableInstance = TypeVar("TableInstance", bound="Table") 21 | QueryResponseType = TypeVar("QueryResponseType", bound=Any) 22 | 23 | 24 | # These are types we can reasonably expect to send to the database. 25 | BasicTypes: TypeAlias = Union[ 26 | bytes, 27 | datetime.date, 28 | datetime.datetime, 29 | datetime.time, 30 | datetime.timedelta, 31 | decimal.Decimal, 32 | dict, 33 | float, 34 | int, 35 | list, 36 | str, 37 | uuid.UUID, 38 | ] 39 | 40 | ############################################################################### 41 | # For backwards compatibility: 42 | 43 | from piccolo.columns.defaults.timestamp import DatetimeDefault # noqa 44 | -------------------------------------------------------------------------------- /tests/apps/user/commands/test_change_password.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import patch 3 | 4 | from piccolo.apps.user.commands.change_password import change_password 5 | from piccolo.apps.user.tables import BaseUser 6 | 7 | 8 | class TestChangePassword(TestCase): 9 | def setUp(self): 10 | BaseUser.create_table(if_not_exists=True).run_sync() 11 | 12 | def tearDown(self): 13 | BaseUser.alter().drop_table().run_sync() 14 | 15 | @patch( 16 | "piccolo.apps.user.commands.change_password.get_username", 17 | return_value="bob123", 18 | ) 19 | @patch( 20 | "piccolo.apps.user.commands.change_password.get_password", 21 | return_value="new_password", 22 | ) 23 | @patch( 24 | "piccolo.apps.user.commands.change_password.get_confirmed_password", 25 | return_value="new_password", 26 | ) 27 | def test_create(self, *args, **kwargs): 28 | user = BaseUser(username="bob123", password="old_password") 29 | user.save().run_sync() 30 | 31 | change_password() 32 | 33 | self.assertTrue( 34 | BaseUser.login_sync(username="bob123", password="new_password") 35 | is not None 36 | ) 37 | -------------------------------------------------------------------------------- /tests/columns/test_bigint.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from piccolo.columns.column_types import BigInt 4 | from piccolo.table import Table 5 | from piccolo.testing.test_case import TableTest 6 | from tests.base import engines_only 7 | 8 | 9 | class MyTable(Table): 10 | value = BigInt() 11 | 12 | 13 | @engines_only("postgres", "cockroach") 14 | class TestBigIntPostgres(TableTest): 15 | """ 16 | Make sure a BigInt column in Postgres can store a large number. 17 | """ 18 | 19 | tables = [MyTable] 20 | 21 | def _test_length(self): 22 | # Can store 8 bytes, but split between positive and negative values. 23 | max_value = int(2**64 / 2) - 1 24 | min_value = max_value * -1 25 | 26 | print("Testing max value") 27 | row = MyTable(value=max_value) 28 | row.save().run_sync() 29 | 30 | print("Testing min value") 31 | row.value = min_value 32 | row.save().run_sync() 33 | 34 | if "TRAVIS" not in os.environ: 35 | # This stalls out on Travis - not sure why. 36 | print("Test exceeding max value") 37 | with self.assertRaises(Exception): 38 | row.value = max_value + 100 39 | row.save().run_sync() 40 | -------------------------------------------------------------------------------- /tests/table/test_drop_db_tables.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.table import ( 4 | create_db_tables_sync, 5 | drop_db_tables_sync, 6 | drop_tables, 7 | ) 8 | from tests.example_apps.music.tables import Band, Manager 9 | 10 | 11 | class TestDropTables(TestCase): 12 | def setUp(self): 13 | create_db_tables_sync(Band, Manager) 14 | 15 | def test_drop_db_tables(self): 16 | """ 17 | Make sure the tables are dropped. 18 | """ 19 | self.assertTrue(Manager.table_exists().run_sync()) 20 | self.assertTrue(Band.table_exists().run_sync()) 21 | 22 | drop_db_tables_sync(Manager, Band) 23 | 24 | self.assertFalse(Manager.table_exists().run_sync()) 25 | self.assertFalse(Band.table_exists().run_sync()) 26 | 27 | def test_drop_tables(self): 28 | """ 29 | This is a deprecated function, which just acts as a proxy. 30 | """ 31 | self.assertTrue(Manager.table_exists().run_sync()) 32 | self.assertTrue(Band.table_exists().run_sync()) 33 | 34 | drop_tables(Manager, Band) 35 | 36 | self.assertFalse(Manager.table_exists().run_sync()) 37 | self.assertFalse(Band.table_exists().run_sync()) 38 | -------------------------------------------------------------------------------- /tests/columns/foreign_key/test_on_delete_on_update.py: -------------------------------------------------------------------------------- 1 | from piccolo.columns import ForeignKey, Varchar 2 | from piccolo.columns.base import OnDelete, OnUpdate 3 | from piccolo.query.constraints import get_fk_constraint_rules 4 | from piccolo.table import Table 5 | from piccolo.testing.test_case import AsyncTableTest 6 | from tests.base import engines_only 7 | 8 | 9 | class Manager(Table): 10 | name = Varchar() 11 | 12 | 13 | class Band(Table): 14 | """ 15 | Contains a ForeignKey with non-default `on_delete` and `on_update` values. 16 | """ 17 | 18 | manager = ForeignKey( 19 | references=Manager, 20 | on_delete=OnDelete.set_null, 21 | on_update=OnUpdate.set_null, 22 | ) 23 | 24 | 25 | @engines_only("postgres", "cockroach") 26 | class TestOnDeleteOnUpdate(AsyncTableTest): 27 | """ 28 | Make sure that on_delete, and on_update are correctly applied in the 29 | database. 30 | """ 31 | 32 | tables = [Manager, Band] 33 | 34 | async def test_on_delete_on_update(self): 35 | constraint_rules = await get_fk_constraint_rules(Band.manager) 36 | self.assertEqual(constraint_rules.on_delete, OnDelete.set_null) 37 | self.assertEqual(constraint_rules.on_update, OnDelete.set_null) 38 | -------------------------------------------------------------------------------- /docs/src/piccolo/functions/basic_usage.rst: -------------------------------------------------------------------------------- 1 | Basic Usage 2 | =========== 3 | 4 | Select queries 5 | -------------- 6 | 7 | Functions can be used in ``select`` queries - here's an example, where we 8 | convert the values to uppercase: 9 | 10 | .. code-block:: python 11 | 12 | >>> from piccolo.query.functions import Upper 13 | 14 | >>> await Band.select( 15 | ... Upper(Band.name, alias="name") 16 | ... ) 17 | 18 | [{"name": "PYTHONISTAS"}] 19 | 20 | Where clauses 21 | ------------- 22 | 23 | Functions can also be used in ``where`` clauses. 24 | 25 | .. code-block:: python 26 | 27 | >>> from piccolo.query.functions import Length 28 | 29 | >>> await Band.select( 30 | ... Band.name 31 | ... ).where( 32 | ... Length(Band.name) > 10 33 | ... ) 34 | 35 | [{"name": "Pythonistas"}] 36 | 37 | Update queries 38 | -------------- 39 | 40 | And even in ``update`` queries: 41 | 42 | .. code-block:: python 43 | 44 | >>> from piccolo.query.functions import Upper 45 | 46 | >>> await Band.update( 47 | ... {Band.name: Upper(Band.name)}, 48 | ... force=True 49 | ... ).returning(Band.name) 50 | 51 | [{"name": "PYTHONISTAS"}, {"name": "RUSTACEANS"}, {"name": "C-SHARPS"}] 52 | 53 | Pretty much everywhere. 54 | -------------------------------------------------------------------------------- /tests/table/test_table_exists.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.columns import Varchar 4 | from piccolo.schema import SchemaManager 5 | from piccolo.table import Table 6 | from tests.base import engines_skip 7 | from tests.example_apps.music.tables import Manager 8 | 9 | 10 | class TestTableExists(TestCase): 11 | def setUp(self): 12 | Manager.create_table().run_sync() 13 | 14 | def tearDown(self): 15 | Manager.alter().drop_table().run_sync() 16 | 17 | def test_table_exists(self): 18 | response = Manager.table_exists().run_sync() 19 | self.assertTrue(response) 20 | 21 | 22 | class Band(Table, schema="schema_1"): 23 | name = Varchar() 24 | 25 | 26 | @engines_skip("sqlite") 27 | class TestTableExistsSchema(TestCase): 28 | def setUp(self): 29 | Band.create_table(auto_create_schema=True).run_sync() 30 | 31 | def tearDown(self): 32 | SchemaManager().drop_schema( 33 | "schema_1", if_exists=True, cascade=True 34 | ).run_sync() 35 | 36 | def test_table_exists(self): 37 | """ 38 | Make sure it works correctly if the table is in a Postgres schema. 39 | """ 40 | response = Band.table_exists().run_sync() 41 | self.assertTrue(response) 42 | -------------------------------------------------------------------------------- /piccolo/query/methods/drop_index.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Sequence 4 | from typing import TYPE_CHECKING, Union 5 | 6 | from piccolo.columns.base import Column 7 | from piccolo.query.base import Query 8 | from piccolo.querystring import QueryString 9 | 10 | if TYPE_CHECKING: # pragma: no cover 11 | from piccolo.table import Table 12 | 13 | 14 | class DropIndex(Query): 15 | def __init__( 16 | self, 17 | table: type[Table], 18 | columns: Union[list[Column], list[str]], 19 | if_exists: bool = True, 20 | **kwargs, 21 | ): 22 | self.columns = columns 23 | self.if_exists = if_exists 24 | super().__init__(table, **kwargs) 25 | 26 | @property 27 | def column_names(self) -> list[str]: 28 | return [ 29 | i._meta.name if isinstance(i, Column) else i for i in self.columns 30 | ] 31 | 32 | @property 33 | def default_querystrings(self) -> Sequence[QueryString]: 34 | column_names = self.column_names 35 | index_name = self.table._get_index_name(column_names) 36 | query = "DROP INDEX" 37 | if self.if_exists: 38 | query += " IF EXISTS" 39 | return [QueryString(f"{query} {index_name}")] 40 | -------------------------------------------------------------------------------- /tests/table/instance/test_create.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.columns import Integer, Varchar 4 | from piccolo.table import Table 5 | 6 | 7 | class Band(Table): 8 | name = Varchar(default=None, null=False) 9 | popularity = Integer() 10 | 11 | 12 | class TestCreate(TestCase): 13 | def setUp(self): 14 | Band.create_table().run_sync() 15 | 16 | def tearDown(self): 17 | Band.alter().drop_table().run_sync() 18 | 19 | def test_create_new(self): 20 | """ 21 | Make sure that creating a new instance works. 22 | """ 23 | Band.objects().create(name="Pythonistas", popularity=1000).run_sync() 24 | 25 | names = [i["name"] for i in Band.select(Band.name).run_sync()] 26 | self.assertTrue("Pythonistas" in names) 27 | 28 | def test_null_values(self): 29 | """ 30 | Make sure we test non-null columns: 31 | https://github.com/piccolo-orm/piccolo/issues/652 32 | """ 33 | with self.assertRaises(ValueError) as manager: 34 | Band.objects().create().run_sync() 35 | 36 | self.assertEqual(str(manager.exception), "name wasn't provided") 37 | 38 | # Shouldn't raise an exception 39 | Band.objects().create(name="Pythonistas").run_sync() 40 | -------------------------------------------------------------------------------- /tests/apps/migrations/commands/test_clean.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.apps.migrations.commands.clean import clean 4 | from piccolo.apps.migrations.tables import Migration 5 | from piccolo.utils.sync import run_sync 6 | 7 | 8 | class TestCleanMigrationCommand(TestCase): 9 | def test_clean(self): 10 | Migration.create_table(if_not_exists=True).run_sync() 11 | 12 | real_migration_ids = [ 13 | "2020-12-17T18:44:30", 14 | "2020-12-17T18:44:39", 15 | "2020-12-17T18:44:44", 16 | ] 17 | 18 | orphaned_migration_id = "2010-01-101T00:00:00" 19 | 20 | migration_ids = real_migration_ids + [orphaned_migration_id] 21 | 22 | Migration.insert( 23 | *[Migration(name=i, app_name="music") for i in migration_ids] 24 | ).run_sync() 25 | 26 | run_sync(clean(app_name="music", auto_agree=True)) 27 | 28 | remaining_rows = ( 29 | Migration.select(Migration.name) 30 | .where(Migration.app_name == "music") 31 | .output(as_list=True) 32 | .order_by(Migration.name) 33 | .run_sync() 34 | ) 35 | self.assertEqual(remaining_rows, real_migration_ids) 36 | 37 | Migration.alter().drop_table(if_exists=True).run_sync() 38 | -------------------------------------------------------------------------------- /docs/src/piccolo/tutorials/fastapi_src/app.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, FastAPI 2 | from pydantic import BaseModel 3 | 4 | from piccolo.columns.column_types import Varchar 5 | from piccolo.engine.sqlite import SQLiteEngine 6 | from piccolo.table import Table 7 | 8 | DB = SQLiteEngine() 9 | 10 | 11 | class Band(Table, db=DB): 12 | """ 13 | You would usually import this from tables.py 14 | """ 15 | 16 | name = Varchar() 17 | 18 | 19 | async def transaction(): 20 | async with DB.transaction() as transaction: 21 | yield transaction 22 | 23 | 24 | app = FastAPI() 25 | 26 | 27 | @app.get("/bands/", dependencies=[Depends(transaction)]) 28 | async def get_bands(): 29 | return await Band.select() 30 | 31 | 32 | class CreateBandModel(BaseModel): 33 | name: str 34 | 35 | 36 | @app.post("/bands/", dependencies=[Depends(transaction)]) 37 | async def create_band(model: CreateBandModel): 38 | await Band({Band.name: model.name}).save() 39 | 40 | # If an exception is raised then the transaction is rolled back. 41 | raise Exception("Oops") 42 | 43 | 44 | async def main(): 45 | await Band.create_table(if_not_exists=True) 46 | 47 | 48 | if __name__ == "__main__": 49 | import asyncio 50 | 51 | import uvicorn 52 | 53 | asyncio.run(main()) 54 | uvicorn.run(app) 55 | -------------------------------------------------------------------------------- /tests/apps/shell/commands/test_run.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import MagicMock, call, patch 3 | 4 | from piccolo.apps.shell.commands.run import run 5 | 6 | 7 | class TestRun(TestCase): 8 | @patch("piccolo.apps.shell.commands.run.start_ipython_shell") 9 | @patch("piccolo.apps.shell.commands.run.print") 10 | def test_run(self, print_: MagicMock, start_ipython_shell: MagicMock): 11 | """ 12 | A simple test to make sure it executes without raising any exceptions. 13 | """ 14 | run() 15 | 16 | self.assertEqual( 17 | print_.mock_calls, 18 | [ 19 | call("-------"), 20 | call("Importing music tables:"), 21 | call("- Band"), 22 | call("- Concert"), 23 | call("- Instrument"), 24 | call("- Manager"), 25 | call("- Poster"), 26 | call("- RecordingStudio"), 27 | call("- Shirt"), 28 | call("- Ticket"), 29 | call("- Venue"), 30 | call("Importing mega tables:"), 31 | call("- MegaTable"), 32 | call("- SmallTable"), 33 | call("-------"), 34 | ], 35 | ) 36 | 37 | self.assertTrue(start_ipython_shell.called) 38 | -------------------------------------------------------------------------------- /tests/columns/foreign_key/test_foreign_key_self.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.columns import ForeignKey, Serial, Varchar 4 | from piccolo.table import Table 5 | 6 | 7 | class Manager(Table, tablename="manager"): 8 | id: Serial 9 | name = Varchar() 10 | manager: ForeignKey["Manager"] = ForeignKey("self", null=True) 11 | 12 | 13 | class TestForeignKeySelf(TestCase): 14 | """ 15 | Test that ForeignKey columns can be created with references to the parent 16 | table. 17 | """ 18 | 19 | def setUp(self): 20 | Manager.create_table().run_sync() 21 | 22 | def tearDown(self): 23 | Manager.alter().drop_table().run_sync() 24 | 25 | def test_foreign_key_self(self): 26 | manager = Manager(name="Mr Manager") 27 | manager.save().run_sync() 28 | 29 | worker = Manager(name="Mr Worker", manager=manager.id) 30 | worker.save().run_sync() 31 | 32 | response = ( 33 | Manager.select(Manager.name, Manager.manager.name) 34 | .order_by(Manager.name) 35 | .run_sync() 36 | ) 37 | self.assertEqual( 38 | response, 39 | [ 40 | {"name": "Mr Manager", "manager.name": None}, 41 | {"name": "Mr Worker", "manager.name": "Mr Manager"}, 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/static/main.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | body { 7 | background-color: #f0f7fd; 8 | color: #2b475f; 9 | font-family: 'Open Sans', sans-serif; 10 | } 11 | 12 | div.hero { 13 | background-color: #4C89C8; 14 | box-sizing: border-box; 15 | padding: 5rem; 16 | } 17 | 18 | a { 19 | color: #4C89C8; 20 | text-decoration: none; 21 | } 22 | 23 | div.hero h1 { 24 | color: white; 25 | font-weight: normal; 26 | text-align: center; 27 | } 28 | 29 | section { 30 | padding-bottom: 2rem; 31 | } 32 | 33 | div.content { 34 | background-color: white; 35 | border-radius: 0.5rem; 36 | box-sizing: border-box; 37 | margin: 1rem auto; 38 | max-width: 50rem; 39 | padding: 2rem; 40 | transform: translateY(-4rem); 41 | box-shadow: 0px 1px 1px 1px rgb(0,0,0,0.05); 42 | } 43 | 44 | div.content h2, div.content h3 { 45 | font-weight: normal; 46 | } 47 | 48 | div.content code { 49 | padding: 2px 4px; 50 | background-color: #f0f7fd; 51 | border-radius: 0.2rem; 52 | } 53 | 54 | p.code { 55 | background-color: #233d58; 56 | color: white; 57 | font-family: monospace; 58 | padding: 1rem; 59 | margin: 0; 60 | display: block; 61 | border-radius: 0.2rem; 62 | } 63 | 64 | p.code span { 65 | display: block; 66 | padding: 0.5rem; 67 | } -------------------------------------------------------------------------------- /piccolo/apps/user/piccolo_migrations/2021-04-30T16-14-15.py: -------------------------------------------------------------------------------- 1 | from piccolo.apps.migrations.auto import MigrationManager 2 | from piccolo.columns.column_types import Boolean, Timestamp 3 | from piccolo.columns.indexes import IndexMethod 4 | 5 | ID = "2021-04-30T16:14:15" 6 | VERSION = "0.18.2" 7 | 8 | 9 | async def forwards(): 10 | manager = MigrationManager(migration_id=ID, app_name="user") 11 | 12 | manager.add_column( 13 | table_class_name="BaseUser", 14 | tablename="piccolo_user", 15 | column_name="superuser", 16 | column_class_name="Boolean", 17 | column_class=Boolean, 18 | params={ 19 | "default": False, 20 | "null": False, 21 | "primary_key": False, 22 | "unique": False, 23 | "index": False, 24 | "index_method": IndexMethod.btree, 25 | }, 26 | ) 27 | 28 | manager.add_column( 29 | table_class_name="BaseUser", 30 | tablename="piccolo_user", 31 | column_name="last_login", 32 | column_class_name="Timestamp", 33 | column_class=Timestamp, 34 | params={ 35 | "default": None, 36 | "null": True, 37 | "primary_key": False, 38 | "unique": False, 39 | "index": False, 40 | "index_method": IndexMethod.btree, 41 | }, 42 | ) 43 | 44 | return manager 45 | -------------------------------------------------------------------------------- /piccolo/utils/warnings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | from enum import Enum 5 | 6 | import colorama # type: ignore 7 | 8 | colorama.init() 9 | 10 | 11 | class Level(Enum): 12 | low = colorama.Fore.WHITE 13 | medium = colorama.Fore.YELLOW 14 | high = colorama.Fore.RED 15 | 16 | 17 | def colored_string(message: str, level: Level = Level.medium) -> str: 18 | return level.value + message + colorama.Fore.RESET 19 | 20 | 21 | def colored_warning( 22 | message: str, 23 | category: type[Warning] = Warning, 24 | stacklevel: int = 3, 25 | level: Level = Level.medium, 26 | ): 27 | """ 28 | A wrapper around the stdlib's `warnings.warn`, which colours the output. 29 | 30 | :param message: 31 | The message to display to the user 32 | :category: 33 | `Warning` has several subclasses which may be more appropriate, for 34 | example `DeprecationWarning`. 35 | :stacklevel: 36 | Used to determine the source of the error within the source code. 37 | See the Python docs for more detail. 38 | https://docs.python.org/3/library/warnings.html#warnings.warn 39 | :level: 40 | Used to determine the colour of the text displayed to the user. 41 | """ 42 | colored_message = colored_string(message=message, level=level) 43 | warnings.warn(colored_message, category=category, stacklevel=stacklevel) 44 | -------------------------------------------------------------------------------- /piccolo/query/methods/exists.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Sequence 4 | from typing import TypeVar, Union 5 | 6 | from piccolo.custom_types import Combinable, TableInstance 7 | from piccolo.query.base import Query 8 | from piccolo.query.methods.select import Select 9 | from piccolo.query.mixins import WhereDelegate 10 | from piccolo.querystring import QueryString 11 | 12 | 13 | class Exists(Query[TableInstance, bool]): 14 | __slots__ = ("where_delegate",) 15 | 16 | def __init__(self, table: type[TableInstance], **kwargs): 17 | super().__init__(table, **kwargs) 18 | self.where_delegate = WhereDelegate() 19 | 20 | def where(self: Self, *where: Union[Combinable, QueryString]) -> Self: 21 | self.where_delegate.where(*where) 22 | return self 23 | 24 | async def response_handler(self, response) -> bool: 25 | # Convert to a bool - postgres returns True, and sqlite return 1. 26 | return bool(response[0]["exists"]) 27 | 28 | @property 29 | def default_querystrings(self) -> Sequence[QueryString]: 30 | select = Select(table=self.table) 31 | select.where_delegate._where = self.where_delegate._where 32 | return [ 33 | QueryString( 34 | 'SELECT EXISTS({}) AS "exists"', select.querystrings[0] 35 | ) 36 | ] 37 | 38 | 39 | Self = TypeVar("Self", bound=Exists) 40 | -------------------------------------------------------------------------------- /docs/src/piccolo/getting_started/example_schema.rst: -------------------------------------------------------------------------------- 1 | .. _ExampleSchema: 2 | 3 | Example Schema 4 | ============== 5 | 6 | This is the schema used by the example queries throughout the docs, and also 7 | in the :ref:`playground`. 8 | 9 | ``Manager`` and ``Band`` are most commonly used: 10 | 11 | .. code-block:: python 12 | 13 | from piccolo.table import Table 14 | from piccolo.columns import ForeignKey, Integer, Varchar 15 | 16 | 17 | class Manager(Table): 18 | name = Varchar(length=100) 19 | 20 | 21 | class Band(Table): 22 | name = Varchar(length=100) 23 | manager = ForeignKey(references=Manager) 24 | popularity = Integer() 25 | 26 | We sometimes use these other tables in the examples too: 27 | 28 | .. code-block:: python 29 | 30 | class Venue(Table): 31 | name = Varchar() 32 | capacity = Integer() 33 | 34 | 35 | class Concert(Table): 36 | band_1 = ForeignKey(references=Band) 37 | band_2 = ForeignKey(references=Band) 38 | venue = ForeignKey(references=Venue) 39 | starts = Timestamp() 40 | duration = Interval() 41 | 42 | 43 | class Ticket(Table): 44 | concert = ForeignKey(references=Concert) 45 | price = Numeric() 46 | 47 | 48 | class RecordingStudio(Table): 49 | name = Varchar() 50 | facilities = JSONB() 51 | 52 | To understand more about defining your own schemas, see :ref:`DefiningSchema`. 53 | -------------------------------------------------------------------------------- /piccolo/columns/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Column, ForeignKeyMeta, OnDelete, OnUpdate, Selectable 2 | from .column_types import ( 3 | JSON, 4 | JSONB, 5 | UUID, 6 | Array, 7 | BigInt, 8 | BigSerial, 9 | Boolean, 10 | Bytea, 11 | Date, 12 | Decimal, 13 | DoublePrecision, 14 | Email, 15 | Float, 16 | ForeignKey, 17 | Integer, 18 | Interval, 19 | Numeric, 20 | PrimaryKey, 21 | Real, 22 | Secret, 23 | Serial, 24 | SmallInt, 25 | Text, 26 | Time, 27 | Timestamp, 28 | Timestamptz, 29 | Varchar, 30 | ) 31 | from .combination import And, Or, Where 32 | from .m2m import M2M 33 | from .reference import LazyTableReference 34 | 35 | __all__ = [ 36 | "Column", 37 | "ForeignKeyMeta", 38 | "OnDelete", 39 | "OnUpdate", 40 | "Selectable", 41 | "JSON", 42 | "JSONB", 43 | "UUID", 44 | "Array", 45 | "BigInt", 46 | "BigSerial", 47 | "Boolean", 48 | "Bytea", 49 | "Date", 50 | "Decimal", 51 | "DoublePrecision", 52 | "Email", 53 | "Float", 54 | "ForeignKey", 55 | "Integer", 56 | "Interval", 57 | "Numeric", 58 | "PrimaryKey", 59 | "Real", 60 | "Secret", 61 | "Serial", 62 | "SmallInt", 63 | "Text", 64 | "Time", 65 | "Timestamp", 66 | "Timestamptz", 67 | "Varchar", 68 | "And", 69 | "Or", 70 | "Where", 71 | "M2M", 72 | "LazyTableReference", 73 | ] 74 | -------------------------------------------------------------------------------- /tests/engine/test_logging.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from tests.base import DBTestCase 4 | from tests.example_apps.music.tables import Manager 5 | 6 | 7 | class TestLogging(DBTestCase): 8 | def tearDown(self): 9 | Manager._meta.db.log_queries = False 10 | Manager._meta.db.log_responses = False 11 | super().tearDown() 12 | 13 | def test_log_queries(self): 14 | Manager._meta.db.log_queries = True 15 | 16 | with patch("piccolo.engine.base.Engine.print_query") as print_query: 17 | Manager.select().run_sync() 18 | print_query.assert_called_once() 19 | 20 | def test_log_responses(self): 21 | Manager._meta.db.log_responses = True 22 | 23 | with patch( 24 | "piccolo.engine.base.Engine.print_response" 25 | ) as print_response: 26 | Manager.select().run_sync() 27 | print_response.assert_called_once() 28 | 29 | def test_log_queries_and_responses(self): 30 | Manager._meta.db.log_queries = True 31 | Manager._meta.db.log_responses = True 32 | 33 | with patch("piccolo.engine.base.Engine.print_query") as print_query: 34 | with patch( 35 | "piccolo.engine.base.Engine.print_response" 36 | ) as print_response: 37 | Manager.select().run_sync() 38 | print_query.assert_called_once() 39 | print_response.assert_called_once() 40 | -------------------------------------------------------------------------------- /tests/query/functions/test_math.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | 3 | from piccolo.columns import Numeric 4 | from piccolo.query.functions.math import Abs, Ceil, Floor, Round 5 | from piccolo.table import Table 6 | from piccolo.testing.test_case import TableTest 7 | 8 | 9 | class Ticket(Table): 10 | price = Numeric(digits=(5, 2)) 11 | 12 | 13 | class TestMath(TableTest): 14 | 15 | tables = [Ticket] 16 | 17 | def setUp(self): 18 | super().setUp() 19 | self.ticket = Ticket({Ticket.price: decimal.Decimal("36.50")}) 20 | self.ticket.save().run_sync() 21 | 22 | def test_floor(self): 23 | response = Ticket.select(Floor(Ticket.price, alias="price")).run_sync() 24 | self.assertListEqual(response, [{"price": decimal.Decimal("36.00")}]) 25 | 26 | def test_ceil(self): 27 | response = Ticket.select(Ceil(Ticket.price, alias="price")).run_sync() 28 | self.assertListEqual(response, [{"price": decimal.Decimal("37.00")}]) 29 | 30 | def test_abs(self): 31 | self.ticket.price = decimal.Decimal("-1.50") 32 | self.ticket.save().run_sync() 33 | response = Ticket.select(Abs(Ticket.price, alias="price")).run_sync() 34 | self.assertListEqual(response, [{"price": decimal.Decimal("1.50")}]) 35 | 36 | def test_round(self): 37 | response = Ticket.select(Round(Ticket.price, alias="price")).run_sync() 38 | self.assertListEqual(response, [{"price": decimal.Decimal("37.00")}]) 39 | -------------------------------------------------------------------------------- /piccolo/query/methods/table_exists.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Sequence 4 | 5 | from piccolo.custom_types import TableInstance 6 | from piccolo.query.base import Query 7 | from piccolo.querystring import QueryString 8 | 9 | 10 | class TableExists(Query[TableInstance, bool]): 11 | 12 | __slots__: tuple = () 13 | 14 | async def response_handler(self, response): 15 | return bool(response[0]["exists"]) 16 | 17 | @property 18 | def sqlite_querystrings(self) -> Sequence[QueryString]: 19 | return [ 20 | QueryString( 21 | "SELECT EXISTS(SELECT * FROM sqlite_master WHERE " 22 | "name = {}) AS 'exists'", 23 | self.table._meta.tablename, 24 | ) 25 | ] 26 | 27 | @property 28 | def postgres_querystrings(self) -> Sequence[QueryString]: 29 | subquery = QueryString( 30 | "SELECT * FROM information_schema.tables WHERE table_name = {}", 31 | self.table._meta.tablename, 32 | ) 33 | 34 | if self.table._meta.schema: 35 | subquery = QueryString( 36 | "{} AND table_schema = {}", subquery, self.table._meta.schema 37 | ) 38 | 39 | query = QueryString("SELECT EXISTS({})", subquery) 40 | 41 | return [query] 42 | 43 | @property 44 | def cockroach_querystrings(self) -> Sequence[QueryString]: 45 | return self.postgres_querystrings 46 | -------------------------------------------------------------------------------- /tests/engine/test_extra_nodes.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | from unittest import TestCase 3 | from unittest.mock import MagicMock 4 | 5 | from piccolo.columns.column_types import Varchar 6 | from piccolo.engine import engine_finder 7 | from piccolo.engine.postgres import PostgresEngine 8 | from piccolo.table import Table 9 | from tests.base import AsyncMock, engines_only 10 | 11 | 12 | @engines_only("postgres", "cockroach") 13 | class TestExtraNodes(TestCase): 14 | def test_extra_nodes(self): 15 | """ 16 | Make sure that other nodes can be queried. 17 | """ 18 | # Get the test database credentials: 19 | test_engine = engine_finder() 20 | assert test_engine is not None 21 | 22 | test_engine = cast(PostgresEngine, test_engine) 23 | 24 | EXTRA_NODE = MagicMock(spec=PostgresEngine(config=test_engine.config)) 25 | EXTRA_NODE.run_querystring = AsyncMock(return_value=[]) 26 | 27 | DB = PostgresEngine( 28 | config=test_engine.config, extra_nodes={"read_1": EXTRA_NODE} 29 | ) 30 | 31 | class Manager(Table, db=DB): 32 | name = Varchar() 33 | 34 | # Make sure the node is queried 35 | Manager.select().run_sync(node="read_1") 36 | self.assertTrue(EXTRA_NODE.run_querystring.called) 37 | 38 | # Make sure that a non existent node raises an error 39 | with self.assertRaises(KeyError): 40 | Manager.select().run_sync(node="read_2") 41 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/_lilya_app.py.jinja: -------------------------------------------------------------------------------- 1 | from lilya.apps import Lilya 2 | from lilya.routing import Include, Path 3 | from lilya.staticfiles import StaticFiles 4 | from piccolo.engine import engine_finder 5 | from piccolo_admin.endpoints import create_admin 6 | from piccolo_api.crud.endpoints import PiccoloCRUD 7 | 8 | from home.endpoints import HomeController 9 | from home.piccolo_app import APP_CONFIG 10 | from home.tables import Task 11 | 12 | app = Lilya( 13 | routes=[ 14 | Path("/", HomeController), 15 | Include( 16 | "/admin/", 17 | create_admin( 18 | tables=APP_CONFIG.table_classes, 19 | # Required when running under HTTPS: 20 | # allowed_hosts=['my_site.com'] 21 | ), 22 | ), 23 | Include("/static/", StaticFiles(directory="static")), 24 | Include("/tasks/", PiccoloCRUD(table=Task)), 25 | ], 26 | ) 27 | 28 | 29 | @app.on_event("on_startup") 30 | async def open_database_connection_pool(): 31 | try: 32 | engine = engine_finder() 33 | await engine.start_connection_pool() 34 | except Exception: 35 | print("Unable to connect to the database") 36 | 37 | 38 | @app.on_event("on_shutdown") 39 | async def close_database_connection_pool(): 40 | try: 41 | engine = engine_finder() 42 | await engine.close_connection_pool() 43 | except Exception: 44 | print("Unable to connect to the database") 45 | -------------------------------------------------------------------------------- /tests/apps/asgi/commands/files/dummy_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import importlib 3 | import sys 4 | from collections.abc import Callable 5 | from typing import Union, cast 6 | 7 | from httpx import ASGITransport, AsyncClient 8 | from uvicorn import Config, Server 9 | 10 | 11 | async def dummy_server(app: Union[str, Callable] = "app:app") -> None: 12 | """ 13 | A very simplistic ASGI server. It's used to run the generated ASGI 14 | applications in unit tests. 15 | 16 | :param app: 17 | Either an ASGI app, or a string representing the path to an ASGI app. 18 | For example, ``module_1.app:app`` which would import an ASGI app called 19 | ``app`` from ``module_1.app``. 20 | 21 | """ 22 | print("Running dummy server ...") 23 | 24 | if isinstance(app, str): 25 | path, app_name = app.rsplit(":") 26 | module = importlib.import_module(path) 27 | app = cast(Callable, getattr(module, app_name)) 28 | 29 | try: 30 | async with AsyncClient(transport=ASGITransport(app=app)) as client: 31 | response = await client.get("http://localhost:8000") 32 | if response.status_code != 200: 33 | sys.exit("The app isn't callable!") 34 | except Exception: 35 | config = Config(app=app) 36 | server = Server(config=config) 37 | asyncio.create_task(server.serve()) 38 | await asyncio.sleep(0.1) 39 | 40 | 41 | if __name__ == "__main__": 42 | asyncio.run(dummy_server()) 43 | -------------------------------------------------------------------------------- /tests/example_apps/music/piccolo_migrations/2021-09-06T13-58-23-024723.py: -------------------------------------------------------------------------------- 1 | from piccolo.apps.migrations.auto import MigrationManager 2 | from piccolo.columns.base import OnDelete, OnUpdate 3 | from piccolo.columns.column_types import ForeignKey, Serial 4 | from piccolo.columns.indexes import IndexMethod 5 | from piccolo.table import Table 6 | 7 | 8 | class Concert(Table, tablename="concert"): 9 | id = Serial( 10 | null=False, 11 | primary_key=True, 12 | unique=False, 13 | index=False, 14 | index_method=IndexMethod.btree, 15 | choices=None, 16 | ) 17 | 18 | 19 | ID = "2021-09-06T13:58:23:024723" 20 | VERSION = "0.43.0" 21 | DESCRIPTION = "" 22 | 23 | 24 | async def forwards(): 25 | manager = MigrationManager( 26 | migration_id=ID, app_name="music", description=DESCRIPTION 27 | ) 28 | 29 | manager.add_column( 30 | table_class_name="Ticket", 31 | tablename="ticket", 32 | column_name="concert", 33 | column_class_name="ForeignKey", 34 | column_class=ForeignKey, 35 | params={ 36 | "references": Concert, 37 | "on_delete": OnDelete.cascade, 38 | "on_update": OnUpdate.cascade, 39 | "null": True, 40 | "primary_key": False, 41 | "unique": False, 42 | "index": False, 43 | "index_method": IndexMethod.btree, 44 | "choices": None, 45 | }, 46 | ) 47 | 48 | return manager 49 | -------------------------------------------------------------------------------- /docs/src/piccolo/getting_started/installing_piccolo.rst: -------------------------------------------------------------------------------- 1 | Installing Piccolo 2 | ================== 3 | 4 | Python 5 | ------ 6 | 7 | You need `Python 3.7 `_ or above installed on your system. 8 | 9 | ------------------------------------------------------------------------------- 10 | 11 | Pip 12 | --- 13 | 14 | Now install Piccolo, ideally inside a `virtualenv `_: 15 | 16 | .. code-block:: bash 17 | 18 | # Optional - creating a virtualenv on Unix: 19 | python3 -m venv my_project 20 | cd my_project 21 | source bin/activate 22 | 23 | # The important bit: 24 | pip install piccolo 25 | 26 | # Install Piccolo with PostgreSQL or CockroachDB driver: 27 | pip install 'piccolo[postgres]' 28 | 29 | # Install Piccolo with SQLite driver: 30 | pip install 'piccolo[sqlite]' 31 | 32 | # Optional: orjson for improved JSON serialisation performance 33 | pip install 'piccolo[orjson]' 34 | 35 | # Optional: uvloop as the default event loop instead of asyncio 36 | # If using Piccolo with Uvicorn, Uvicorn will set uvloop as the default 37 | # event loop if installed 38 | pip install 'piccolo[uvloop]' 39 | 40 | # If you just want Piccolo with all of it's functionality, you might prefer 41 | # to use this: 42 | pip install 'piccolo[all]' 43 | 44 | .. hint:: 45 | On Windows, you may need to use double quotes instead. For example 46 | ``pip install "piccolo[all]"``. 47 | -------------------------------------------------------------------------------- /tests/query/operators/test_json.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.columns import JSONB 4 | from piccolo.query.operators.json import GetChildElement, GetElementFromPath 5 | from piccolo.table import Table 6 | from tests.base import engines_skip 7 | 8 | 9 | class RecordingStudio(Table): 10 | facilities = JSONB(null=True) 11 | 12 | 13 | @engines_skip("sqlite") 14 | class TestGetChildElement(TestCase): 15 | 16 | def test_query(self): 17 | """ 18 | Make sure the generated SQL looks correct. 19 | """ 20 | querystring = GetChildElement( 21 | GetChildElement(RecordingStudio.facilities, "a"), "b" 22 | ) 23 | 24 | sql, query_args = querystring.compile_string() 25 | 26 | self.assertEqual( 27 | sql, 28 | '"recording_studio"."facilities" -> $1 -> $2', 29 | ) 30 | 31 | self.assertListEqual(query_args, ["a", "b"]) 32 | 33 | 34 | @engines_skip("sqlite") 35 | class TestGetElementFromPath(TestCase): 36 | 37 | def test_query(self): 38 | """ 39 | Make sure the generated SQL looks correct. 40 | """ 41 | querystring = GetElementFromPath( 42 | RecordingStudio.facilities, ["a", "b"] 43 | ) 44 | 45 | sql, query_args = querystring.compile_string() 46 | 47 | self.assertEqual( 48 | sql, 49 | '"recording_studio"."facilities" #> $1', 50 | ) 51 | 52 | self.assertListEqual(query_args, [["a", "b"]]) 53 | -------------------------------------------------------------------------------- /docs/src/piccolo/ecosystem/index.rst: -------------------------------------------------------------------------------- 1 | .. _Ecosystem: 2 | 3 | Ecosystem 4 | ========= 5 | 6 | Piccolo API 7 | ----------- 8 | 9 | Provides some handy utilities for creating an API around your Piccolo tables. 10 | Examples include: 11 | 12 | * Easily creating CRUD endpoints for ASGI apps, based on Piccolo tables. 13 | * Automatically creating Pydantic models from your Piccolo tables. 14 | * Great FastAPI integration. 15 | * Authentication and rate limiting. 16 | 17 | `See the docs `_ for 18 | more information. 19 | 20 | ------------------------------------------------------------------------------- 21 | 22 | .. _PiccoloAdmin: 23 | 24 | Piccolo Admin 25 | ------------- 26 | 27 | Lets you create a powerful web GUI for your tables in two minutes. View the 28 | project on `Github `_, and the 29 | `docs `_ for more information. 30 | 31 | .. image:: https://raw.githubusercontent.com/piccolo-orm/piccolo_admin/master/docs/images/screenshot.png 32 | 33 | It's a modern UI built with Vue JS, which supports powerful data filtering, and 34 | CSV exports. It's the crown jewel in the Piccolo ecosystem! 35 | 36 | ------------------------------------------------------------------------------- 37 | 38 | Piccolo Examples 39 | ---------------- 40 | 41 | A `repository `_ containing 42 | example projects built with Piccolo, as well as links to community projects. 43 | -------------------------------------------------------------------------------- /docs/src/piccolo/features/types_and_tab_completion.rst: -------------------------------------------------------------------------------- 1 | .. _tab_completion: 2 | 3 | Types and Tab Completion 4 | ======================== 5 | 6 | Type annotations 7 | ---------------- 8 | 9 | The Piccolo codebase uses type annotations extensively. This means it has great 10 | tab completion support in tools like iPython and VSCode. 11 | 12 | It also means it works well with type checkers like Mypy. 13 | 14 | To learn more about how Piccolo achieves this, read this `article about type annotations `_, 15 | and this `article about descriptors `_. 16 | 17 | ------------------------------------------------------------------------------- 18 | 19 | Troubleshooting 20 | --------------- 21 | 22 | Here are some issues you may encounter when using Mypy, or another type 23 | checker. 24 | 25 | ``id`` column doesn't exist 26 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 27 | 28 | If you don't explicitly declare a column on your table with ``primary_key=True``, 29 | Piccolo creates a ``Serial`` column for you called ``id``. 30 | 31 | In the following situation, the type checker might complains that ``id`` 32 | doesn't exist: 33 | 34 | .. code-block:: python 35 | 36 | await Band.select(Band.id) 37 | 38 | You can fix this as follows: 39 | 40 | .. code-block:: python 41 | 42 | # tables.py 43 | from piccolo.table import Table 44 | from piccolo.columns.column_types import Serial, Varchar 45 | 46 | 47 | class Band(Table): 48 | id: Serial # Add an annotation 49 | name = Varchar() 50 | -------------------------------------------------------------------------------- /piccolo/apps/migrations/auto/operations.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Optional 3 | 4 | from piccolo.columns.base import Column 5 | 6 | 7 | @dataclass 8 | class RenameTable: 9 | old_class_name: str 10 | old_tablename: str 11 | new_class_name: str 12 | new_tablename: str 13 | schema: Optional[str] = None 14 | 15 | 16 | @dataclass 17 | class ChangeTableSchema: 18 | class_name: str 19 | tablename: str 20 | old_schema: Optional[str] 21 | new_schema: Optional[str] 22 | 23 | 24 | @dataclass 25 | class RenameColumn: 26 | table_class_name: str 27 | tablename: str 28 | old_column_name: str 29 | new_column_name: str 30 | old_db_column_name: str 31 | new_db_column_name: str 32 | schema: Optional[str] = None 33 | 34 | 35 | @dataclass 36 | class AlterColumn: 37 | table_class_name: str 38 | column_name: str 39 | db_column_name: str 40 | tablename: str 41 | params: dict[str, Any] 42 | old_params: dict[str, Any] 43 | column_class: Optional[type[Column]] = None 44 | old_column_class: Optional[type[Column]] = None 45 | schema: Optional[str] = None 46 | 47 | 48 | @dataclass 49 | class DropColumn: 50 | table_class_name: str 51 | column_name: str 52 | db_column_name: str 53 | tablename: str 54 | schema: Optional[str] = None 55 | 56 | 57 | @dataclass 58 | class AddColumn: 59 | table_class_name: str 60 | column_name: str 61 | db_column_name: str 62 | column_class_name: str 63 | column_class: type[Column] 64 | params: dict[str, Any] 65 | schema: Optional[str] = None 66 | -------------------------------------------------------------------------------- /tests/columns/test_bytea.py: -------------------------------------------------------------------------------- 1 | from piccolo.columns.column_types import Bytea 2 | from piccolo.table import Table 3 | from piccolo.testing.test_case import TableTest 4 | 5 | 6 | class MyTable(Table): 7 | token = Bytea() 8 | 9 | 10 | class MyTableDefault(Table): 11 | """ 12 | Test the different default types. 13 | """ 14 | 15 | token = Bytea() 16 | token_bytes = Bytea(default=b"my-token") 17 | token_bytearray = Bytea(default=bytearray(b"my-token")) 18 | token_none = Bytea(default=None, null=True) 19 | 20 | 21 | class TestBytea(TableTest): 22 | tables = [MyTable] 23 | 24 | def test_bytea(self): 25 | """ 26 | Test storing a valid bytes value. 27 | """ 28 | row = MyTable(token=b"my-token") 29 | row.save().run_sync() 30 | self.assertEqual(row.token, b"my-token") 31 | 32 | self.assertEqual( 33 | MyTable.select(MyTable.token).first().run_sync(), 34 | {"token": b"my-token"}, 35 | ) 36 | 37 | 38 | class TestByteaDefault(TableTest): 39 | tables = [MyTableDefault] 40 | 41 | def test_json_default(self): 42 | row = MyTableDefault() 43 | row.save().run_sync() 44 | 45 | self.assertEqual(row.token, b"") 46 | self.assertEqual(row.token_bytes, b"my-token") 47 | self.assertEqual(row.token_bytearray, b"my-token") 48 | self.assertEqual(row.token_none, None) 49 | 50 | def test_invalid_default(self): 51 | with self.assertRaises(ValueError): 52 | for value in ("a", 1, ("x", "y", "z")): 53 | Bytea(default=value) # type: ignore 54 | -------------------------------------------------------------------------------- /docs/src/piccolo/query_clauses/batch.rst: -------------------------------------------------------------------------------- 1 | .. _batch: 2 | 3 | batch 4 | ===== 5 | 6 | You can use ``batch`` clauses with the following queries: 7 | 8 | * :ref:`Objects` 9 | * :ref:`Raw` 10 | * :ref:`Select` 11 | 12 | Example 13 | ------- 14 | 15 | By default, a query will return as many rows as you ask it for. The problem is 16 | when you have a table containing millions of rows - you might not want to 17 | load them all into memory at once. To get around this, you can batch the 18 | responses. 19 | 20 | .. code-block:: python 21 | 22 | # Returns 100 rows at a time: 23 | async with await Manager.select().batch(batch_size=100) as batch: 24 | async for _batch in batch: 25 | print(_batch) 26 | 27 | Node 28 | ---- 29 | 30 | If you're using ``extra_nodes`` with :class:`PostgresEngine `, 31 | you can specify which node to query: 32 | 33 | .. code-block:: python 34 | 35 | # Returns 100 rows at a time from read_replica_db 36 | async with await Manager.select().batch( 37 | batch_size=100, 38 | node="read_replica_db", 39 | ) as batch: 40 | async for _batch in batch: 41 | print(_batch) 42 | 43 | Synchronous version 44 | ------------------- 45 | 46 | There's currently no synchronous version. However, it's easy enough to achieve: 47 | 48 | .. code-block:: python 49 | 50 | async def get_batch(): 51 | async with await Manager.select().batch(batch_size=100) as batch: 52 | async for _batch in batch: 53 | print(_batch) 54 | 55 | from piccolo.utils.sync import run_sync 56 | run_sync(get_batch()) 57 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from piccolo.engine.finder import engine_finder 4 | 5 | ENGINE = engine_finder() 6 | 7 | 8 | async def drop_tables(): 9 | tables = [ 10 | "ticket", 11 | "concert", 12 | "venue", 13 | "band", 14 | "manager", 15 | "poster", 16 | "migration", 17 | "musician", 18 | "my_table", 19 | "recording_studio", 20 | "instrument", 21 | "shirt", 22 | "instrument", 23 | "mega_table", 24 | "small_table", 25 | ] 26 | assert ENGINE is not None 27 | 28 | if ENGINE.engine_type == "sqlite": 29 | # SQLite doesn't allow us to drop more than one table at a time. 30 | for table in tables: 31 | await ENGINE._run_in_new_connection( 32 | f"DROP TABLE IF EXISTS {table}" 33 | ) 34 | else: 35 | table_str = ", ".join(tables) 36 | await ENGINE._run_in_new_connection( 37 | f"DROP TABLE IF EXISTS {table_str} CASCADE" 38 | ) 39 | 40 | 41 | def pytest_sessionstart(session): 42 | """ 43 | Make sure all the tables have been dropped, just in case a previous test 44 | run was aborted part of the way through. 45 | 46 | https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_configure 47 | """ 48 | print("Session starting") 49 | asyncio.run(drop_tables()) 50 | 51 | 52 | def pytest_sessionfinish(session, exitstatus): 53 | """ 54 | https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_sessionfinish 55 | """ 56 | print("Session finishing") 57 | -------------------------------------------------------------------------------- /tests/apps/schema/commands/test_graph.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import uuid 4 | from unittest import TestCase 5 | from unittest.mock import MagicMock, patch 6 | 7 | from piccolo.apps.schema.commands.graph import graph 8 | 9 | 10 | class TestGraph(TestCase): 11 | def _verify_contents(self, file_contents: str): 12 | """ 13 | Make sure the contents of the file are correct. 14 | """ 15 | # Make sure no extra content was output at the start. 16 | self.assertTrue(file_contents.startswith("digraph model_graph")) 17 | 18 | # Make sure the tables are present 19 | self.assertTrue("TABLE_Band [label" in file_contents) 20 | self.assertTrue("TABLE_Manager [label" in file_contents) 21 | 22 | # Make sure a relation is present 23 | self.assertTrue("TABLE_Concert -> TABLE_Band" in file_contents) 24 | 25 | @patch("piccolo.apps.schema.commands.graph.print") 26 | def test_graph(self, print_: MagicMock): 27 | """ 28 | Make sure the file contents can be printed to stdout. 29 | """ 30 | graph() 31 | file_contents = print_.call_args[0][0] 32 | self._verify_contents(file_contents) 33 | 34 | def test_graph_to_file(self): 35 | """ 36 | Make sure the file contents can be written to disk. 37 | """ 38 | directory = tempfile.gettempdir() 39 | path = os.path.join(directory, f"{uuid.uuid4()}.dot") 40 | 41 | graph(output=path) 42 | 43 | with open(path, "r") as f: 44 | file_contents = f.read() 45 | 46 | self._verify_contents(file_contents) 47 | os.unlink(path) 48 | -------------------------------------------------------------------------------- /tests/columns/foreign_key/test_reverse.py: -------------------------------------------------------------------------------- 1 | from piccolo.columns import ForeignKey, Text, Varchar 2 | from piccolo.table import Table 3 | from piccolo.testing.test_case import TableTest 4 | 5 | 6 | class Band(Table): 7 | name = Varchar() 8 | 9 | 10 | class FanClub(Table): 11 | address = Text() 12 | band = ForeignKey(Band, unique=True) 13 | 14 | 15 | class Treasurer(Table): 16 | name = Varchar() 17 | fan_club = ForeignKey(FanClub, unique=True) 18 | 19 | 20 | class TestReverse(TableTest): 21 | tables = [Band, FanClub, Treasurer] 22 | 23 | def setUp(self): 24 | super().setUp() 25 | 26 | band = Band({Band.name: "Pythonistas"}) 27 | band.save().run_sync() 28 | 29 | fan_club = FanClub( 30 | {FanClub.band: band, FanClub.address: "1 Flying Circus, UK"} 31 | ) 32 | fan_club.save().run_sync() 33 | 34 | treasurer = Treasurer( 35 | {Treasurer.fan_club: fan_club, Treasurer.name: "Bob"} 36 | ) 37 | treasurer.save().run_sync() 38 | 39 | def test_reverse(self): 40 | response = Band.select( 41 | Band.name, 42 | FanClub.band.reverse().address.as_alias("address"), 43 | Treasurer.fan_club._.band.reverse().name.as_alias( 44 | "treasurer_name" 45 | ), 46 | ).run_sync() 47 | self.assertListEqual( 48 | response, 49 | [ 50 | { 51 | "name": "Pythonistas", 52 | "address": "1 Flying Circus, UK", 53 | "treasurer_name": "Bob", 54 | } 55 | ], 56 | ) 57 | -------------------------------------------------------------------------------- /piccolo/columns/operators/comparison.py: -------------------------------------------------------------------------------- 1 | from piccolo.columns.operators.base import Operator 2 | 3 | 4 | class ComparisonOperator(Operator): 5 | template = "" 6 | 7 | 8 | class IsNull(ComparisonOperator): 9 | template = "{name} IS NULL" 10 | 11 | 12 | class IsNotNull(ComparisonOperator): 13 | template = "{name} IS NOT NULL" 14 | 15 | 16 | class Equal(ComparisonOperator): 17 | template = "{name} = {value}" 18 | 19 | 20 | class NotEqual(ComparisonOperator): 21 | template = "{name} != {value}" 22 | 23 | 24 | class In(ComparisonOperator): 25 | template = "{name} IN ({values})" 26 | 27 | 28 | class NotIn(ComparisonOperator): 29 | template = "{name} NOT IN ({values})" 30 | 31 | 32 | class Like(ComparisonOperator): 33 | template = "{name} LIKE {value}" 34 | 35 | 36 | class ILike(ComparisonOperator): 37 | template = "{name} ILIKE {value}" 38 | 39 | 40 | class NotLike(ComparisonOperator): 41 | template = "{name} NOT LIKE {value}" 42 | 43 | 44 | class GreaterThan(ComparisonOperator): 45 | # Add permitted types??? 46 | template = "{name} > {value}" 47 | 48 | 49 | class GreaterEqualThan(ComparisonOperator): 50 | template = "{name} >= {value}" 51 | 52 | 53 | class LessThan(ComparisonOperator): 54 | template = "{name} < {value}" 55 | 56 | 57 | class LessEqualThan(ComparisonOperator): 58 | template = "{name} <= {value}" 59 | 60 | 61 | class ArrayAny(ComparisonOperator): 62 | template = "{value} = ANY ({name})" 63 | 64 | 65 | class ArrayNotAny(ComparisonOperator): 66 | template = "NOT {value} = ANY ({name})" 67 | 68 | 69 | class ArrayAll(ComparisonOperator): 70 | template = "{value} = ALL ({name})" 71 | -------------------------------------------------------------------------------- /tests/columns/test_reserved_column_names.py: -------------------------------------------------------------------------------- 1 | from piccolo.columns.column_types import Integer, Varchar 2 | from piccolo.table import Table 3 | from piccolo.testing.test_case import TableTest 4 | 5 | 6 | class Concert(Table): 7 | """ 8 | ``order`` is a problematic name, as it clashes with a reserved SQL keyword: 9 | 10 | https://www.postgresql.org/docs/current/sql-keywords-appendix.html 11 | 12 | """ 13 | 14 | name = Varchar() 15 | order = Integer() 16 | 17 | 18 | class TestReservedColumnNames(TableTest): 19 | """ 20 | Make sure the table works as expected, even though it has a problematic 21 | column name. 22 | """ 23 | 24 | tables = [Concert] 25 | 26 | def test_common_operations(self): 27 | # Save / Insert 28 | concert = Concert(name="Royal Albert Hall", order=1) 29 | concert.save().run_sync() 30 | self.assertEqual( 31 | Concert.select(Concert.order).run_sync(), 32 | [{"order": 1}], 33 | ) 34 | 35 | # Save / Update 36 | concert.order = 2 37 | concert.save().run_sync() 38 | self.assertEqual( 39 | Concert.select(Concert.order).run_sync(), 40 | [{"order": 2}], 41 | ) 42 | 43 | # Update 44 | Concert.update({Concert.order: 3}, force=True).run_sync() 45 | self.assertEqual( 46 | Concert.select(Concert.order).run_sync(), 47 | [{"order": 3}], 48 | ) 49 | 50 | # Delete 51 | Concert.delete().where(Concert.order == 3).run_sync() 52 | self.assertEqual( 53 | Concert.select(Concert.order).run_sync(), 54 | [], 55 | ) 56 | -------------------------------------------------------------------------------- /piccolo/apps/fixtures/commands/shared.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import TYPE_CHECKING, Any 5 | 6 | import pydantic 7 | 8 | from piccolo.conf.apps import Finder 9 | from piccolo.utils.pydantic import create_pydantic_model 10 | 11 | if TYPE_CHECKING: # pragma: no cover 12 | from piccolo.table import Table 13 | 14 | 15 | @dataclass 16 | class FixtureConfig: 17 | app_name: str 18 | table_class_names: list[str] 19 | 20 | 21 | def create_pydantic_fixture_model(fixture_configs: list[FixtureConfig]): 22 | """ 23 | Returns a nested Pydantic model for serialising and deserialising fixtures. 24 | """ 25 | columns: dict[str, Any] = {} 26 | 27 | finder = Finder() 28 | 29 | for fixture_config in fixture_configs: 30 | 31 | app_columns: dict[str, Any] = {} 32 | 33 | for table_class_name in fixture_config.table_class_names: 34 | table_class: type[Table] = finder.get_table_with_name( 35 | app_name=fixture_config.app_name, 36 | table_class_name=table_class_name, 37 | ) 38 | app_columns[table_class_name] = ( 39 | list[ # type: ignore 40 | create_pydantic_model( 41 | table_class, include_default_columns=True 42 | ) 43 | ], 44 | ..., 45 | ) 46 | 47 | app_model: Any = pydantic.create_model( 48 | f"{fixture_config.app_name.title()}Model", **app_columns 49 | ) 50 | 51 | columns[fixture_config.app_name] = (app_model, ...) 52 | 53 | return pydantic.create_model("FixtureModel", **columns) 54 | -------------------------------------------------------------------------------- /piccolo/apps/schema/commands/templates/graphviz.dot.jinja: -------------------------------------------------------------------------------- 1 | digraph model_graph { 2 | fontname = "Roboto" 3 | fontsize = 8 4 | splines = true 5 | rankdir = "{{ direction }}"; 6 | 7 | node [ 8 | fontname = "Roboto" 9 | fontsize = 8 10 | shape = "plaintext" 11 | ] 12 | 13 | edge [ 14 | fontname = "Roboto" 15 | fontsize = 8 16 | ] 17 | 18 | // Tables 19 | {% for table in tables %} 20 | TABLE_{{ table.name }} [label=< 21 | 22 | 23 | 28 | 29 | 30 | {% for column in table.columns %} 31 | 32 | 37 | 42 | 43 | {% endfor %} 44 |
24 | 25 | {{ table.name }} 26 | 27 |
33 | 34 | {{ column.name }} 35 | 36 | 38 | 39 | {{ column.type }} 40 | 41 |
45 | >] 46 | {% endfor %} 47 | 48 | // Relations 49 | {% for relation in relations %} 50 | TABLE_{{ relation.table_a }} -> TABLE_{{ relation.table_b }} 51 | [label="{{ relation.label }}"] [arrowhead=none, arrowtail=dot, dir=both]; 52 | {% endfor %} 53 | } 54 | -------------------------------------------------------------------------------- /piccolo/apps/asgi/commands/templates/app/_starlette_app.py.jinja: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | 3 | from piccolo.engine import engine_finder 4 | from piccolo_admin.endpoints import create_admin 5 | from piccolo_api.crud.endpoints import PiccoloCRUD 6 | from starlette.applications import Starlette 7 | from starlette.routing import Mount, Route 8 | from starlette.staticfiles import StaticFiles 9 | 10 | from home.endpoints import HomeEndpoint 11 | from home.piccolo_app import APP_CONFIG 12 | from home.tables import Task 13 | 14 | 15 | async def open_database_connection_pool(): 16 | try: 17 | engine = engine_finder() 18 | await engine.start_connection_pool() 19 | except Exception: 20 | print("Unable to connect to the database") 21 | 22 | 23 | async def close_database_connection_pool(): 24 | try: 25 | engine = engine_finder() 26 | await engine.close_connection_pool() 27 | except Exception: 28 | print("Unable to connect to the database") 29 | 30 | 31 | @asynccontextmanager 32 | async def lifespan(app: Starlette): 33 | await open_database_connection_pool() 34 | yield 35 | await close_database_connection_pool() 36 | 37 | 38 | app = Starlette( 39 | routes=[ 40 | Route("/", HomeEndpoint), 41 | Mount( 42 | "/admin/", 43 | create_admin( 44 | tables=APP_CONFIG.table_classes, 45 | # Required when running under HTTPS: 46 | # allowed_hosts=['my_site.com'] 47 | ), 48 | ), 49 | Mount("/static/", StaticFiles(directory="static")), 50 | Mount("/tasks/", PiccoloCRUD(table=Task)), 51 | ], 52 | lifespan=lifespan, 53 | ) 54 | -------------------------------------------------------------------------------- /tests/utils/test_dictionary.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from piccolo.utils.dictionary import make_nested 4 | 5 | 6 | class TestMakeNested(TestCase): 7 | def test_nesting(self): 8 | response = make_nested( 9 | { 10 | "id": 1, 11 | "name": "Pythonistas", 12 | "manager.id": 1, 13 | "manager.name": "Guido", 14 | "manager.car.colour": "green", 15 | } 16 | ) 17 | self.assertEqual( 18 | response, 19 | { 20 | "id": 1, 21 | "name": "Pythonistas", 22 | "manager": { 23 | "id": 1, 24 | "name": "Guido", 25 | "car": {"colour": "green"}, 26 | }, 27 | }, 28 | ) 29 | 30 | def test_name_clash(self): 31 | """ 32 | In this example, `manager` and `manager.*` could potentially clash. 33 | Nesting should take precedence. 34 | """ 35 | response = make_nested( 36 | { 37 | "id": 1, 38 | "name": "Pythonistas", 39 | "manager": 1, 40 | "manager.id": 1, 41 | "manager.name": "Guido", 42 | "manager.car.colour": "green", 43 | } 44 | ) 45 | self.assertEqual( 46 | response, 47 | { 48 | "id": 1, 49 | "name": "Pythonistas", 50 | "manager": { 51 | "id": 1, 52 | "name": "Guido", 53 | "car": {"colour": "green"}, 54 | }, 55 | }, 56 | ) 57 | -------------------------------------------------------------------------------- /piccolo/apps/user/commands/change_permissions.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Optional, Union 2 | 3 | from piccolo.apps.user.tables import BaseUser 4 | from piccolo.utils.warnings import Level, colored_string 5 | 6 | if TYPE_CHECKING: # pragma: no cover 7 | from piccolo.columns import Column 8 | 9 | 10 | async def change_permissions( 11 | username: str, 12 | admin: Optional[bool] = None, 13 | superuser: Optional[bool] = None, 14 | active: Optional[bool] = None, 15 | ): 16 | """ 17 | Change a user's permissions. 18 | 19 | :param username: 20 | Change the permissions for this user. 21 | :param admin: 22 | Set `admin` for the user (true / false). 23 | :param superuser: 24 | Set `superuser` for the user (true / false). 25 | :param active: 26 | Set `active` for the user (true / false). 27 | 28 | """ 29 | if not await BaseUser.exists().where(BaseUser.username == username).run(): 30 | print( 31 | colored_string( 32 | f"User {username} doesn't exist!", level=Level.medium 33 | ) 34 | ) 35 | return 36 | 37 | params: dict[Union[Column, str], bool] = {} 38 | 39 | if admin is not None: 40 | params[BaseUser.admin] = admin 41 | 42 | if superuser is not None: 43 | params[BaseUser.superuser] = superuser 44 | 45 | if active is not None: 46 | params[BaseUser.active] = active 47 | 48 | if params: 49 | await BaseUser.update(params).where( 50 | BaseUser.username == username 51 | ).run() 52 | else: 53 | print(colored_string("No changes detected", level=Level.medium)) 54 | return 55 | 56 | print(f"Updated permissions for {username}") 57 | --------------------------------------------------------------------------------