python-corekit 0.4.0__tar.gz → 0.4.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.
- {python_corekit-0.4.0 → python_corekit-0.4.1}/PKG-INFO +24 -1
- {python_corekit-0.4.0 → python_corekit-0.4.1}/README.md +23 -0
- python_corekit-0.4.1/corekit/exceptionator/__init__.py +68 -0
- python_corekit-0.4.1/corekit/exceptionator/actions.py +46 -0
- python_corekit-0.4.1/corekit/exceptionator/asgi.py +115 -0
- python_corekit-0.4.1/corekit/exceptionator/constants.py +7 -0
- python_corekit-0.4.1/corekit/exceptionator/exceptionator.py +62 -0
- python_corekit-0.4.1/corekit/exceptionator/fingerprint.py +86 -0
- python_corekit-0.4.1/corekit/exceptionator/guard.py +53 -0
- python_corekit-0.4.1/corekit/exceptionator/jobs.py +52 -0
- python_corekit-0.4.1/corekit/exceptionator/models.py +98 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/jobs/runner.py +22 -2
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/utils/__init__.py +2 -1
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/utils/text.py +21 -2
- {python_corekit-0.4.0 → python_corekit-0.4.1}/pyproject.toml +1 -1
- {python_corekit-0.4.0 → python_corekit-0.4.1}/python_corekit.egg-info/PKG-INFO +24 -1
- {python_corekit-0.4.0 → python_corekit-0.4.1}/python_corekit.egg-info/SOURCES.txt +10 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_architecture.py +1 -0
- python_corekit-0.4.1/tests/test_exceptionator.py +326 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_utils_text.py +16 -1
- {python_corekit-0.4.0 → python_corekit-0.4.1}/LICENSE +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/api/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/api/application.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/api/handler.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/api/lifespan.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/api/middleware.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/api/responses.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/api/routers.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/concurrency/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/concurrency/decorators.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/concurrency/thread_local.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/concurrency/worker.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/config/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/config/loader.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/config/settings.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/config/sources.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/connectable.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/decorators.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/redis/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/redis/connection.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/registry.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/connection.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/fields/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/fields/jsonb.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/migration/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/migration/base.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/migration/operations.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/migration/registry.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/migration/table.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/operations/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/operations/base.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/operations/statements.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/query.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/connections/sql/table.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/crypto/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/crypto/constants.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/crypto/enum.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/crypto/hasher.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/data/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/data/dataset.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/data/expressions/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/data/expressions/comparison.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/data/expressions/expression.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/data/expressions/operator.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/data/expressions/target.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/data/record.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/data/stats.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/decorators/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/decorators/exception_handling.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/decorators/warnings.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/docker/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/docker/watchdog.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/connection.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/extract/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/extract/extractor.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/extract/schemas.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/load/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/load/loader.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/load/schemas.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/orchestrator.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/schemas.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/transform/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/transform/schemas.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/etl/transform/transformer.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/events/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/events/enum.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/events/frames.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/events/models.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/events/publisher.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/events/reader.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/events/sse.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/events/websocket.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/exceptions/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/exceptions/base.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/exceptions/custom/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/exceptions/enum.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/exceptions/http/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/exceptions/types.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/files/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/files/base.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/files/enum.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/files/json.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/files/toml.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/http/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/http/api.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/http/client.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/http/exceptions.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/http/exponential_backoff.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/http/response.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/http/status.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/http/stream.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/jobs/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/jobs/registry.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/jobs/task.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/client.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/enum.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/events.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/messages.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/prompts/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/prompts/enum.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/prompts/exceptions.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/prompts/loader.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/prompts/template.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/protocols.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/streaming.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/tools/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/tools/base.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/tools/detection.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/tools/loop.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/tools/registry.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/llm/wire.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/log_monitor/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/log_monitor/constants.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/log_monitor/models.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/log_monitor/service.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/notifications/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/notifications/base.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/notifications/models.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/observability/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/observability/benchmarkable.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/observability/loggable.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/observability/request_context.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/observability/timing/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/observability/timing/constants.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/observability/timing/split.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/observability/timing/timer.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/py.typed +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/registry/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/registry/ordered.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/registry/registry.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/schemas/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/schemas/dataclasses/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/schemas/enum.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/schemas/models/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/schemas/models/arbitrary.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/schemas/models/date_models.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/schemas/pydantic/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/schemas/pydantic/fields.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/schemas/types.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/schemas/version.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/serialization/__init__.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/serialization/enum.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/serialization/pickle_file.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/serialization/serializable.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/serialization/serializer.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/utils/coercion.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/utils/collections.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/utils/ids.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/utils/payload.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/utils/raise_exc.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/utils/time.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/utils/validators.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/corekit/utils/void.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/python_corekit.egg-info/dependency_links.txt +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/python_corekit.egg-info/requires.txt +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/python_corekit.egg-info/top_level.txt +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/setup.cfg +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/setup.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_api.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_application.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_concurrency.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_config.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_connections.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_data.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_docker.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_etl.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_events.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_exceptions.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_expressions.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_expressions_sqlalchemy.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_files.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_imports.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_imports_are_top_level.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_jobs.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_lifespan.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_llm.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_log_monitor.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_middleware_stack.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_migration.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_notifications.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_observability.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_operations.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_ordered_registry.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_redis.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_registry.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_request_context.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_requests.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_schemas_enum.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_serialization.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_sql.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_utils.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_utils_coercion.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_utils_collections.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_utils_ids.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/tests/test_utils_payload.py +0 -0
- {python_corekit-0.4.0 → python_corekit-0.4.1}/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.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Shared foundations for Python projects: logging, benchmarking, registries, FastAPI application and routers, SQL statements and migrations, background tasks, ETL, and LLM chat/tool primitives
|
|
5
5
|
Author: Steven Jacobsen
|
|
6
6
|
License-Expression: MIT
|
|
@@ -90,6 +90,29 @@ class Report(Benchmarkable):
|
|
|
90
90
|
self.timing("queried") # logs the time since the previous split
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
## Exceptionator
|
|
94
|
+
|
|
95
|
+
Catch an exception, run one action, then rethrow so the existing failure path
|
|
96
|
+
still happens. Built-in actions are no-op and log-traceback; apps supply their
|
|
97
|
+
own (persist, notify, …).
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from corekit.exceptionator import (
|
|
101
|
+
Exceptionator,
|
|
102
|
+
ExceptionatorMiddleware,
|
|
103
|
+
LogTracebackAction,
|
|
104
|
+
install_run_task_hook,
|
|
105
|
+
set_process_exceptionator,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
exceptionator = Exceptionator(LogTracebackAction(), service="api")
|
|
109
|
+
set_process_exceptionator(exceptionator)
|
|
110
|
+
install_run_task_hook(exceptionator) # optional; run_task captures with task name
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`ExceptionatorMiddleware` is raw ASGI (not `BaseHTTPMiddleware`). With no
|
|
114
|
+
`run_task` hook installed, jobs behave as in 0.4.0.
|
|
115
|
+
|
|
93
116
|
## Assembling an application
|
|
94
117
|
|
|
95
118
|
`Application` is a plain `FastAPI` subclass — every constructor argument,
|
|
@@ -54,6 +54,29 @@ class Report(Benchmarkable):
|
|
|
54
54
|
self.timing("queried") # logs the time since the previous split
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## Exceptionator
|
|
58
|
+
|
|
59
|
+
Catch an exception, run one action, then rethrow so the existing failure path
|
|
60
|
+
still happens. Built-in actions are no-op and log-traceback; apps supply their
|
|
61
|
+
own (persist, notify, …).
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from corekit.exceptionator import (
|
|
65
|
+
Exceptionator,
|
|
66
|
+
ExceptionatorMiddleware,
|
|
67
|
+
LogTracebackAction,
|
|
68
|
+
install_run_task_hook,
|
|
69
|
+
set_process_exceptionator,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
exceptionator = Exceptionator(LogTracebackAction(), service="api")
|
|
73
|
+
set_process_exceptionator(exceptionator)
|
|
74
|
+
install_run_task_hook(exceptionator) # optional; run_task captures with task name
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`ExceptionatorMiddleware` is raw ASGI (not `BaseHTTPMiddleware`). With no
|
|
78
|
+
`run_task` hook installed, jobs behave as in 0.4.0.
|
|
79
|
+
|
|
57
80
|
## Assembling an application
|
|
58
81
|
|
|
59
82
|
`Application` is a plain `FastAPI` subclass — every constructor argument,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Process-wide exception capture: catch, run one action, rethrow.
|
|
3
|
+
|
|
4
|
+
Product concerns (Postgres, ntfy, prompts) stay in the installing application.
|
|
5
|
+
This package owns the wrapper, built-in actions, fingerprinting, ASGI middleware,
|
|
6
|
+
and the ``run_task`` hook point.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from corekit.exceptionator.actions import (
|
|
10
|
+
ExceptionatorAction,
|
|
11
|
+
LogTracebackAction,
|
|
12
|
+
NoOpAction,
|
|
13
|
+
)
|
|
14
|
+
from corekit.exceptionator.asgi import (
|
|
15
|
+
ExceptionatorMiddleware,
|
|
16
|
+
get_process_exceptionator,
|
|
17
|
+
record_http_exception,
|
|
18
|
+
set_process_exceptionator,
|
|
19
|
+
should_record_http_exception,
|
|
20
|
+
)
|
|
21
|
+
from corekit.exceptionator.exceptionator import Exceptionator
|
|
22
|
+
from corekit.exceptionator.fingerprint import ExceptionFingerprint, ExceptionText
|
|
23
|
+
from corekit.exceptionator.guard import (
|
|
24
|
+
begin_handling,
|
|
25
|
+
end_handling,
|
|
26
|
+
handling_active,
|
|
27
|
+
is_captured,
|
|
28
|
+
mark_captured,
|
|
29
|
+
)
|
|
30
|
+
from corekit.exceptionator.jobs import (
|
|
31
|
+
clear_run_task_hook,
|
|
32
|
+
get_run_task_hook,
|
|
33
|
+
install_run_task_hook,
|
|
34
|
+
)
|
|
35
|
+
from corekit.exceptionator.models import (
|
|
36
|
+
CapturedException,
|
|
37
|
+
ExceptionatorActionName,
|
|
38
|
+
ExceptionContext,
|
|
39
|
+
ExceptionSource,
|
|
40
|
+
build_captured,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"CapturedException",
|
|
45
|
+
"ExceptionContext",
|
|
46
|
+
"ExceptionFingerprint",
|
|
47
|
+
"ExceptionSource",
|
|
48
|
+
"ExceptionText",
|
|
49
|
+
"Exceptionator",
|
|
50
|
+
"ExceptionatorAction",
|
|
51
|
+
"ExceptionatorActionName",
|
|
52
|
+
"ExceptionatorMiddleware",
|
|
53
|
+
"LogTracebackAction",
|
|
54
|
+
"NoOpAction",
|
|
55
|
+
"begin_handling",
|
|
56
|
+
"build_captured",
|
|
57
|
+
"clear_run_task_hook",
|
|
58
|
+
"end_handling",
|
|
59
|
+
"get_process_exceptionator",
|
|
60
|
+
"get_run_task_hook",
|
|
61
|
+
"handling_active",
|
|
62
|
+
"install_run_task_hook",
|
|
63
|
+
"is_captured",
|
|
64
|
+
"mark_captured",
|
|
65
|
+
"record_http_exception",
|
|
66
|
+
"set_process_exceptionator",
|
|
67
|
+
"should_record_http_exception",
|
|
68
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Built-in Exceptionator actions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from corekit.exceptionator.models import CapturedException
|
|
8
|
+
from corekit.observability.loggable import Loggable
|
|
9
|
+
|
|
10
|
+
__all__ = ["ExceptionatorAction", "LogTracebackAction", "NoOpAction"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExceptionatorAction(Loggable):
|
|
14
|
+
"""
|
|
15
|
+
Work performed after an exception is captured and before it continues.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def run(self, captured: CapturedException) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Perform the action. Must not raise.
|
|
21
|
+
"""
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NoOpAction(ExceptionatorAction):
|
|
26
|
+
"""
|
|
27
|
+
Catch and continue. Records nothing and publishes nothing.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def run(self, captured: CapturedException) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Do nothing with the captured exception.
|
|
33
|
+
"""
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LogTracebackAction(ExceptionatorAction):
|
|
38
|
+
"""
|
|
39
|
+
Write the message and traceback. Does not store or notify.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def run(self, captured: CapturedException) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Log the incident text.
|
|
45
|
+
"""
|
|
46
|
+
self.error(f"{captured.service} {captured.exception_type}: {captured.message}\n{captured.traceback_text}")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ASGI middleware and HTTP helpers for Exceptionator.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Awaitable, Callable
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from fastapi import Request
|
|
11
|
+
from fastapi.exceptions import RequestValidationError
|
|
12
|
+
from fastapi.responses import JSONResponse
|
|
13
|
+
from starlette.exceptions import HTTPException
|
|
14
|
+
from starlette.websockets import WebSocketDisconnect
|
|
15
|
+
|
|
16
|
+
from corekit.exceptionator.exceptionator import Exceptionator
|
|
17
|
+
from corekit.exceptionator.models import ExceptionContext, ExceptionSource
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ExceptionatorMiddleware",
|
|
21
|
+
"get_process_exceptionator",
|
|
22
|
+
"record_http_exception",
|
|
23
|
+
"set_process_exceptionator",
|
|
24
|
+
"should_record_http_exception",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
ASGIApp = Callable[..., Awaitable[None]]
|
|
28
|
+
|
|
29
|
+
_process_exceptionator: Exceptionator | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def set_process_exceptionator(exceptionator: Exceptionator) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Remember the process Exceptionator for middleware and HTTP handlers.
|
|
35
|
+
"""
|
|
36
|
+
global _process_exceptionator
|
|
37
|
+
_process_exceptionator = exceptionator
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_process_exceptionator() -> Exceptionator:
|
|
41
|
+
"""
|
|
42
|
+
The Exceptionator installed for this process.
|
|
43
|
+
"""
|
|
44
|
+
if _process_exceptionator is None:
|
|
45
|
+
raise RuntimeError("No Exceptionator installed; call set_process_exceptionator first")
|
|
46
|
+
return _process_exceptionator
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def should_record_http_exception(exc: BaseException) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Client errors and validation failures are not incidents. HTTP 500s are.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(exc, RequestValidationError):
|
|
54
|
+
return False
|
|
55
|
+
if isinstance(exc, WebSocketDisconnect):
|
|
56
|
+
return False
|
|
57
|
+
status = getattr(exc, "status_code", None)
|
|
58
|
+
if isinstance(exc, HTTPException) and isinstance(status, int) and status < 500:
|
|
59
|
+
return False
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ExceptionatorMiddleware:
|
|
64
|
+
"""
|
|
65
|
+
ASGI middleware that records unhandled HTTP failures and rethrows them.
|
|
66
|
+
|
|
67
|
+
Not ``BaseHTTPMiddleware``: that wrapper breaks streaming and exception groups.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, app: ASGIApp, exceptionator: Exceptionator | None = None) -> None:
|
|
71
|
+
self.app = app
|
|
72
|
+
self._exceptionator = exceptionator
|
|
73
|
+
|
|
74
|
+
async def __call__(self, scope: dict[str, Any], receive: Any, send: Any) -> None:
|
|
75
|
+
if scope.get("type") != "http":
|
|
76
|
+
await self.app(scope, receive, send)
|
|
77
|
+
return
|
|
78
|
+
try:
|
|
79
|
+
await self.app(scope, receive, send)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
if should_record_http_exception(exc):
|
|
82
|
+
self._resolve().capture(
|
|
83
|
+
exc,
|
|
84
|
+
context=ExceptionContext(
|
|
85
|
+
source=ExceptionSource.HTTP,
|
|
86
|
+
http_method=scope.get("method"),
|
|
87
|
+
http_path=scope.get("path"),
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
def _resolve(self) -> Exceptionator:
|
|
93
|
+
if self._exceptionator is not None:
|
|
94
|
+
return self._exceptionator
|
|
95
|
+
return get_process_exceptionator()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def record_http_exception(request: Request, exc: HTTPException) -> JSONResponse:
|
|
99
|
+
"""
|
|
100
|
+
Store HTTP 500s, then return the same JSON body FastAPI would have sent.
|
|
101
|
+
|
|
102
|
+
Starlette answers ``HTTPException`` inside the router, so the ASGI middleware
|
|
103
|
+
never sees it. 4xx responses are returned unchanged and are not incidents.
|
|
104
|
+
"""
|
|
105
|
+
if should_record_http_exception(exc):
|
|
106
|
+
get_process_exceptionator().capture(
|
|
107
|
+
exc,
|
|
108
|
+
context=ExceptionContext(
|
|
109
|
+
source=ExceptionSource.HTTP,
|
|
110
|
+
http_method=request.method,
|
|
111
|
+
http_path=request.url.path,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
headers = getattr(exc, "headers", None)
|
|
115
|
+
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}, headers=headers)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Catch an exception, run one action, and let the caller rethrow.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
|
|
10
|
+
from corekit.exceptionator.actions import ExceptionatorAction
|
|
11
|
+
from corekit.exceptionator.guard import (
|
|
12
|
+
begin_handling,
|
|
13
|
+
end_handling,
|
|
14
|
+
handling_active,
|
|
15
|
+
is_captured,
|
|
16
|
+
mark_captured,
|
|
17
|
+
)
|
|
18
|
+
from corekit.exceptionator.models import CapturedException, ExceptionContext, build_captured
|
|
19
|
+
from corekit.observability.loggable import Loggable
|
|
20
|
+
|
|
21
|
+
__all__ = ["Exceptionator"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Exceptionator(Loggable):
|
|
25
|
+
"""
|
|
26
|
+
Catch an exception, run the configured action, and let the caller rethrow.
|
|
27
|
+
|
|
28
|
+
``guard`` rethrows. ``capture`` runs the same action and returns, for a
|
|
29
|
+
caller that has already decided to swallow the error.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, action: ExceptionatorAction, *, service: str) -> None:
|
|
33
|
+
super().__init__()
|
|
34
|
+
self._action = action
|
|
35
|
+
self.service = service
|
|
36
|
+
|
|
37
|
+
@contextmanager
|
|
38
|
+
def guard(self, context: ExceptionContext | None = None) -> Iterator[None]:
|
|
39
|
+
"""
|
|
40
|
+
Run the action when the block raises, then rethrow the original exception.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
yield
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
self.capture(exc, context=context)
|
|
46
|
+
raise
|
|
47
|
+
|
|
48
|
+
def capture(self, exc: BaseException, *, context: ExceptionContext | None = None) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Run the action once for this exception. Later calls on the same object no-op.
|
|
51
|
+
"""
|
|
52
|
+
if handling_active() or is_captured(exc):
|
|
53
|
+
return
|
|
54
|
+
token = begin_handling()
|
|
55
|
+
try:
|
|
56
|
+
captured: CapturedException = build_captured(exc, service=self.service, context=context)
|
|
57
|
+
mark_captured(exc)
|
|
58
|
+
self._action.run(captured)
|
|
59
|
+
except Exception:
|
|
60
|
+
self.exception("Exceptionator failed while recording an exception")
|
|
61
|
+
finally:
|
|
62
|
+
end_handling(token)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Identity for an exception based on its type and innermost frames.
|
|
3
|
+
|
|
4
|
+
The message is left out so ids embedded in the text do not split one bug into
|
|
5
|
+
many fingerprints.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import traceback
|
|
12
|
+
from types import TracebackType
|
|
13
|
+
from typing import ClassVar
|
|
14
|
+
|
|
15
|
+
from corekit.exceptionator.constants import FINGERPRINT_FRAMES
|
|
16
|
+
from corekit.utils.text import truncate
|
|
17
|
+
|
|
18
|
+
__all__ = ["ExceptionFingerprint", "ExceptionText"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExceptionText:
|
|
22
|
+
"""
|
|
23
|
+
Backend text extracted from an exception.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def message(exc: BaseException) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Backend message. Public exceptions stringify to user copy, so prefer ``for_log``.
|
|
30
|
+
"""
|
|
31
|
+
for_log = getattr(exc, "for_log", None)
|
|
32
|
+
if callable(for_log):
|
|
33
|
+
try:
|
|
34
|
+
return str(for_log())
|
|
35
|
+
except Exception:
|
|
36
|
+
return str(exc)
|
|
37
|
+
return str(exc)
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def first_line(text: str, limit: int) -> str:
|
|
41
|
+
"""
|
|
42
|
+
The first line of ``text``, truncated to ``limit`` characters.
|
|
43
|
+
"""
|
|
44
|
+
line = text.splitlines()[0] if text else ""
|
|
45
|
+
return truncate(line, limit)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ExceptionFingerprint:
|
|
49
|
+
"""
|
|
50
|
+
Stable identity for an exception based on its type and innermost frames.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
path_markers: ClassVar[tuple[str, ...]] = ()
|
|
54
|
+
|
|
55
|
+
def of(self, exc: BaseException) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Hash the exception type plus the innermost stack frames.
|
|
58
|
+
"""
|
|
59
|
+
frames = traceback.extract_tb(exc.__traceback__)[-FINGERPRINT_FRAMES:]
|
|
60
|
+
parts = [type(exc).__qualname__]
|
|
61
|
+
for frame in frames:
|
|
62
|
+
parts.append(f"{self.normalize_path(frame.filename)}|{frame.name}|{frame.lineno}")
|
|
63
|
+
return hashlib.sha256("\n".join(parts).encode()).hexdigest()
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def normalize_path(cls, filename: str) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Drop machine-specific prefixes so a container path and a checkout match.
|
|
69
|
+
"""
|
|
70
|
+
normalized = filename.replace("\\", "/")
|
|
71
|
+
for marker in cls.path_markers:
|
|
72
|
+
index = normalized.find(marker)
|
|
73
|
+
if index >= 0:
|
|
74
|
+
return normalized[index:]
|
|
75
|
+
parts = [part for part in normalized.split("/") if part]
|
|
76
|
+
if not parts:
|
|
77
|
+
return normalized
|
|
78
|
+
return "/".join(parts[-2:])
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def format_traceback(exc: BaseException) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Render the traceback text for storage.
|
|
84
|
+
"""
|
|
85
|
+
tb: TracebackType | None = exc.__traceback__
|
|
86
|
+
return "".join(traceback.format_exception(type(exc), exc, tb))
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ContextVar and per-exception marker so one failure is recorded once.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from contextvars import ContextVar, Token
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"begin_handling",
|
|
11
|
+
"end_handling",
|
|
12
|
+
"handling_active",
|
|
13
|
+
"is_captured",
|
|
14
|
+
"mark_captured",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
CAPTURED_MARKER = "_exceptionator_captured"
|
|
18
|
+
_handling: ContextVar[bool] = ContextVar("exceptionator_handling", default=False)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_captured(exc: BaseException) -> bool:
|
|
22
|
+
"""
|
|
23
|
+
Whether this exception object was already handed to an Exceptionator.
|
|
24
|
+
"""
|
|
25
|
+
return bool(getattr(exc, CAPTURED_MARKER, False))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def mark_captured(exc: BaseException) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Remember that this exception object has been handled.
|
|
31
|
+
"""
|
|
32
|
+
setattr(exc, CAPTURED_MARKER, True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def handling_active() -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Whether this task is already inside the Exceptionator.
|
|
38
|
+
"""
|
|
39
|
+
return _handling.get()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def begin_handling() -> Token[bool]:
|
|
43
|
+
"""
|
|
44
|
+
Mark this task as inside the Exceptionator. Pair with ``end_handling``.
|
|
45
|
+
"""
|
|
46
|
+
return _handling.set(True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def end_handling(token: Token[bool]) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Leave the Exceptionator guard.
|
|
52
|
+
"""
|
|
53
|
+
_handling.reset(token)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hook ``run_task`` calls after a task fails and before ``failure()``.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from corekit.exceptionator.models import ExceptionContext, ExceptionSource
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from corekit.exceptionator.exceptionator import Exceptionator
|
|
14
|
+
|
|
15
|
+
__all__ = ["clear_run_task_hook", "get_run_task_hook", "install_run_task_hook"]
|
|
16
|
+
|
|
17
|
+
RunTaskHook = Callable[[str, BaseException], None]
|
|
18
|
+
|
|
19
|
+
_hook: RunTaskHook | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def install_run_task_hook(exceptionator: Exceptionator) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Register ``exceptionator`` so ``run_task`` captures failures with the task name.
|
|
25
|
+
|
|
26
|
+
Passing the same instance again is a no-op. Call ``clear_run_task_hook`` to
|
|
27
|
+
remove it. With no hook registered, ``run_task`` behaves as it did before.
|
|
28
|
+
"""
|
|
29
|
+
global _hook
|
|
30
|
+
|
|
31
|
+
def hook(task_name: str, exc: BaseException) -> None:
|
|
32
|
+
exceptionator.capture(
|
|
33
|
+
exc,
|
|
34
|
+
context=ExceptionContext(source=ExceptionSource.RQ, job_name=task_name),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_hook = hook
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def clear_run_task_hook() -> None:
|
|
41
|
+
"""
|
|
42
|
+
Remove any ``run_task`` capture hook.
|
|
43
|
+
"""
|
|
44
|
+
global _hook
|
|
45
|
+
_hook = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_run_task_hook() -> RunTaskHook | None:
|
|
49
|
+
"""
|
|
50
|
+
The hook ``run_task`` should call, or ``None``.
|
|
51
|
+
"""
|
|
52
|
+
return _hook
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Values every Exceptionator action receives.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
from corekit.exceptionator.constants import MESSAGE_MAX_CHARS, TRACEBACK_MAX_CHARS
|
|
13
|
+
from corekit.exceptionator.fingerprint import ExceptionFingerprint, ExceptionText
|
|
14
|
+
from corekit.schemas import StringEnum
|
|
15
|
+
from corekit.utils.text import truncate
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"CapturedException",
|
|
19
|
+
"ExceptionContext",
|
|
20
|
+
"ExceptionSource",
|
|
21
|
+
"ExceptionatorActionName",
|
|
22
|
+
"build_captured",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
_fingerprint = ExceptionFingerprint()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ExceptionatorActionName(StringEnum):
|
|
29
|
+
"""
|
|
30
|
+
Built-in action names. Apps may define additional string names for custom actions.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
NOOP = "noop"
|
|
34
|
+
LOG = "log"
|
|
35
|
+
PERSIST_AND_NOTIFY = "persist_and_notify"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ExceptionSource(StringEnum):
|
|
39
|
+
"""
|
|
40
|
+
Which process boundary captured the exception.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
HTTP = "http"
|
|
44
|
+
RQ = "rq"
|
|
45
|
+
DISCORD = "discord"
|
|
46
|
+
TASK = "task"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ExceptionContext(BaseModel):
|
|
50
|
+
"""
|
|
51
|
+
Where an exception was caught, plus whatever the boundary already knows.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
source: ExceptionSource = ExceptionSource.TASK
|
|
55
|
+
http_method: str | None = None
|
|
56
|
+
http_path: str | None = None
|
|
57
|
+
job_name: str | None = None
|
|
58
|
+
extra: dict[str, Any] = Field(default_factory=dict)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class CapturedException:
|
|
63
|
+
"""
|
|
64
|
+
One incident, ready for an action. Not a database row.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
service: str
|
|
68
|
+
exception_type: str
|
|
69
|
+
message: str
|
|
70
|
+
traceback_text: str
|
|
71
|
+
fingerprint: str
|
|
72
|
+
context: ExceptionContext
|
|
73
|
+
exc: BaseException = field(repr=False)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_captured(
|
|
77
|
+
exc: BaseException,
|
|
78
|
+
*,
|
|
79
|
+
service: str,
|
|
80
|
+
context: ExceptionContext | None = None,
|
|
81
|
+
) -> CapturedException:
|
|
82
|
+
"""
|
|
83
|
+
Truncate an exception into the object actions receive.
|
|
84
|
+
|
|
85
|
+
Product code that stores the text should redact secrets before writing.
|
|
86
|
+
"""
|
|
87
|
+
resolved = context or ExceptionContext()
|
|
88
|
+
message = ExceptionText.message(exc)
|
|
89
|
+
traceback_text = ExceptionFingerprint.format_traceback(exc)
|
|
90
|
+
return CapturedException(
|
|
91
|
+
service=service,
|
|
92
|
+
exception_type=type(exc).__qualname__,
|
|
93
|
+
message=truncate(message, MESSAGE_MAX_CHARS),
|
|
94
|
+
traceback_text=truncate(traceback_text, TRACEBACK_MAX_CHARS, suffix="\n… truncated"),
|
|
95
|
+
fingerprint=_fingerprint.of(exc),
|
|
96
|
+
context=resolved,
|
|
97
|
+
exc=exc,
|
|
98
|
+
)
|