loom-kernel 0.1.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- loom_kernel-0.2.1/CHANGELOG.md +105 -0
- loom_kernel-0.2.1/CHANGELOG_RELEASE.md +39 -0
- loom_kernel-0.2.1/PKG-INFO +457 -0
- loom_kernel-0.2.1/README.md +422 -0
- loom_kernel-0.2.1/docs/_static/custom.css +13 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/conf.py +4 -0
- loom_kernel-0.2.1/docs/examples-repo/index.md +642 -0
- loom_kernel-0.2.1/docs/guides/autocrud.md +166 -0
- loom_kernel-0.2.1/docs/guides/celery.md +538 -0
- loom_kernel-0.2.1/docs/guides/fake-repo-examples.md +45 -0
- loom_kernel-0.2.1/docs/guides/quickstart.md +423 -0
- loom_kernel-0.2.1/docs/guides/use-case-dsl.md +477 -0
- loom_kernel-0.2.1/docs/index.rst +68 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/pyproject.toml +4 -2
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/celery/__init__.py +2 -0
- loom_kernel-0.2.1/src/loom/celery/auto.py +45 -0
- loom_kernel-0.2.1/src/loom/celery/bootstrap.py +777 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/celery/config.py +20 -0
- loom_kernel-0.2.1/src/loom/celery/constants.py +22 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/celery/runner.py +9 -7
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/celery/service.py +42 -21
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/backend/__init__.py +6 -0
- loom_kernel-0.2.1/src/loom/core/backend/core_model.py +426 -0
- loom_kernel-0.2.1/src/loom/core/backend/sqlalchemy.py +909 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/bootstrap/__init__.py +8 -1
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/bootstrap/bootstrap.py +15 -5
- loom_kernel-0.2.1/src/loom/core/bootstrap/kernel.py +100 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/cache/repository.py +107 -4
- loom_kernel-0.2.1/src/loom/core/config/keys.py +24 -0
- loom_kernel-0.2.1/src/loom/core/contracts/__init__.py +1 -0
- loom_kernel-0.2.1/src/loom/core/contracts/manifest.py +38 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/di/container.py +26 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/discovery/_utils.py +70 -27
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/discovery/interfaces.py +1 -1
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/discovery/manifest.py +30 -6
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/discovery/modules.py +1 -1
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/engine/compiler.py +1 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/engine/executor.py +26 -11
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/engine/plan.py +5 -0
- loom_kernel-0.2.1/src/loom/core/errors/codes.py +32 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/errors/errors.py +8 -6
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/job/service.py +0 -1
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/logger/config.py +23 -1
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/model/__init__.py +2 -4
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/model/base.py +7 -15
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/model/projection.py +19 -19
- loom_kernel-0.2.1/src/loom/core/model/struct.py +14 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/model/timestamped.py +4 -13
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/projection/__init__.py +8 -6
- loom_kernel-0.2.1/src/loom/core/projection/loaders.py +265 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/projection/runtime.py +89 -159
- loom_kernel-0.2.1/src/loom/core/repository/__init__.py +39 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/abc/__init__.py +15 -1
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/abc/query.py +2 -2
- loom_kernel-0.2.1/src/loom/core/repository/abc/repo_for.py +151 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/abc/repository.py +8 -0
- loom_kernel-0.2.1/src/loom/core/repository/registration.py +213 -0
- loom_kernel-0.2.1/src/loom/core/repository/registry.py +140 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/__init__.py +6 -8
- loom_kernel-0.2.1/src/loom/core/repository/sqlalchemy/loaders.py +134 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/mixins.py +179 -310
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/query_compiler/compiler.py +44 -40
- loom_kernel-0.2.1/src/loom/core/repository/sqlalchemy/registry.py +121 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/repository.py +17 -2
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/response/base.py +2 -2
- loom_kernel-0.2.1/src/loom/core/use_case/constants.py +27 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/use_case/factory.py +4 -27
- loom_kernel-0.2.1/src/loom/core/use_case/invoker.py +120 -0
- loom_kernel-0.2.1/src/loom/core/use_case/keys.py +55 -0
- loom_kernel-0.2.1/src/loom/core/use_case/registry.py +88 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/use_case/use_case.py +56 -20
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/prometheus/__init__.py +1 -1
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/prometheus/adapter.py +43 -4
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/prometheus/middleware.py +3 -8
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/autocrud.py +68 -27
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/compiler.py +4 -0
- loom_kernel-0.2.1/src/loom/rest/constants.py +29 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/errors.py +7 -6
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/fastapi/app.py +12 -6
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/fastapi/auto.py +180 -85
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/fastapi/openapi.py +17 -15
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/fastapi/router_runtime.py +13 -10
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/model.py +1 -1
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/testing/in_memory.py +29 -16
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/testing/repository_harness.py +12 -1
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/testing/runner.py +1 -1
- loom_kernel-0.2.1/tests/integration/celery_bootstrap/config/conf.celery.integration.yaml +51 -0
- loom_kernel-0.2.1/tests/integration/celery_bootstrap/test_auto_create_app_integration.py +402 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/celery_bootstrap/test_bootstrap_worker.py +6 -5
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/core/repository/sqlalchemy/test_repository_integration.py +88 -1
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/core/rest/test_auto_interface_integration.py +77 -6
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/core/rest/test_fastapi_app_integration.py +21 -10
- loom_kernel-0.2.1/tests/integration/core/use_case/test_custom_repository_integration.py +238 -0
- loom_kernel-0.2.1/tests/integration/fake_repo/config/conf.interfaces.yaml +14 -0
- loom_kernel-0.2.1/tests/integration/fake_repo/config/conf.manifest.yaml +8 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/config/conf.modules.yaml +4 -17
- loom_kernel-0.1.0/tests/integration/fake_repo/config/conf.manifest.yaml → loom_kernel-0.2.1/tests/integration/fake_repo/config/conf.yaml +0 -4
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/manifest.py +4 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/product/interface.py +6 -0
- loom_kernel-0.2.1/tests/integration/fake_repo/product/jobs.py +17 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/product/model.py +7 -21
- loom_kernel-0.2.1/tests/integration/fake_repo/product/repository.py +38 -0
- loom_kernel-0.2.1/tests/integration/fake_repo/product/repository_contract.py +14 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/product/use_cases.py +10 -0
- loom_kernel-0.2.1/tests/integration/support/__init__.py +1 -0
- loom_kernel-0.2.1/tests/integration/support/logical_repo_fixtures.py +34 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/celery_bootstrap/test_bootstrap.py +162 -3
- loom_kernel-0.2.1/tests/unit/celery_jobs/test_auto.py +73 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/celery_jobs/test_celery_service.py +8 -4
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/celery_jobs/test_runner.py +9 -4
- loom_kernel-0.2.1/tests/unit/core/backend/test_backend_compiler.py +246 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/bootstrap/test_bootstrap.py +8 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/bootstrap/test_bootstrap_metrics.py +22 -16
- loom_kernel-0.2.1/tests/unit/core/bootstrap/test_kernel.py +58 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/cache/test_cached_repository.py +120 -0
- loom_kernel-0.2.1/tests/unit/core/discovery/test_manifest.py +50 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/engine/test_executor.py +118 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/engine/test_executor_uow.py +53 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/errors/test_errors.py +8 -7
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/job/test_inline_service.py +2 -4
- loom_kernel-0.2.1/tests/unit/core/model/test_struct.py +12 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/projection/test_runtime.py +13 -29
- loom_kernel-0.2.1/tests/unit/core/repository/sqlalchemy/test_loaders.py +113 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/repository/sqlalchemy/test_repository.py +89 -1
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/use_case/test_factory.py +49 -16
- loom_kernel-0.2.1/tests/unit/core/use_case/test_invoker.py +152 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/rest/test_autocrud.py +30 -27
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/rest/test_fastapi_auto_logger.py +29 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/rest/test_rest_adapter.py +36 -1
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/rest/test_rest_compiler.py +50 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/rest/test_rest_model.py +2 -2
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/rest/test_router_runtime.py +3 -3
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/testing/test_golden.py +12 -4
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/testing/test_http_harness.py +3 -4
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/testing/test_in_memory.py +9 -3
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/testing/test_runner.py +11 -3
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/uv.lock +18 -4
- loom_kernel-0.1.0/CHANGELOG.md +0 -69
- loom_kernel-0.1.0/CHANGELOG_RELEASE.md +0 -74
- loom_kernel-0.1.0/PKG-INFO +0 -248
- loom_kernel-0.1.0/README.md +0 -215
- loom_kernel-0.1.0/docs/_static/custom.css +0 -11
- loom_kernel-0.1.0/docs/examples-repo/index.md +0 -16
- loom_kernel-0.1.0/docs/guides/autocrud.md +0 -25
- loom_kernel-0.1.0/docs/guides/fake-repo-examples.md +0 -17
- loom_kernel-0.1.0/docs/guides/quickstart.md +0 -59
- loom_kernel-0.1.0/docs/guides/use-case-dsl.md +0 -76
- loom_kernel-0.1.0/docs/index.rst +0 -38
- loom_kernel-0.1.0/src/loom/celery/bootstrap.py +0 -317
- loom_kernel-0.1.0/src/loom/core/backend/sqlalchemy.py +0 -291
- loom_kernel-0.1.0/src/loom/core/projection/loaders.py +0 -94
- loom_kernel-0.1.0/src/loom/core/repository/__init__.py +0 -21
- loom_kernel-0.1.0/src/loom/core/repository/abc/repo_for.py +0 -56
- loom_kernel-0.1.0/src/loom/core/repository/sqlalchemy/loaders.py +0 -235
- loom_kernel-0.1.0/tests/integration/fake_repo/config/conf.interfaces.yaml +0 -24
- loom_kernel-0.1.0/tests/integration/fake_repo/config/conf.yaml +0 -24
- loom_kernel-0.1.0/tests/unit/core/backend/test_backend_compiler.py +0 -114
- loom_kernel-0.1.0/tests/unit/core/repository/sqlalchemy/test_loaders.py +0 -493
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/.github/workflows/ci-main.yml +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/.github/workflows/ci-pr.yml +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/.github/workflows/docs.yml +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/.github/workflows/release.yml +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/.gitignore +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/.pre-commit-config.yaml +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/.readthedocs.yaml +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/LICENSE +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/Makefile +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/codecov.yml +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/_static/.gitkeep +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/_static/logo-transparent.png +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/_static/logo.svg +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/architecture/adr/README.md +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/architecture/clean-architecture.md +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/architecture/overview.md +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/reference/api/core.rst +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/reference/api/repository.rst +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/reference/api/rest.rst +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/reference/api/testing.rst +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/reference/index.rst +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/docs/requirements.txt +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/sonar-project.properties +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/celery/event_loop.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/backend/protocol.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/cache/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/cache/abc/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/cache/abc/backend.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/cache/abc/config.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/cache/abc/dependency.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/cache/codec.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/cache/decorators.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/cache/dependency.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/cache/gateway.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/cache/keys.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/cache/serializer.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/command/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/command/adapter.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/command/base.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/command/field.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/command/introspection.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/config/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/config/errors.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/config/loader.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/config/model.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/di/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/di/scope.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/discovery/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/discovery/base.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/engine/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/engine/compilable.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/engine/events.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/engine/metrics.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/errors/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/job/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/job/callback.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/job/context.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/job/handle.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/job/job.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/logger/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/logger/abc.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/logger/registry.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/logger/structlogger.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/model/enums.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/model/field.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/model/introspection.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/model/relation.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/model/types.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/model/types_postgres.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/mutation.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/integrity.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/model.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/profile_loader.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/projection.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/query_compiler/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/query_compiler/cursor.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/query_compiler/errors.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/query_compiler/filters.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/query_compiler/ordering.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/query_compiler/paths.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/query_compiler/subquery.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/session_manager.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/transactional.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/repository/sqlalchemy/uow.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/response/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/tracing/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/tracing/context.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/transport/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/transport/adapter.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/uow/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/uow/abc.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/uow/context.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/use_case/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/use_case/_predicates.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/use_case/compute.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/use_case/field_ref.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/use_case/markers.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/core/use_case/rule.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/adapter.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/fastapi/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/fastapi/response.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/middleware.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/rest/rest_adapter.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/testing/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/testing/golden.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/src/loom/testing/http_harness.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/conftest.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/golden/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/golden/baselines/.gitkeep +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/golden/outputs/.gitkeep +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/golden/plans/.gitkeep +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/helpers/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/celery_bootstrap/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/conftest.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/core/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/core/repository/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/core/repository/sqlalchemy/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/core/repository/sqlalchemy/conftest.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/core/repository/sqlalchemy/test_cache_integration.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/core/rest/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/core/use_case/test_use_case_crud_integration.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/config/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/main.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/product/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/product/category/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/product/category/model.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/product/category/schemas.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/product/relations.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/product/review/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/product/review/model.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/product/review/schemas.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/integration/fake_repo/product/schemas.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/celery_bootstrap/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/celery_bootstrap/test_event_loop.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/celery_jobs/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/celery_jobs/test_config.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/backend/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/bootstrap/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/command/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/command/test_command_base.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/command/test_command_field.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/command/test_command_patch.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/command/test_introspection.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/config/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/config/test_config.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/di/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/di/test_container.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/engine/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/engine/test_compiler.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/engine/test_executor_trace.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/engine/test_metrics.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/engine/test_plan.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/errors/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/job/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/job/conftest.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/job/test_callback.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/job/test_context.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/job/test_handle.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/job/test_job.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/logger/test_registry.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/model/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/model/test_model.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/model/test_timestamped.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/repository/abc/conftest.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/repository/abc/test_query.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/repository/abc/test_repository_contract.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/repository/sqlalchemy/conftest.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/repository/sqlalchemy/test_transactional.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/tracing/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/tracing/test_context.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/uow/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/uow/test_executor_uow.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/uow/test_sqlalchemy_uow.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/uow/test_uow_protocols.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/use_case/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/use_case/test_compute.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/use_case/test_field_ref.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/use_case/test_markers.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/use_case/test_rule.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/core/use_case/test_use_case.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/prometheus/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/prometheus/test_adapter.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/prometheus/test_middleware.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/rest/__init__.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/rest/test_middleware.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/rest/test_pydantic_adapter.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/rest/test_response.py +0 -0
- {loom_kernel-0.1.0 → loom_kernel-0.2.1}/tests/unit/testing/__init__.py +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# 🚀 Release 0.2.1 ([#12](https://github.com/the-reacher-data/loom-py/pull/12)) ([`87f7d1f`](https://github.com/the-reacher-data/loom-py/commit/87f7d1f1eb1ccde71d0aca1c5584b83317e30707))
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## ✨ Features
|
|
5
|
+
### logger
|
|
6
|
+
- **logger:** support per-logger levels from config
|
|
7
|
+
|
|
8
|
+
### repository
|
|
9
|
+
- **repository:** generalize main repo registration for loom structs
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## 🐛 Fixes
|
|
13
|
+
### rest
|
|
14
|
+
- **rest:** serialize pagination envelopes in camel case
|
|
15
|
+
- **rest:** support loom structs in autocrud tests
|
|
16
|
+
|
|
17
|
+
### prometheus
|
|
18
|
+
- **prometheus:** expose metrics at exact path
|
|
19
|
+
|
|
20
|
+
### review
|
|
21
|
+
- **review:** apply quick-fix pass from code review
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## 📖 Documentation
|
|
25
|
+
### repository
|
|
26
|
+
- **repository:** align custom repo examples
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## ♻️ Refactor
|
|
31
|
+
### repository
|
|
32
|
+
- **repository:** clarify sqlalchemy registration builder
|
|
33
|
+
- **repository:** inject default repository builder
|
|
34
|
+
- **repository:** simplify registration module
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# 🚀 Release 0.2.0 ([#9](https://github.com/the-reacher-data/loom-py/pull/9)) ([`2f669ab`](https://github.com/the-reacher-data/loom-py/commit/2f669ab205c7255eb6494e4cdb8ab8092817af62))
|
|
43
|
+
|
|
44
|
+
## ✨ Features
|
|
45
|
+
|
|
46
|
+
### cache
|
|
47
|
+
- **cache:** aiocache gateway with auto-inferred invalidation specs<br>
|
|
48
|
+
> CachedRepository wraps any repository with read-through/write-through caching. ONE_TO_MANY depends_on specs are auto-generated from field annotations — no explicit declaration needed. Explicit depends_on always wins.
|
|
49
|
+
|
|
50
|
+
### celery
|
|
51
|
+
- **celery:** production-ready Celery integration layer<br>
|
|
52
|
+
> CeleryJobService, persistent worker event loop, trace propagation, eager fallback, and task_default_queue routing so callbacks land on the correct consumed queue. bootstrap_worker compiles use cases, repositories, and registers Celery tasks in a single call.
|
|
53
|
+
|
|
54
|
+
- **celery:** worker job discovery from modules or manifest<br>
|
|
55
|
+
> bootstrap_worker discovers and registers Job classes automatically from module include paths (mode: modules) or from a typed WorkerManifest (mode: manifest). WorkerManifest replaces scattered JOBS/USE_CASES/INTERFACES module attributes with a single typed contract.
|
|
56
|
+
|
|
57
|
+
- **celery:** interfaces= and use_cases= on bootstrap_worker<br>
|
|
58
|
+
> Callbacks that call ApplicationInvoker need matching use-case keys compiled in the worker. interfaces= extracts use-case types from RestInterface route declarations (including AutoCRUD-generated ones). use_cases= handles non-AutoCRUD scenarios. Both can be combined with discovery mode.
|
|
59
|
+
|
|
60
|
+
### core
|
|
61
|
+
- **core:** typed repository abstractions and SQLAlchemy backend<br>
|
|
62
|
+
> Async repository protocol (RepositoryRead, RepositoryWrite, RepoFor) backed by SQLAlchemy 2.0 async session. Struct-based model system using msgspec.Struct as the single source of truth — models compile to SA mapped classes at startup via compile_all(). count() and UPDATE RETURNING included as first-class operations.
|
|
63
|
+
|
|
64
|
+
- **core:** use-case DSL with field refs, compute, rules and typed markers<br>
|
|
65
|
+
> Declarative use-case definition via Input, Load, LoadById, Exists, Compute and Rule markers. Signature inspection runs once at compile time; RuntimeExecutor drives execution from an immutable ExecutionPlan. No per-request reflection.
|
|
66
|
+
|
|
67
|
+
- **core:** ApplicationInvoker and named use-case registry<br>
|
|
68
|
+
> Use cases and job callbacks invoke other use cases by type through ApplicationInvoker without direct coupling. A named registry maps use-case keys to compiled instances at bootstrap, providing a stable cross-invocation contract.
|
|
69
|
+
|
|
70
|
+
- **core:** compiled model artifact and cache entity keys<br>
|
|
71
|
+
> compile_all() produces a typed CompiledCore artifact exposing stable entity keys used by the cache layer for deterministic repository-level invalidation across reads and writes.
|
|
72
|
+
|
|
73
|
+
- **core:** executor skips UoW for read-only use cases and GET routes<br>
|
|
74
|
+
> UseCase.read_only=True and all GET routes bypass UoW.begin/commit, removing at minimum one BEGIN+COMMIT round-trip from every read request on PostgreSQL.
|
|
75
|
+
|
|
76
|
+
### job
|
|
77
|
+
- **job:** async job domain model and orchestration primitives<br>
|
|
78
|
+
> Job[ResultT] base class with Celery routing ClassVars. JobHandle / JobGroup with dual-mode waiting (Celery + inline). JobCallback lifecycle with on_success/on_failure. Dispatch is transactionally safe — jobs flush on UoW commit and are cleared on rollback.
|
|
79
|
+
|
|
80
|
+
### observability
|
|
81
|
+
- **observability:** trace_id propagation and Prometheus adapter<br>
|
|
82
|
+
> trace_id injected into every request context and propagated to job callbacks. MetricsAdapter protocol emits execution events; PrometheusAdapter records latency histograms and error counters with low cardinality labels.
|
|
83
|
+
|
|
84
|
+
### projection
|
|
85
|
+
- **projection:** compiler-driven memory/SQL routing<br>
|
|
86
|
+
> Projections are source-agnostic at declaration time. The backend compiler decides at compile_all() whether each projection runs in-memory (relation already loaded in the active profile) or via SQL. Users declare only CountLoader, ExistsLoader, or JoinFieldsLoader — no source= parameter. Internal _Memory* and _Sql* loaders are synthesized at compile time.
|
|
87
|
+
|
|
88
|
+
### rest
|
|
89
|
+
- **rest:** AutoCRUD and FastAPI adapter<br>
|
|
90
|
+
> RestInterface.auto=True generates full CRUD routes at class definition time via build_auto_routes(). OpenAPI contracts expose query params, pagination defaults, and decoupled CreateInput/UpdateInput write DTOs. Discovery engine mounts all declared interfaces at bootstrap.
|
|
91
|
+
|
|
92
|
+
## 📖 Documentation
|
|
93
|
+
|
|
94
|
+
- Sphinx documentation platform with full public guides<br>
|
|
95
|
+
> Quickstart, use-case DSL reference, AutoCRUD guide, Celery integration guide (job definition, dispatch, callbacks, YAML reference, bootstrap options, ApplicationInvoker, Docker-compose stack), and dummy-loom examples-repo walkthrough. Deployed to Read the Docs.
|
|
96
|
+
|
|
97
|
+
## ⚡ Performance
|
|
98
|
+
|
|
99
|
+
### engine
|
|
100
|
+
- **engine:** UPDATE RETURNING replaces SELECT + flush + refresh<br>
|
|
101
|
+
> SQLAlchemyUpdateMixin.update() issues a single UPDATE ... RETURNING round-trip. Server-side onupdate expressions are pre-computed at init time and injected into the SET clause automatically.
|
|
102
|
+
|
|
103
|
+
### repository
|
|
104
|
+
- **repository:** single-query total count for offset pagination<br>
|
|
105
|
+
> list_with_query with PaginationMode.OFFSET issues a single SELECT COUNT(*) instead of a separate full-table scan, eliminating one round-trip per paginated list operation.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 🚀 Release 0.2.1 ([#12](https://github.com/the-reacher-data/loom-py/pull/12)) ([`87f7d1f`](https://github.com/the-reacher-data/loom-py/commit/87f7d1f1eb1ccde71d0aca1c5584b83317e30707))
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## ✨ Features
|
|
5
|
+
### logger
|
|
6
|
+
- **logger:** support per-logger levels from config
|
|
7
|
+
|
|
8
|
+
### repository
|
|
9
|
+
- **repository:** generalize main repo registration for loom structs
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## 🐛 Fixes
|
|
13
|
+
### rest
|
|
14
|
+
- **rest:** serialize pagination envelopes in camel case
|
|
15
|
+
- **rest:** support loom structs in autocrud tests
|
|
16
|
+
|
|
17
|
+
### prometheus
|
|
18
|
+
- **prometheus:** expose metrics at exact path
|
|
19
|
+
|
|
20
|
+
### review
|
|
21
|
+
- **review:** apply quick-fix pass from code review
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## 📖 Documentation
|
|
25
|
+
### repository
|
|
26
|
+
- **repository:** align custom repo examples
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## ♻️ Refactor
|
|
31
|
+
### repository
|
|
32
|
+
- **repository:** clarify sqlalchemy registration builder
|
|
33
|
+
- **repository:** inject default repository builder
|
|
34
|
+
- **repository:** simplify registration module
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loom-kernel
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Loom Python project
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: aiocache<1.0,>=0.12
|
|
8
|
+
Requires-Dist: msgspec<1.0,>=0.18
|
|
9
|
+
Requires-Dist: omegaconf<3.0,>=2.3
|
|
10
|
+
Requires-Dist: prometheus-client>=0.20
|
|
11
|
+
Requires-Dist: structlog<26.0,>=24.4
|
|
12
|
+
Provides-Extra: cache
|
|
13
|
+
Requires-Dist: aiocache<1.0,>=0.12; extra == 'cache'
|
|
14
|
+
Requires-Dist: omegaconf<3.0,>=2.3; extra == 'cache'
|
|
15
|
+
Provides-Extra: celery
|
|
16
|
+
Requires-Dist: celery<6.0,>=5.3; extra == 'celery'
|
|
17
|
+
Requires-Dist: kombu<6.0,>=5.3; extra == 'celery'
|
|
18
|
+
Requires-Dist: redis<6.0,>=5.0; extra == 'celery'
|
|
19
|
+
Provides-Extra: config
|
|
20
|
+
Requires-Dist: omegaconf<3.0,>=2.3; extra == 'config'
|
|
21
|
+
Provides-Extra: prometheus
|
|
22
|
+
Requires-Dist: prometheus-client>=0.20; extra == 'prometheus'
|
|
23
|
+
Provides-Extra: pyspark
|
|
24
|
+
Requires-Dist: pyspark<4.0,>=3.5; extra == 'pyspark'
|
|
25
|
+
Provides-Extra: rest
|
|
26
|
+
Requires-Dist: fastapi<1.0,>=0.115; extra == 'rest'
|
|
27
|
+
Requires-Dist: httptools<1.0,>=0.6; extra == 'rest'
|
|
28
|
+
Requires-Dist: prometheus-client>=0.20; extra == 'rest'
|
|
29
|
+
Requires-Dist: pydantic<3.0,>=2.0; extra == 'rest'
|
|
30
|
+
Requires-Dist: uvicorn<1.0,>=0.30; extra == 'rest'
|
|
31
|
+
Requires-Dist: uvloop<1.0,>=0.21; (sys_platform != 'win32') and extra == 'rest'
|
|
32
|
+
Provides-Extra: sqlalchemy
|
|
33
|
+
Requires-Dist: sqlalchemy<3.0,>=2.0; extra == 'sqlalchemy'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<img src="docs/_static/logo-transparent.png" alt="loom-kernel" width="160" style="background:#ffffff;border-radius:6px;padding:8px 20px;" />
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
# loom-kernel
|
|
41
|
+
|
|
42
|
+
[](https://github.com/the-reacher-data/loom-py/actions/workflows/ci-main.yml)
|
|
43
|
+
[](https://github.com/the-reacher-data/loom-py/actions/workflows/docs.yml)
|
|
44
|
+
[](https://sonarcloud.io/summary/new_code?id=the-reacher-data_loom-py)
|
|
45
|
+
[](https://sonarcloud.io/summary/new_code?id=the-reacher-data_loom-py)
|
|
46
|
+
[](https://sonarcloud.io/summary/new_code?id=the-reacher-data_loom-py)
|
|
47
|
+
[](https://app.codecov.io/gh/the-reacher-data/loom-py/tree/master)
|
|
48
|
+
[](https://pypi.org/project/loom-kernel/)
|
|
49
|
+
[](https://github.com/the-reacher-data/loom-py/blob/master/pyproject.toml)
|
|
50
|
+
|
|
51
|
+
Framework-agnostic Python toolkit to build backend applications with:
|
|
52
|
+
|
|
53
|
+
- **Auto-CRUD** — full REST surface from a model declaration, two lines of code
|
|
54
|
+
- typed use cases (`msgspec.Struct`) with rules, computes, and dependency injection
|
|
55
|
+
- repositories decoupled from infrastructure
|
|
56
|
+
- REST/FastAPI adapters with OpenAPI generation
|
|
57
|
+
- background jobs and Celery workers, first-class
|
|
58
|
+
- testing utilities for business workflows
|
|
59
|
+
|
|
60
|
+
## Purpose
|
|
61
|
+
|
|
62
|
+
`loom-kernel` helps you ship production APIs faster without sacrificing clean
|
|
63
|
+
architecture. Declare your domain model, describe your business rules, and let the
|
|
64
|
+
framework handle the infrastructure plumbing — DB wiring, DI, routing, serialization.
|
|
65
|
+
|
|
66
|
+
The library separates core contracts from concrete adapters so you can swap
|
|
67
|
+
infrastructure (DB, cache, transport) without breaking business logic.
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
- Usage guides and architecture docs are available in the `docs/` site.
|
|
72
|
+
- API reference is autogenerated from public docstrings.
|
|
73
|
+
- End-to-end demo application: [`dummy-loom`](https://github.com/the-reacher-data/dummy-loom).
|
|
74
|
+
|
|
75
|
+
## Main subpaths
|
|
76
|
+
|
|
77
|
+
| Subpath | What it is for |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `src/loom/core/use_case` | `UseCase` definition, rules (`Rule`), and compute steps (`Compute`). |
|
|
80
|
+
| `src/loom/core/engine` | Compilation and runtime execution of a use-case plan. |
|
|
81
|
+
| `src/loom/core/repository/abc` | Repository contracts, pagination, and typed query spec. |
|
|
82
|
+
| `src/loom/core/repository/sqlalchemy` | Concrete async SQLAlchemy repository implementation. |
|
|
83
|
+
| `src/loom/core/model` | Base model, fields, relations, and entity introspection. |
|
|
84
|
+
| `src/loom/core/cache` | Decorators and cached repository with dependency invalidation. |
|
|
85
|
+
| `src/loom/rest` | Framework-agnostic REST model and route compiler. |
|
|
86
|
+
| `src/loom/rest/fastapi` | Direct FastAPI integration (auto wiring and runtime router). |
|
|
87
|
+
| `src/loom/prometheus` | Middleware and adapter for runtime metrics. |
|
|
88
|
+
| `src/loom/testing` | Harnesses for unit/integration tests and golden tests. |
|
|
89
|
+
|
|
90
|
+
## Quick start
|
|
91
|
+
|
|
92
|
+
### 1. Define your model
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from loom.core.model import ColumnField, OnDelete, TimestampedModel
|
|
96
|
+
|
|
97
|
+
class User(TimestampedModel):
|
|
98
|
+
__tablename__ = "users"
|
|
99
|
+
id: int = ColumnField(primary_key=True, autoincrement=True)
|
|
100
|
+
full_name: str = ColumnField(length=120)
|
|
101
|
+
email: str = ColumnField(length=255, unique=True, index=True)
|
|
102
|
+
|
|
103
|
+
class Address(TimestampedModel):
|
|
104
|
+
__tablename__ = "addresses"
|
|
105
|
+
id: int = ColumnField(primary_key=True, autoincrement=True)
|
|
106
|
+
user_id: int = ColumnField(foreign_key="users.id", on_delete=OnDelete.CASCADE, index=True)
|
|
107
|
+
city: str = ColumnField(length=120)
|
|
108
|
+
country: str = ColumnField(length=120)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 2. Write use cases
|
|
112
|
+
|
|
113
|
+
Use cases declare their inputs and invariants declaratively. The engine resolves them before `execute()` runs.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import re
|
|
117
|
+
from loom.core.command import Command, Patch
|
|
118
|
+
from loom.core.errors import NotFound
|
|
119
|
+
from loom.core.use_case import Exists, F, Input, LoadById, OnMissing, Rule
|
|
120
|
+
from loom.core.use_case.use_case import UseCase
|
|
121
|
+
|
|
122
|
+
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
123
|
+
|
|
124
|
+
class CreateUser(Command, frozen=True):
|
|
125
|
+
full_name: str
|
|
126
|
+
email: str
|
|
127
|
+
|
|
128
|
+
class UpdateUser(Command, frozen=True):
|
|
129
|
+
full_name: Patch[str] = None
|
|
130
|
+
email: Patch[str] = None
|
|
131
|
+
|
|
132
|
+
def _name_must_not_be_blank(full_name: str) -> str | None:
|
|
133
|
+
return None if full_name.strip() else "full_name must not be blank"
|
|
134
|
+
|
|
135
|
+
def _email_must_be_valid(email: str) -> str | None:
|
|
136
|
+
return None if _EMAIL_RE.fullmatch(email) else "email must be valid"
|
|
137
|
+
|
|
138
|
+
class CreateUserUseCase(UseCase[User, User]):
|
|
139
|
+
rules = [
|
|
140
|
+
Rule.check(F(CreateUser).full_name, via=_name_must_not_be_blank),
|
|
141
|
+
Rule.check(F(CreateUser).email, via=_email_must_be_valid),
|
|
142
|
+
Rule.forbid(lambda _, __, exists: exists, message="email already exists").from_params("email_exists"),
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
async def execute(
|
|
146
|
+
self,
|
|
147
|
+
cmd: CreateUser = Input(),
|
|
148
|
+
email_exists: bool = Exists(User, from_command="email", against="email"),
|
|
149
|
+
) -> User:
|
|
150
|
+
return await self.main_repo.create(cmd)
|
|
151
|
+
|
|
152
|
+
class UpdateUserUseCase(UseCase[User, User | None]):
|
|
153
|
+
rules = [Rule.check(F(UpdateUser).full_name, via=_name_must_not_be_blank).when_present(F(UpdateUser).full_name)]
|
|
154
|
+
|
|
155
|
+
async def execute(
|
|
156
|
+
self,
|
|
157
|
+
user_id: int,
|
|
158
|
+
cmd: UpdateUser = Input(),
|
|
159
|
+
current_user: User = LoadById(User, by="user_id"), # loaded automatically
|
|
160
|
+
) -> User | None:
|
|
161
|
+
return await self.main_repo.update(user_id, cmd)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**`Exists`** checks a DB condition before execute runs — no boilerplate in the body.
|
|
165
|
+
**`LoadById`** fetches an entity by a path/command parameter, available in rules and the body.
|
|
166
|
+
**`Patch[T]`** marks a field as optional in partial updates; `.when_present(...)` gates rules on whether the field was sent.
|
|
167
|
+
|
|
168
|
+
### 3. Scope resources under a parent
|
|
169
|
+
|
|
170
|
+
Use `from_param` to guard nested routes (e.g. `/users/{user_id}/addresses/{address_id}`):
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from loom.core.use_case import Exists, Input, OnMissing
|
|
174
|
+
|
|
175
|
+
class CreateAddressUseCase(UseCase[Address, Address]):
|
|
176
|
+
async def execute(
|
|
177
|
+
self,
|
|
178
|
+
user_id: int,
|
|
179
|
+
cmd: CreateUserAddress = Input(),
|
|
180
|
+
_user_exists: bool = Exists(User, from_param="user_id", against="id", on_missing=OnMissing.RAISE),
|
|
181
|
+
) -> Address:
|
|
182
|
+
return await self.main_repo.create(CreateAddressRecord(user_id=user_id, **cmd.__dict__))
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`OnMissing.RAISE` returns a structured 404 automatically — no `if` in the body.
|
|
186
|
+
|
|
187
|
+
### 4. Structured queries
|
|
188
|
+
|
|
189
|
+
Build explicit queries without raw SQL:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from loom.core.repository.abc.query import (
|
|
193
|
+
FilterGroup, FilterOp, FilterSpec, PageResult, PaginationMode, QuerySpec, SortSpec,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
class ListLowStockProductsUseCase(UseCase[Product, PageResult[Product]]):
|
|
197
|
+
async def execute(self, profile: str = "default") -> PageResult[Product]:
|
|
198
|
+
query = QuerySpec(
|
|
199
|
+
filters=FilterGroup(filters=(FilterSpec(field="stock", op=FilterOp.LTE, value=5),)),
|
|
200
|
+
sort=(SortSpec(field="stock", direction="ASC"),),
|
|
201
|
+
pagination=PaginationMode.OFFSET,
|
|
202
|
+
limit=20,
|
|
203
|
+
page=1,
|
|
204
|
+
)
|
|
205
|
+
result = await self.main_repo.list_with_query(query, profile=profile)
|
|
206
|
+
if not isinstance(result, PageResult):
|
|
207
|
+
raise RuntimeError("expected offset result")
|
|
208
|
+
return result
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 5. Background jobs
|
|
212
|
+
|
|
213
|
+
Jobs are use-case-like executors that run in a queue. `LoadById` works the same way:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from loom.core.job.job import Job
|
|
217
|
+
from loom.core.use_case import Input, LoadById
|
|
218
|
+
|
|
219
|
+
class SendRestockEmailJob(Job[bool]):
|
|
220
|
+
__queue__ = "notifications"
|
|
221
|
+
|
|
222
|
+
async def execute(
|
|
223
|
+
self,
|
|
224
|
+
product_id: int,
|
|
225
|
+
cmd: SendRestockEmailCommand = Input(),
|
|
226
|
+
product: Product = LoadById(Product, by="product_id"),
|
|
227
|
+
) -> bool:
|
|
228
|
+
if product.stock > 0:
|
|
229
|
+
return False
|
|
230
|
+
# send email to cmd.recipient_email ...
|
|
231
|
+
return True
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 6. Dispatch jobs from use cases + callbacks
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from loom.core.job.service import JobService
|
|
238
|
+
from loom.core.use_case.use_case import UseCase
|
|
239
|
+
|
|
240
|
+
class DispatchRestockEmailUseCase(UseCase[Product, DispatchRestockEmailResponse]):
|
|
241
|
+
def __init__(self, job_service: JobService) -> None:
|
|
242
|
+
self._jobs = job_service
|
|
243
|
+
|
|
244
|
+
async def execute(self, product_id: str, cmd: DispatchRestockEmailCommand = Input()) -> DispatchRestockEmailResponse:
|
|
245
|
+
handle = self._jobs.dispatch(
|
|
246
|
+
SendRestockEmailJob,
|
|
247
|
+
params={"product_id": int(product_id)},
|
|
248
|
+
payload={"product_id": int(product_id), "recipient_email": cmd.recipient_email},
|
|
249
|
+
on_success=RestockEmailSuccessCallback,
|
|
250
|
+
on_failure=RestockEmailFailureCallback,
|
|
251
|
+
)
|
|
252
|
+
return DispatchRestockEmailResponse(job_id=handle.job_id, queue=handle.queue)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Callbacks are resolved by the DI container and receive the job result + context:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
class RestockEmailSuccessCallback:
|
|
259
|
+
def __init__(self, app: ApplicationInvoker) -> None:
|
|
260
|
+
self._app = app
|
|
261
|
+
|
|
262
|
+
async def on_success(self, job_id: str, result: Any, **context: Any) -> None:
|
|
263
|
+
if not result:
|
|
264
|
+
return
|
|
265
|
+
entity = self._app.entity(Product)
|
|
266
|
+
product = await entity.get(params={"id": context["product_id"]})
|
|
267
|
+
if product:
|
|
268
|
+
await entity.update(params={"id": product.id}, payload={"category": f"{product.category}-notified"})
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 7. Chain use cases (workflow pattern)
|
|
272
|
+
|
|
273
|
+
`ApplicationInvoker` lets a use case call another use case by type — no tight coupling:
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from loom.core.use_case.invoker import ApplicationInvoker
|
|
277
|
+
|
|
278
|
+
class RestockWorkflowUseCase(UseCase[Product, RestockWorkflowResponse]):
|
|
279
|
+
def __init__(self, app: ApplicationInvoker, job_service: JobService) -> None:
|
|
280
|
+
self._app = app
|
|
281
|
+
self._jobs = job_service
|
|
282
|
+
|
|
283
|
+
async def execute(self, product_id: str, cmd: DispatchRestockEmailCommand = Input()) -> RestockWorkflowResponse:
|
|
284
|
+
summary = await self._app.invoke(BuildProductSummaryUseCase, params={"product_id": int(product_id)})
|
|
285
|
+
handle = self._jobs.dispatch(SendRestockEmailJob, params={"product_id": int(product_id)}, payload={...})
|
|
286
|
+
return RestockWorkflowResponse(summary=summary.summary, restock_job_id=handle.job_id, queue=handle.queue)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 8. Declare REST interfaces
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
from loom.rest.autocrud import build_auto_routes
|
|
293
|
+
from loom.rest.model import PaginationMode, RestInterface, RestRoute
|
|
294
|
+
|
|
295
|
+
class ProductRestInterface(RestInterface[Product]):
|
|
296
|
+
prefix = "/products"
|
|
297
|
+
tags = ("Products",)
|
|
298
|
+
pagination_mode = PaginationMode.CURSOR
|
|
299
|
+
routes = (
|
|
300
|
+
RestRoute(use_case=ListLowStockProductsUseCase, method="GET", path="/low-stock",
|
|
301
|
+
summary="List low stock products"),
|
|
302
|
+
RestRoute(use_case=DispatchRestockEmailUseCase, method="POST",
|
|
303
|
+
path="/{product_id}/jobs/restock-email", status_code=202,
|
|
304
|
+
summary="Dispatch restock email"),
|
|
305
|
+
RestRoute(use_case=RestockWorkflowUseCase, method="POST",
|
|
306
|
+
path="/{product_id}/workflows/restock", status_code=202,
|
|
307
|
+
summary="Run restock workflow"),
|
|
308
|
+
*build_auto_routes(Product, ()), # adds GET, POST, PATCH, DELETE automatically
|
|
309
|
+
)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Nested resource interfaces work the same way — routes mirror the URL hierarchy:
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
class AddressRestInterface(RestInterface[Address]):
|
|
316
|
+
prefix = "/users"
|
|
317
|
+
tags = ("UserAddresses",)
|
|
318
|
+
routes = (
|
|
319
|
+
RestRoute(use_case=CreateAddressUseCase, method="POST", path="/{user_id}/addresses/", status_code=201),
|
|
320
|
+
RestRoute(use_case=ListAddressesUseCase, method="GET", path="/{user_id}/addresses/"),
|
|
321
|
+
RestRoute(use_case=GetAddressUseCase, method="GET", path="/{user_id}/addresses/{address_id}"),
|
|
322
|
+
RestRoute(use_case=UpdateAddressUseCase, method="PATCH", path="/{user_id}/addresses/{address_id}"),
|
|
323
|
+
RestRoute(use_case=DeleteAddressUseCase, method="DELETE", path="/{user_id}/addresses/{address_id}"),
|
|
324
|
+
)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### 9. Bootstrap with YAML
|
|
328
|
+
|
|
329
|
+
The `create_app()` factory wires everything — DB, cache, DI, routes — from a YAML config:
|
|
330
|
+
|
|
331
|
+
```yaml
|
|
332
|
+
# config/api.yaml
|
|
333
|
+
app:
|
|
334
|
+
name: my_store
|
|
335
|
+
code_path: src
|
|
336
|
+
discovery:
|
|
337
|
+
mode: modules
|
|
338
|
+
modules:
|
|
339
|
+
include:
|
|
340
|
+
- app.user.model
|
|
341
|
+
- app.user.interface
|
|
342
|
+
- app.product.model
|
|
343
|
+
- app.product.interface
|
|
344
|
+
rest:
|
|
345
|
+
backend: fastapi
|
|
346
|
+
title: My Store API
|
|
347
|
+
version: 0.1.0
|
|
348
|
+
|
|
349
|
+
database:
|
|
350
|
+
url: ${oc.env:DATABASE_URL,sqlite+aiosqlite:///store.db}
|
|
351
|
+
|
|
352
|
+
metrics:
|
|
353
|
+
enabled: false
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
# main.py — 3 lines
|
|
358
|
+
from loom.rest.fastapi.auto import create_app
|
|
359
|
+
|
|
360
|
+
app = create_app("config/api.yaml")
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
For larger projects, use `mode: manifest` and a manifest module:
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
# app/manifest.py
|
|
367
|
+
from app.user.model import User
|
|
368
|
+
from app.user.interface import UserRestInterface
|
|
369
|
+
|
|
370
|
+
MODELS = [User, ...]
|
|
371
|
+
INTERFACES = [UserRestInterface, ...]
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
```yaml
|
|
375
|
+
discovery:
|
|
376
|
+
mode: manifest
|
|
377
|
+
manifest:
|
|
378
|
+
module: app.manifest
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 10. Rules + Computes (advanced)
|
|
382
|
+
|
|
383
|
+
For compute-heavy write flows, declare field derivations and run them before rules:
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
from loom.core.use_case import Compute, F
|
|
387
|
+
|
|
388
|
+
class PricingPreviewUseCase(UseCase[Record, PricingPreviewResponse]):
|
|
389
|
+
computes = (
|
|
390
|
+
Compute.set(F(PricingCommand).normalized_email).from_command(
|
|
391
|
+
F(PricingCommand).email, via=lambda v: v.strip().lower(),
|
|
392
|
+
),
|
|
393
|
+
Compute.set(F(PricingCommand).subtotal).from_command(
|
|
394
|
+
F(PricingCommand).unit_price, F(PricingCommand).quantity,
|
|
395
|
+
via=lambda price, qty: price * qty,
|
|
396
|
+
),
|
|
397
|
+
Compute.set(F(PricingCommand).tax_amount).from_command(
|
|
398
|
+
F(PricingCommand).subtotal, F(PricingCommand).tax_rate,
|
|
399
|
+
via=lambda sub, rate: sub * rate,
|
|
400
|
+
),
|
|
401
|
+
)
|
|
402
|
+
rules = (
|
|
403
|
+
Rule.check(F(PricingCommand).unit_price, via=lambda v: v <= 0, message="unit_price must be > 0"),
|
|
404
|
+
Rule.check(F(PricingCommand).country, via=lambda v: v not in TAX_RATES, message="Unsupported country"),
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
async def execute(self, record_id: int, cmd: PricingCommand = Input()) -> PricingPreviewResponse:
|
|
408
|
+
...
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Computes run in declaration order — later computes can reference fields set by earlier ones.
|
|
412
|
+
|
|
413
|
+
For deeper references, review the integration examples under
|
|
414
|
+
`tests/integration/fake_repo`.
|
|
415
|
+
For a runnable full-stack sample with all patterns combined, check the companion repository
|
|
416
|
+
[`dummy-loom`](https://github.com/the-reacher-data/dummy-loom).
|
|
417
|
+
|
|
418
|
+
## Performance
|
|
419
|
+
|
|
420
|
+
`loom-kernel` adds zero measurable overhead at the concurrency levels typical of
|
|
421
|
+
production REST APIs. The benchmark below compares `loompy` (loom-kernel + SQLAlchemy)
|
|
422
|
+
against a hand-written FastAPI application hitting the same PostgreSQL database.
|
|
423
|
+
|
|
424
|
+
**Methodology:** 3 independent runs × 3 concurrency levels (20 / 100 / 300 workers),
|
|
425
|
+
median RPS reported. Dataset: 1 200 records, 3 notes each.
|
|
426
|
+
Infrastructure: each target runs in its own isolated Docker container with a dedicated
|
|
427
|
+
PostgreSQL instance.
|
|
428
|
+
|
|
429
|
+
**loompy vs hand-written FastAPI (median RPS, 3 repeats):**
|
|
430
|
+
|
|
431
|
+
| Scenario | c=20 | c=100 | c=300 |
|
|
432
|
+
| --- | --- | --- | --- |
|
|
433
|
+
| `GET /:id` with joins | ≈ tied | **+8.9 %** | −11.7 % |
|
|
434
|
+
| `LIST` cursor | −2.8 % | **+3.8 %** | −19.4 % |
|
|
435
|
+
| `LIST` offset + `COUNT(*)` | ≈ tied | **+6.2 %** | −25.4 % |
|
|
436
|
+
| `PATCH` (UPDATE RETURNING) | **+2.1 %** | ≈ tied | **+12.7 %** |
|
|
437
|
+
| `GET /ping` (no DB) | ≈ tied | −5.1 % | **+16.5 %** |
|
|
438
|
+
|
|
439
|
+
At the production sweet spot (moderate concurrency, c=100), loom-kernel matches or
|
|
440
|
+
outperforms the baseline in 4 out of 5 scenarios — without a single line of hand-tuned
|
|
441
|
+
SQL. The `GET` and `LIST` advantages come from the compiled single-pass SQL read path.
|
|
442
|
+
The `PATCH` advantage at high concurrency comes from the `UPDATE … RETURNING` pattern
|
|
443
|
+
(one round-trip vs three in a naive implementation).
|
|
444
|
+
|
|
445
|
+
The full benchmark suite is available in [`dummy-loom`](https://github.com/the-reacher-data/dummy-loom).
|
|
446
|
+
|
|
447
|
+
## Status
|
|
448
|
+
|
|
449
|
+
Project under active development.
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Author
|
|
454
|
+
|
|
455
|
+
Designed and built by [Massive Data Scope](mailto:massivedatascope@gmail.com).
|
|
456
|
+
|
|
457
|
+
For questions, feedback, or collaboration: **massivedatascope@gmail.com**
|