python-corekit 0.2.0__tar.gz → 0.3.0__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.
- {python_corekit-0.2.0 → python_corekit-0.3.0}/PKG-INFO +7 -7
- {python_corekit-0.2.0 → python_corekit-0.3.0}/README.md +6 -6
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/api/application.py +47 -9
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/api/lifespan.py +26 -3
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/concurrency/__init__.py +2 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/concurrency/decorators.py +32 -5
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/concurrency/thread_local.py +2 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/concurrency/worker.py +9 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/config/loader.py +42 -5
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/config/settings.py +11 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/__init__.py +7 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/connectable.py +45 -4
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/redis/connection.py +53 -10
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/__init__.py +2 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/connection.py +39 -5
- python_corekit-0.3.0/corekit/connections/sql/fields/__init__.py +7 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/fields/jsonb.py +13 -6
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/migration/__init__.py +4 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/migration/operations.py +69 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/operations/base.py +11 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/operations/statements.py +25 -5
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/table.py +7 -29
- python_corekit-0.3.0/corekit/crypto/__init__.py +3 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/crypto/constants.py +2 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/crypto/hasher.py +9 -4
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/data/dataset.py +8 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/data/expressions/__init__.py +3 -3
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/data/expressions/comparison.py +19 -80
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/data/expressions/expression.py +0 -32
- python_corekit-0.3.0/corekit/data/expressions/operator.py +39 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/data/stats.py +3 -0
- python_corekit-0.3.0/corekit/decorators/exception_handling.py +72 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/docker/watchdog.py +50 -31
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/__init__.py +2 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/connection.py +14 -12
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/extract/extractor.py +6 -13
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/orchestrator.py +19 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/schemas.py +2 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/transform/transformer.py +4 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/events/publisher.py +1 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/events/reader.py +26 -21
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/events/sse.py +4 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/events/websocket.py +24 -11
- python_corekit-0.3.0/corekit/exceptions/__init__.py +33 -0
- python_corekit-0.3.0/corekit/exceptions/base.py +174 -0
- python_corekit-0.3.0/corekit/exceptions/enum.py +17 -0
- python_corekit-0.3.0/corekit/exceptions/types.py +17 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/files/__init__.py +2 -4
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/files/base.py +15 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/files/enum.py +0 -5
- python_corekit-0.3.0/corekit/files/json.py +26 -0
- python_corekit-0.3.0/corekit/http/__init__.py +51 -0
- python_corekit-0.3.0/corekit/http/api.py +24 -0
- python_corekit-0.3.0/corekit/http/client.py +203 -0
- python_corekit-0.3.0/corekit/http/exceptions.py +140 -0
- python_corekit-0.3.0/corekit/http/response.py +61 -0
- python_corekit-0.3.0/corekit/http/status.py +89 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/jobs/runner.py +12 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/jobs/task.py +23 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/log_monitor/models.py +8 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/log_monitor/service.py +77 -38
- python_corekit-0.3.0/corekit/notifications/base.py +59 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/observability/__init__.py +9 -2
- python_corekit-0.3.0/corekit/observability/benchmarkable.py +30 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/observability/loggable.py +21 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/observability/request_context.py +55 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/observability/timing/timer.py +4 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/registry/__init__.py +2 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/registry/registry.py +55 -14
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/schemas/enum.py +22 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/schemas/types.py +6 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/serialization/__init__.py +2 -0
- python_corekit-0.3.0/corekit/serialization/pickle_file.py +61 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/serialization/serializable.py +22 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/serialization/serializer.py +9 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/utils/collections.py +22 -13
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/utils/payload.py +12 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/pyproject.toml +2 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/python_corekit.egg-info/PKG-INFO +7 -7
- {python_corekit-0.2.0 → python_corekit-0.3.0}/python_corekit.egg-info/SOURCES.txt +9 -3
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_application.py +46 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_architecture.py +5 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_config.py +6 -5
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_connections.py +26 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_data.py +10 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_docker.py +13 -2
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_etl.py +24 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_events.py +67 -6
- python_corekit-0.3.0/tests/test_exceptions.py +90 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_expressions.py +10 -88
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_expressions_sqlalchemy.py +26 -3
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_files.py +23 -3
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_jobs.py +32 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_lifespan.py +14 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_log_monitor.py +115 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_migration.py +42 -0
- python_corekit-0.3.0/tests/test_notifications.py +40 -0
- python_corekit-0.3.0/tests/test_observability.py +51 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_operations.py +36 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_redis.py +47 -4
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_registry.py +34 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_request_context.py +33 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_requests.py +58 -5
- python_corekit-0.3.0/tests/test_schemas_enum.py +36 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_serialization.py +25 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_sql.py +45 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_utils.py +24 -1
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_utils_collections.py +44 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_utils_payload.py +16 -0
- python_corekit-0.2.0/corekit/connections/sql/fields/__init__.py +0 -7
- python_corekit-0.2.0/corekit/constants.py +0 -45
- python_corekit-0.2.0/corekit/crypto/__init__.py +0 -1
- python_corekit-0.2.0/corekit/data/expressions/operator.py +0 -54
- python_corekit-0.2.0/corekit/decorators/exception_handling.py +0 -44
- python_corekit-0.2.0/corekit/exceptions/__init__.py +0 -18
- python_corekit-0.2.0/corekit/exceptions/base.py +0 -45
- python_corekit-0.2.0/corekit/exceptions/http/exceptions.py +0 -37
- python_corekit-0.2.0/corekit/exceptions/types.py +0 -17
- python_corekit-0.2.0/corekit/files/json.py +0 -12
- python_corekit-0.2.0/corekit/files/pickle.py +0 -12
- python_corekit-0.2.0/corekit/http/__init__.py +0 -13
- python_corekit-0.2.0/corekit/http/client.py +0 -176
- python_corekit-0.2.0/corekit/http/response.py +0 -12
- python_corekit-0.2.0/corekit/notifications/base.py +0 -51
- python_corekit-0.2.0/corekit/observability/benchmarkable.py +0 -12
- {python_corekit-0.2.0 → python_corekit-0.3.0}/LICENSE +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/api/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/api/handler.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/api/middleware.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/api/responses.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/api/routers.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/config/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/config/sources.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/decorators.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/redis/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/registry.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/migration/base.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/migration/registry.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/migration/table.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/operations/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/connections/sql/query.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/crypto/enum.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/data/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/data/expressions/target.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/data/record.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/decorators/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/decorators/warnings.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/docker/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/extract/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/extract/schemas.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/load/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/load/loader.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/load/schemas.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/transform/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/etl/transform/schemas.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/events/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/events/enum.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/events/frames.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/events/models.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/exceptions/custom/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/exceptions/http/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/files/toml.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/http/exponential_backoff.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/jobs/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/jobs/registry.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/log_monitor/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/log_monitor/constants.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/notifications/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/notifications/models.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/observability/timing/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/observability/timing/constants.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/observability/timing/split.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/py.typed +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/registry/ordered.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/schemas/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/schemas/dataclasses/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/schemas/models/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/schemas/models/arbitrary.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/schemas/models/date_models.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/schemas/pydantic/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/schemas/pydantic/fields.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/serialization/enum.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/utils/__init__.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/utils/coercion.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/utils/ids.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/utils/raise_exc.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/utils/text.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/utils/time.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/utils/validators.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/corekit/utils/void.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/python_corekit.egg-info/dependency_links.txt +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/python_corekit.egg-info/requires.txt +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/python_corekit.egg-info/top_level.txt +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/setup.cfg +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/setup.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_api.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_concurrency.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_imports.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_imports_are_top_level.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_middleware_stack.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_ordered_registry.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_utils_coercion.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_utils_ids.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_utils_text.py +0 -0
- {python_corekit-0.2.0 → python_corekit-0.3.0}/tests/test_utils_time.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-corekit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Shared foundations for Python projects: logging, benchmarking, registries, FastAPI application and routers, SQL statements and migrations, background tasks, and ETL
|
|
5
5
|
Author: Steven Jacobsen
|
|
6
6
|
License-Expression: MIT
|
|
@@ -55,7 +55,7 @@ extras to remember, and no import that fails because something was left out.
|
|
|
55
55
|
Pin a compatible release rather than tracking whatever is newest:
|
|
56
56
|
|
|
57
57
|
```
|
|
58
|
-
python-corekit~=0.
|
|
58
|
+
python-corekit~=0.3.0
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
Before 1.0, the minor version carries breaking changes.
|
|
@@ -284,7 +284,7 @@ instead, which loses results silently and so is opt-in.
|
|
|
284
284
|
## HTTP clients
|
|
285
285
|
|
|
286
286
|
```python
|
|
287
|
-
from corekit.http
|
|
287
|
+
from corekit.http import BaseApiClient
|
|
288
288
|
|
|
289
289
|
class GithubClient(BaseApiClient):
|
|
290
290
|
"""
|
|
@@ -301,7 +301,8 @@ response.data["login"]
|
|
|
301
301
|
|
|
302
302
|
Retries 429 and 5xx with exponential backoff. Every response is a
|
|
303
303
|
`BaseApiResponse`, so a non-JSON error page leaves `data` empty rather than
|
|
304
|
-
raising.
|
|
304
|
+
raising. The same methods are awaitable inside a running event loop:
|
|
305
|
+
`await client.get(...)`.
|
|
305
306
|
|
|
306
307
|
## Serialization
|
|
307
308
|
|
|
@@ -351,7 +352,7 @@ Settings are grouped by concern, so `get_settings().concurrency.max_threads`
|
|
|
351
352
|
says where a value belongs. Environment variables use a double underscore for
|
|
352
353
|
the section: `COREKIT_CONCURRENCY__MAX_THREADS=16`.
|
|
353
354
|
|
|
354
|
-
Environment variables use a `COREKIT_` prefix (`
|
|
355
|
+
Environment variables use a `COREKIT_` prefix (`COREKIT_CRYPTO__SALT`). Empty
|
|
355
356
|
values are treated as unset, because container runtimes routinely pass `FOO=`
|
|
356
357
|
for a variable that was never set.
|
|
357
358
|
|
|
@@ -375,7 +376,6 @@ Imports go downward only.
|
|
|
375
376
|
```
|
|
376
377
|
corekit/
|
|
377
378
|
config/ settings, sources, loader
|
|
378
|
-
constants.py
|
|
379
379
|
|
|
380
380
|
exceptions/ error types
|
|
381
381
|
|
|
@@ -390,7 +390,7 @@ corekit/
|
|
|
390
390
|
connections/ the Connectable lifecycle and @connect
|
|
391
391
|
sql/ SQLConnection, statements, migrations
|
|
392
392
|
redis/ RedisConnection
|
|
393
|
-
http/ BaseApiClient, retries, responses
|
|
393
|
+
http/ BaseHttpClient, BaseApiClient, retries, responses
|
|
394
394
|
|
|
395
395
|
api/ Application, lifespan, middleware, routers
|
|
396
396
|
docker/ notifications/ etl/
|
|
@@ -20,7 +20,7 @@ extras to remember, and no import that fails because something was left out.
|
|
|
20
20
|
Pin a compatible release rather than tracking whatever is newest:
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
python-corekit~=0.
|
|
23
|
+
python-corekit~=0.3.0
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Before 1.0, the minor version carries breaking changes.
|
|
@@ -249,7 +249,7 @@ instead, which loses results silently and so is opt-in.
|
|
|
249
249
|
## HTTP clients
|
|
250
250
|
|
|
251
251
|
```python
|
|
252
|
-
from corekit.http
|
|
252
|
+
from corekit.http import BaseApiClient
|
|
253
253
|
|
|
254
254
|
class GithubClient(BaseApiClient):
|
|
255
255
|
"""
|
|
@@ -266,7 +266,8 @@ response.data["login"]
|
|
|
266
266
|
|
|
267
267
|
Retries 429 and 5xx with exponential backoff. Every response is a
|
|
268
268
|
`BaseApiResponse`, so a non-JSON error page leaves `data` empty rather than
|
|
269
|
-
raising.
|
|
269
|
+
raising. The same methods are awaitable inside a running event loop:
|
|
270
|
+
`await client.get(...)`.
|
|
270
271
|
|
|
271
272
|
## Serialization
|
|
272
273
|
|
|
@@ -316,7 +317,7 @@ Settings are grouped by concern, so `get_settings().concurrency.max_threads`
|
|
|
316
317
|
says where a value belongs. Environment variables use a double underscore for
|
|
317
318
|
the section: `COREKIT_CONCURRENCY__MAX_THREADS=16`.
|
|
318
319
|
|
|
319
|
-
Environment variables use a `COREKIT_` prefix (`
|
|
320
|
+
Environment variables use a `COREKIT_` prefix (`COREKIT_CRYPTO__SALT`). Empty
|
|
320
321
|
values are treated as unset, because container runtimes routinely pass `FOO=`
|
|
321
322
|
for a variable that was never set.
|
|
322
323
|
|
|
@@ -340,7 +341,6 @@ Imports go downward only.
|
|
|
340
341
|
```
|
|
341
342
|
corekit/
|
|
342
343
|
config/ settings, sources, loader
|
|
343
|
-
constants.py
|
|
344
344
|
|
|
345
345
|
exceptions/ error types
|
|
346
346
|
|
|
@@ -355,7 +355,7 @@ corekit/
|
|
|
355
355
|
connections/ the Connectable lifecycle and @connect
|
|
356
356
|
sql/ SQLConnection, statements, migrations
|
|
357
357
|
redis/ RedisConnection
|
|
358
|
-
http/ BaseApiClient, retries, responses
|
|
358
|
+
http/ BaseHttpClient, BaseApiClient, retries, responses
|
|
359
359
|
|
|
360
360
|
api/ Application, lifespan, middleware, routers
|
|
361
361
|
docker/ notifications/ etl/
|
|
@@ -26,16 +26,34 @@ from typing import Any, Sequence
|
|
|
26
26
|
|
|
27
27
|
from fastapi import FastAPI
|
|
28
28
|
from fastapi.middleware.cors import CORSMiddleware
|
|
29
|
+
from fastapi.responses import JSONResponse
|
|
29
30
|
from fastapi.staticfiles import StaticFiles
|
|
30
31
|
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
|
31
32
|
|
|
32
33
|
from corekit.api.middleware import MiddlewareStack
|
|
33
34
|
from corekit.api.routers import CatchAllRouter, SimpleRouter, router_registry
|
|
35
|
+
from corekit.exceptions import CoreHTTPException
|
|
34
36
|
from corekit.observability.loggable import Loggable
|
|
35
37
|
|
|
36
38
|
__all__ = ["Application"]
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
def _origin_in_package(router: SimpleRouter, package_name: str) -> bool:
|
|
42
|
+
"""
|
|
43
|
+
Whether this router was constructed by ``package_name`` or a submodule.
|
|
44
|
+
"""
|
|
45
|
+
origin = router._origin_module
|
|
46
|
+
return origin == package_name or origin.startswith(f"{package_name}.")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _core_http_exception_handler(request: Any, exc: CoreHTTPException) -> JSONResponse:
|
|
50
|
+
"""
|
|
51
|
+
The same body FastAPI returns for ``HTTPException``.
|
|
52
|
+
"""
|
|
53
|
+
headers = getattr(exc, "headers", None)
|
|
54
|
+
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}, headers=headers)
|
|
55
|
+
|
|
56
|
+
|
|
39
57
|
class Application(FastAPI, Loggable):
|
|
40
58
|
"""
|
|
41
59
|
``FastAPI`` plus router discovery, middleware helpers and static mounting.
|
|
@@ -47,6 +65,7 @@ class Application(FastAPI, Loggable):
|
|
|
47
65
|
"""
|
|
48
66
|
FastAPI.__init__(self, *args, **kwargs)
|
|
49
67
|
Loggable.__init__(self)
|
|
68
|
+
self._register_http_exception_handler()
|
|
50
69
|
|
|
51
70
|
# ─── Routers ──────────────────────────────────────────────────────────
|
|
52
71
|
|
|
@@ -55,9 +74,12 @@ class Application(FastAPI, Loggable):
|
|
|
55
74
|
Import every module under ``package`` and mount the routers it defines.
|
|
56
75
|
|
|
57
76
|
Importing a module is what constructs its routers, so discovery is a
|
|
58
|
-
walk over ``package`` that lets each one register itself.
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
walk over ``package`` that lets each one register itself. Only routers
|
|
78
|
+
constructed inside that package are mounted: the registry is
|
|
79
|
+
process-wide, and an unclaimed router from some other import must not
|
|
80
|
+
ride along. A router that was included into another router is skipped
|
|
81
|
+
-- its parent already owns it -- and any ``CatchAllRouter`` is mounted
|
|
82
|
+
last.
|
|
61
83
|
|
|
62
84
|
Args:
|
|
63
85
|
package: The imported package to search, e.g. ``app.backend.routers``.
|
|
@@ -78,11 +100,12 @@ class Application(FastAPI, Loggable):
|
|
|
78
100
|
|
|
79
101
|
self._warn_on_unimportable_dirs(package)
|
|
80
102
|
|
|
81
|
-
before = len(router_registry)
|
|
82
103
|
scanned = self._import_submodules(package)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
104
|
+
# unclaimed is every top-level router in the process. Mounting that
|
|
105
|
+
# list would include routers built by some other package that happens
|
|
106
|
+
# to be imported. Origin is where the instance was constructed, which
|
|
107
|
+
# is the scanned package for a router that belongs here.
|
|
108
|
+
mountable = [router for router in router_registry.unclaimed if _origin_in_package(router, name)]
|
|
86
109
|
if not mountable:
|
|
87
110
|
raise ValueError(
|
|
88
111
|
f"No routers found in '{name}' (scanned {scanned} modules). "
|
|
@@ -90,8 +113,9 @@ class Application(FastAPI, Loggable):
|
|
|
90
113
|
f"a SimpleRouter or SmartRouter at import time."
|
|
91
114
|
)
|
|
92
115
|
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
in_package = [router for router in router_registry if _origin_in_package(router, name)]
|
|
117
|
+
claimed = len(in_package) - len(mountable)
|
|
118
|
+
self.info(f" scanned {scanned} modules, found {len(in_package)} routers")
|
|
95
119
|
if claimed > 0:
|
|
96
120
|
plural = "s" if claimed != 1 else ""
|
|
97
121
|
self.info(f" skipped {claimed} sub-router{plural} (already included by a parent)")
|
|
@@ -235,3 +259,17 @@ class Application(FastAPI, Loggable):
|
|
|
235
259
|
self.mount(url_path, StaticFiles(directory=str(path), **kwargs), name=name or url_path.strip("/"))
|
|
236
260
|
self.info(f" + static {url_path:<24} -> {path}")
|
|
237
261
|
return True
|
|
262
|
+
|
|
263
|
+
def _register_http_exception_handler(self) -> None:
|
|
264
|
+
"""
|
|
265
|
+
Handle the library's HTTP exceptions the same way FastAPI handles its own.
|
|
266
|
+
|
|
267
|
+
``CoreHTTPException`` is already an ``HTTPException``, so Starlette
|
|
268
|
+
would answer it. Registering the type explicitly keeps that response
|
|
269
|
+
-- ``{"detail": ...}`` only, never ``message`` or ``error`` -- even
|
|
270
|
+
if a later change in lookup order would otherwise miss the subclass.
|
|
271
|
+
A handler the caller already installed is left in place.
|
|
272
|
+
"""
|
|
273
|
+
if CoreHTTPException in self.exception_handlers:
|
|
274
|
+
return
|
|
275
|
+
self.add_exception_handler(CoreHTTPException, _core_http_exception_handler)
|
|
@@ -108,7 +108,9 @@ class Lifespan(Loggable):
|
|
|
108
108
|
startup: Called on the way up, in the order steps were added.
|
|
109
109
|
shutdown: Called on the way down, in reverse order. Called only if
|
|
110
110
|
``startup`` completed, so it can assume what ``startup`` builds.
|
|
111
|
-
tags:
|
|
111
|
+
tags: Labels used by ``for_tags``. Untagged steps always run; a
|
|
112
|
+
tagged step runs only when one of its tags is selected. With
|
|
113
|
+
no filter, every step runs.
|
|
112
114
|
always_shutdown: Call ``shutdown`` even when ``startup`` raised.
|
|
113
115
|
For a startup that acquires something before it can fail -- a
|
|
114
116
|
connection opened, then a handshake that throws. The hook then
|
|
@@ -136,6 +138,25 @@ class Lifespan(Loggable):
|
|
|
136
138
|
"""The registered steps, in the order they will start."""
|
|
137
139
|
return list(self._steps)
|
|
138
140
|
|
|
141
|
+
def for_tags(self, *tags: str) -> "Lifespan":
|
|
142
|
+
"""
|
|
143
|
+
A lifespan that runs the steps matching ``tags``, plus untagged ones.
|
|
144
|
+
|
|
145
|
+
An untagged step is ordinary startup and always runs. A tagged step
|
|
146
|
+
runs only when the caller asked for one of its tags, so a label is a
|
|
147
|
+
filter rather than decoration. No tags means every step, which is
|
|
148
|
+
what ``__call__`` does.
|
|
149
|
+
"""
|
|
150
|
+
if not tags:
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
wanted = set(tags)
|
|
154
|
+
selected = Lifespan()
|
|
155
|
+
for step in self._steps:
|
|
156
|
+
if not step.tags or wanted.intersection(step.tags):
|
|
157
|
+
selected._steps.append(step)
|
|
158
|
+
return selected
|
|
159
|
+
|
|
139
160
|
@asynccontextmanager
|
|
140
161
|
async def __call__(self, app: Any = None) -> AsyncIterator[None]:
|
|
141
162
|
"""
|
|
@@ -166,7 +187,8 @@ class Lifespan(Loggable):
|
|
|
166
187
|
stack.push_async_callback(self._run_shutdown, step)
|
|
167
188
|
|
|
168
189
|
if step.startup is not None:
|
|
169
|
-
|
|
190
|
+
label = step.name if not step.tags else f"{step.name} [{', '.join(step.tags)}]"
|
|
191
|
+
self.info(f" ^ {label}")
|
|
170
192
|
try:
|
|
171
193
|
await self._maybe_await(step.startup)
|
|
172
194
|
except Exception:
|
|
@@ -192,7 +214,8 @@ class Lifespan(Loggable):
|
|
|
192
214
|
self.warning(f" - {step.name} (skipped: startup did not complete)")
|
|
193
215
|
return
|
|
194
216
|
|
|
195
|
-
|
|
217
|
+
label = step.name if not step.tags else f"{step.name} [{', '.join(step.tags)}]"
|
|
218
|
+
self.info(f" v {label}")
|
|
196
219
|
try:
|
|
197
220
|
await self._maybe_await(step.shutdown)
|
|
198
221
|
except Exception:
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Concurrency primitives: per-thread storage, workers, and parallel mapping.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from corekit.concurrency.decorators import parallelize
|
|
5
|
+
from corekit.concurrency.decorators import allow_sync, parallelize
|
|
6
6
|
from corekit.concurrency.thread_local import ThreadLocalRegistry
|
|
7
7
|
from corekit.concurrency.worker import ThreadWorker
|
|
8
8
|
|
|
9
|
-
__all__ = ["ThreadLocalRegistry", "ThreadWorker", "parallelize"]
|
|
9
|
+
__all__ = ["ThreadLocalRegistry", "ThreadWorker", "allow_sync", "parallelize"]
|
|
@@ -18,19 +18,26 @@ machine rather than an error message.
|
|
|
18
18
|
|
|
19
19
|
Results arrive out of order, because they arrive as they complete. Pass
|
|
20
20
|
``ordered=True`` to get them in the order the inputs were given.
|
|
21
|
+
|
|
22
|
+
``allow_sync`` wraps an async function so a script can call it directly, while
|
|
23
|
+
an already-running event loop still gets a coroutine to await.
|
|
21
24
|
"""
|
|
22
25
|
|
|
26
|
+
import asyncio
|
|
23
27
|
import functools
|
|
28
|
+
import logging
|
|
29
|
+
from collections.abc import Awaitable, Callable
|
|
24
30
|
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
|
|
25
|
-
from typing import Any,
|
|
31
|
+
from typing import Any, Iterable, Iterator, ParamSpec, TypeVar
|
|
26
32
|
|
|
27
33
|
from corekit.config import get_settings
|
|
28
|
-
from corekit.observability import Loggable
|
|
29
34
|
|
|
30
|
-
__all__ = ["parallelize"]
|
|
35
|
+
__all__ = ["parallelize", "allow_sync"]
|
|
36
|
+
|
|
37
|
+
_logger = logging.getLogger("parallelize")
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
P = ParamSpec("P")
|
|
40
|
+
R = TypeVar("R")
|
|
34
41
|
|
|
35
42
|
|
|
36
43
|
def parallelize(
|
|
@@ -70,3 +77,23 @@ def parallelize(
|
|
|
70
77
|
return wrapper
|
|
71
78
|
|
|
72
79
|
return decorator
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def allow_sync(async_func: Callable[P, Awaitable[R]]) -> Callable[P, R | Awaitable[R]]:
|
|
83
|
+
"""
|
|
84
|
+
Call an async function from sync code, or return its coroutine if a loop is running.
|
|
85
|
+
|
|
86
|
+
A script gets the result back immediately. Inside an async app the wrapper
|
|
87
|
+
returns the coroutine, so the caller must await it — calling the wrapper
|
|
88
|
+
and ignoring the result will not run the function.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
@functools.wraps(async_func)
|
|
92
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | Awaitable[R]:
|
|
93
|
+
try:
|
|
94
|
+
asyncio.get_running_loop()
|
|
95
|
+
except RuntimeError:
|
|
96
|
+
return asyncio.run(async_func(*args, **kwargs))
|
|
97
|
+
return async_func(*args, **kwargs)
|
|
98
|
+
|
|
99
|
+
return wrapper
|
|
@@ -19,7 +19,7 @@ import threading
|
|
|
19
19
|
from typing import Any
|
|
20
20
|
|
|
21
21
|
from corekit.observability.loggable import Loggable
|
|
22
|
-
from corekit.registry import
|
|
22
|
+
from corekit.registry import normalize_key
|
|
23
23
|
|
|
24
24
|
__all__ = ["ThreadLocalRegistry"]
|
|
25
25
|
|
|
@@ -59,7 +59,7 @@ class ThreadLocalRegistry(threading.local, Loggable):
|
|
|
59
59
|
turn SQLConnection into "sqlconnection" rather than "sql_connection".
|
|
60
60
|
"""
|
|
61
61
|
name = key.__name__ if isinstance(key, type) else str(key)
|
|
62
|
-
return
|
|
62
|
+
return normalize_key(name).replace("-", "_")
|
|
63
63
|
|
|
64
64
|
def get(self, key: Any, fallback: Any = None) -> Any:
|
|
65
65
|
"""
|
|
@@ -5,6 +5,15 @@ import time
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class ThreadWorker(threading.Thread):
|
|
8
|
+
"""
|
|
9
|
+
A daemon thread that waits for ``can_start``, then calls ``process`` once.
|
|
10
|
+
|
|
11
|
+
``timeout`` and ``loop`` are stored for a subclass that wants them.
|
|
12
|
+
``run`` does not enforce ``timeout`` and does not drive ``loop``.
|
|
13
|
+
``stop`` sets ``running`` so a subclass of ``process`` can notice it; it
|
|
14
|
+
does not interrupt a thread already inside ``process``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
8
17
|
def __init__(
|
|
9
18
|
self,
|
|
10
19
|
can_start: threading.Event,
|
|
@@ -9,6 +9,8 @@ settings know only their own shape.
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
|
|
12
14
|
from corekit.config.settings import ENV_PREFIX, CorekitSettings
|
|
13
15
|
from corekit.config.sources import ConfigFileSource, ConfigSource, EnvironmentSource, PyprojectSource
|
|
14
16
|
from corekit.observability import Loggable
|
|
@@ -72,11 +74,12 @@ class SettingsLoader(Loggable):
|
|
|
72
74
|
"""
|
|
73
75
|
Read every source and construct the settings.
|
|
74
76
|
|
|
75
|
-
Invalid configuration
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
Invalid configuration drops the bad section and keeps the rest.
|
|
78
|
+
A bad thread count must not discard a valid salt or serialization
|
|
79
|
+
key that arrived from another section or the environment. If what
|
|
80
|
+
remains still will not validate, the last resort is defaults --
|
|
81
|
+
corekit has to remain importable. The log line is the difference
|
|
82
|
+
between this and silently ignoring it.
|
|
80
83
|
"""
|
|
81
84
|
values: dict[str, Any] = {}
|
|
82
85
|
for source in self.sources():
|
|
@@ -84,10 +87,44 @@ class SettingsLoader(Loggable):
|
|
|
84
87
|
|
|
85
88
|
try:
|
|
86
89
|
return CorekitSettings(**values)
|
|
90
|
+
except ValidationError as exc:
|
|
91
|
+
cleaned, dropped = self._without_invalid_sections(values, exc)
|
|
92
|
+
if dropped:
|
|
93
|
+
self.error(
|
|
94
|
+
f"Invalid corekit configuration in {', '.join(dropped)}; "
|
|
95
|
+
f"those sections fall back to defaults: {exc}"
|
|
96
|
+
)
|
|
97
|
+
try:
|
|
98
|
+
return CorekitSettings(**cleaned)
|
|
99
|
+
except ValidationError as retry_exc:
|
|
100
|
+
self.error(f"Invalid corekit configuration, falling back to defaults: {retry_exc}")
|
|
101
|
+
return CorekitSettings()
|
|
102
|
+
self.error(f"Invalid corekit configuration, falling back to defaults: {exc}")
|
|
103
|
+
return CorekitSettings()
|
|
87
104
|
except Exception as exc:
|
|
88
105
|
self.error(f"Invalid corekit configuration, falling back to defaults: {exc}")
|
|
89
106
|
return CorekitSettings()
|
|
90
107
|
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _without_invalid_sections(values: dict[str, Any], exc: ValidationError) -> tuple[dict[str, Any], list[str]]:
|
|
110
|
+
"""
|
|
111
|
+
Drop each section named in a validation error, leaving the others.
|
|
112
|
+
|
|
113
|
+
A section is the first element of the error location. A failure that
|
|
114
|
+
names no section cannot be isolated, so nothing is dropped and the
|
|
115
|
+
caller falls back to defaults.
|
|
116
|
+
"""
|
|
117
|
+
dropped: list[str] = []
|
|
118
|
+
for error in exc.errors():
|
|
119
|
+
loc = error.get("loc") or ()
|
|
120
|
+
if not loc:
|
|
121
|
+
continue
|
|
122
|
+
section = str(loc[0])
|
|
123
|
+
if section not in dropped:
|
|
124
|
+
dropped.append(section)
|
|
125
|
+
cleaned = {key: value for key, value in values.items() if key not in dropped}
|
|
126
|
+
return cleaned, dropped
|
|
127
|
+
|
|
91
128
|
@staticmethod
|
|
92
129
|
def _merge(base: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]:
|
|
93
130
|
"""
|
|
@@ -35,6 +35,10 @@ class StandardsSettings(BaseModel):
|
|
|
35
35
|
|
|
36
36
|
Off by default. These are conventions, not library invariants, and crashing
|
|
37
37
|
a consumer's application over a missing docstring would be hostile.
|
|
38
|
+
|
|
39
|
+
``strict_mode`` makes a ``SmartRegistry`` refuse a second write to a
|
|
40
|
+
normalized key instead of warning and replacing. It does not imply
|
|
41
|
+
``require_handler_docstrings``; that check stays its own flag.
|
|
38
42
|
"""
|
|
39
43
|
|
|
40
44
|
require_handler_docstrings: bool = False
|
|
@@ -80,7 +84,13 @@ class ConcurrencySettings(BaseModel):
|
|
|
80
84
|
@staticmethod
|
|
81
85
|
def cpu_default() -> int:
|
|
82
86
|
"""
|
|
83
|
-
A
|
|
87
|
+
A machine-sized thread count, for a caller that asks for one.
|
|
88
|
+
|
|
89
|
+
Not what ``resolve`` or ``parallelize`` use when the caller is silent.
|
|
90
|
+
Those use ``default_threads``. Pass this in when the work should
|
|
91
|
+
scale with the machine::
|
|
92
|
+
|
|
93
|
+
parallelize(num_threads=ConcurrencySettings.cpu_default())
|
|
84
94
|
"""
|
|
85
95
|
return min(32, (os.cpu_count() or 1) * 5)
|
|
86
96
|
|
|
@@ -17,7 +17,12 @@ implementations of the abstraction directly above them, and there is no useful
|
|
|
17
17
|
way to think about one without the other.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
from corekit.connections.connectable import
|
|
20
|
+
from corekit.connections.connectable import (
|
|
21
|
+
Connectable,
|
|
22
|
+
ConnectableType,
|
|
23
|
+
ConnectionPreference,
|
|
24
|
+
ReadOnlyConnectionError,
|
|
25
|
+
)
|
|
21
26
|
from corekit.connections.decorators import connect
|
|
22
27
|
from corekit.connections.registry import ConnectionRegistry, registry
|
|
23
28
|
|
|
@@ -26,6 +31,7 @@ __all__ = [
|
|
|
26
31
|
"ConnectableType",
|
|
27
32
|
"ConnectionPreference",
|
|
28
33
|
"ConnectionRegistry",
|
|
34
|
+
"ReadOnlyConnectionError",
|
|
29
35
|
"connect",
|
|
30
36
|
"registry",
|
|
31
37
|
]
|
|
@@ -21,22 +21,40 @@ class without importing it directly.
|
|
|
21
21
|
from abc import ABC, abstractmethod
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
24
|
+
from corekit.exceptions import InternalCoreException, Retryability
|
|
24
25
|
from corekit.observability.loggable import Loggable
|
|
25
26
|
from corekit.registry import SmartRegistry
|
|
26
27
|
from corekit.schemas.enum import StringEnum
|
|
27
28
|
|
|
28
|
-
__all__ = ["Connectable", "ConnectableType", "ConnectionPreference"]
|
|
29
|
+
__all__ = ["Connectable", "ConnectableType", "ConnectionPreference", "ReadOnlyConnectionError"]
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class ConnectionPreference(StringEnum):
|
|
32
33
|
"""
|
|
33
|
-
|
|
34
|
+
A stored hint for which path a caller prefers.
|
|
35
|
+
|
|
36
|
+
The base class records this and does not consult it. ``connect`` and
|
|
37
|
+
``async_connect`` follow the method the caller invoked, and ``@connect``
|
|
38
|
+
always opens with ``with``. A subclass may read the hint; nothing here
|
|
39
|
+
switches paths because of it.
|
|
34
40
|
"""
|
|
35
41
|
|
|
36
42
|
ASYNC = "async"
|
|
37
43
|
SYNC = "sync"
|
|
38
44
|
|
|
39
45
|
|
|
46
|
+
class ReadOnlyConnectionError(InternalCoreException):
|
|
47
|
+
"""
|
|
48
|
+
Raised when a write is attempted on a connection opened read-only.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, connection: str) -> None:
|
|
52
|
+
super().__init__(
|
|
53
|
+
message=f"{connection} is read-only",
|
|
54
|
+
retryable=Retryability.NON_RETRYABLE,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
40
58
|
class Connectable(Loggable, ABC):
|
|
41
59
|
"""
|
|
42
60
|
Base class for anything with a connection lifecycle.
|
|
@@ -97,10 +115,20 @@ class Connectable(Loggable, ABC):
|
|
|
97
115
|
@property
|
|
98
116
|
def connection_preference(self) -> ConnectionPreference:
|
|
99
117
|
"""
|
|
100
|
-
|
|
118
|
+
The path hint recorded at construction. Not used to open the connection.
|
|
101
119
|
"""
|
|
102
120
|
return self._connection_preference
|
|
103
121
|
|
|
122
|
+
def require_writable(self) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Raise if this connection was opened for reading only.
|
|
125
|
+
|
|
126
|
+
Corekit's own write methods call this. The base class does not wrap
|
|
127
|
+
every method, so a subclass with its own writes should call it too.
|
|
128
|
+
"""
|
|
129
|
+
if self._read_only:
|
|
130
|
+
raise ReadOnlyConnectionError(type(self).__name__)
|
|
131
|
+
|
|
104
132
|
def __repr__(self) -> str:
|
|
105
133
|
return f"{self.__class__.__name__}(connected={self.is_connected})"
|
|
106
134
|
|
|
@@ -190,11 +218,24 @@ class Connectable(Loggable, ABC):
|
|
|
190
218
|
self.debug(f"Terminating {self.__class__.__name__} connection")
|
|
191
219
|
self._disconnect()
|
|
192
220
|
|
|
221
|
+
def _async_is_open(self) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Whether the async path is already up.
|
|
224
|
+
|
|
225
|
+
``is_connected`` is the sync flag. Redis keeps a process-wide sync pool
|
|
226
|
+
and a separate async client, so a sync pool must not stand in for the
|
|
227
|
+
async client. Classes without ``is_async_connected`` use ``is_connected``.
|
|
228
|
+
"""
|
|
229
|
+
for cls in type(self).mro():
|
|
230
|
+
if "is_async_connected" in cls.__dict__:
|
|
231
|
+
return bool(self.is_async_connected)
|
|
232
|
+
return self.is_connected
|
|
233
|
+
|
|
193
234
|
async def async_connect(self, force_reconnect: bool = False) -> None:
|
|
194
235
|
"""
|
|
195
236
|
Connect asynchronously if not already connected.
|
|
196
237
|
"""
|
|
197
|
-
if self.
|
|
238
|
+
if self._async_is_open() and not force_reconnect:
|
|
198
239
|
return
|
|
199
240
|
|
|
200
241
|
self.debug(f"Establishing async {self.__class__.__name__} connection")
|