openframe-core 3.3.0__tar.gz → 3.6.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.
- {openframe_core-3.3.0 → openframe_core-3.6.0}/.github/CHANGELOG.md +42 -1
- {openframe_core-3.3.0 → openframe_core-3.6.0}/PKG-INFO +1 -1
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/plugins/__init__.py +3 -3
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/plugins/registry.py +9 -5
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/__init__.py +10 -2
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/outbound/__init__.py +24 -4
- openframe_core-3.6.0/openframe/core/ports/outbound/repository.py +372 -0
- openframe_core-3.6.0/openframe/core/resilience/__init__.py +44 -0
- openframe_core-3.6.0/openframe/core/resilience/circuit_breaker.py +260 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/runtime/__init__.py +4 -1
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/runtime/bootstrap.py +9 -5
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/__init__.py +8 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/__init__.py +10 -0
- openframe_core-3.6.0/openframe/core/testing/contracts/graph_store.py +143 -0
- openframe_core-3.6.0/openframe/core/testing/contracts/vector_store.py +148 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/__init__.py +4 -0
- openframe_core-3.6.0/openframe/core/testing/fakes/graph_store.py +120 -0
- openframe_core-3.6.0/openframe/core/testing/fakes/vector_store.py +128 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/pyproject.toml +1 -1
- openframe_core-3.6.0/tests/test_resilience.py +233 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_testing_fakes.py +79 -0
- openframe_core-3.3.0/openframe/core/ports/outbound/repository.py +0 -158
- {openframe_core-3.3.0 → openframe_core-3.6.0}/.github/requirements/docs.txt +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/.github/requirements/test.txt +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/.github/workflows/app-test.yml +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/.github/workflows/auto-docs.yml +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/.github/workflows/python-build.yml +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/.gitignore +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/.vscode/settings.json +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/LICENSE +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/README.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/assets/images/banner.png +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/assets/images/favicon.png +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/assets/images/logo-dark.png +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/assets/images/logo-light.png +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/assets/javascripts/extra.js +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/assets/stylesheets/extra.css +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/cicd/pipeline.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/getting-started/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/config.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/exceptions.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/health.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/inbound.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/middleware.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/plugins.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/ports.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/runtime.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/telemetry.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/testing.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/tracing.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/developer-guide/debugging.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/developer-guide/explore.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/developer-guide/first-change.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/developer-guide/how-it-works.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/developer-guide/local-dev.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/developer-guide/quick-start/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-007-schema-contracts.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/capability-taxonomy.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/component-lifecycle.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/data-flows.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/design-decisions.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/error-taxonomy.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/adapter-lifecycle.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/error-propagation.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/template-wiring.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/system-architecture.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/deployment/cicd.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/deployment/environments.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/deployment/secrets-and-config.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/infrastructure/networking.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/infrastructure/platform.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/infrastructure/storage.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/index.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/monitoring.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/adapter-connection-failure.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/queue-consumer-stuck.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/redis-connection-lost.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/overview/glossary.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/overview/roadmap.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/overview/system-overview.md +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/mkdocs.yml +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/config/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/config/settings.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/exceptions/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/exceptions/adapter.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/exceptions/base.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/exceptions/codes.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/exceptions/plugin.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/inbound/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/inbound/command.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/inbound/context.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/inbound/query.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/inbound/usecase.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/middleware/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/middleware/telemetry.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/middleware/types.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/capability.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/context.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/health.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/identity.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/lifecycle.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/outbound/consumer.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/outbound/producer.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/port.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/schemas/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/telemetry/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/telemetry/setup.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/consumer.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/inbound.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/lifecycle.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/port.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/producer.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/repository.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/consumer.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/inbound.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/producer.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/repository.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/fixtures.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/tracing/__init__.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/tracing/propagation.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/tracing/proxy.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/py.typed +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/conftest.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_config.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_exceptions.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_inbound.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_inbound_contracts.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_middleware.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_middleware_types.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_plugins.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_ports.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_ports_primitives.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_record_error.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_runtime.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_schemas.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_shutdown_telemetry.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_telemetry_sidecar.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_tracing.py +0 -0
- {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_tracing_propagation.py +0 -0
|
@@ -11,6 +11,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## v3.4.0 — 2026-09-28
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`openframe.core.resilience.CircuitBreakerProxy`** (new module,
|
|
19
|
+
experimental) — zero-code async circuit breaker. Wraps any object and
|
|
20
|
+
short-circuits its async methods after `failure_threshold` consecutive
|
|
21
|
+
failures, raising `AdapterConnectionError` ("circuit open", retryable)
|
|
22
|
+
immediately instead of letting every caller block until
|
|
23
|
+
`operation_timeout` on every single call during a sustained outage.
|
|
24
|
+
Standard closed → open → half-open → closed/open state machine.
|
|
25
|
+
Mirrors `TracingProxy`'s exact wrapping mechanism (`__getattr__`
|
|
26
|
+
interception, fresh `getattr` resolution of the wrapped method on every
|
|
27
|
+
call, sync methods pass through unwrapped) so the two proxies compose
|
|
28
|
+
predictably — see the module docstring for the recommended composition
|
|
29
|
+
order (`CircuitBreakerProxy(TracingProxy(repo, ...), ...)`, not the
|
|
30
|
+
reverse, so a short-circuited call never produces a misleading adapter
|
|
31
|
+
span). Addresses a real gap: none of the four `openframe-adapters`
|
|
32
|
+
packages (or anything else in the ecosystem) previously had any
|
|
33
|
+
resilience pattern beyond "block until timeout, then raise."
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Graduated `PluginRegistry` and `ApplicationBootstrap` from `experimental`
|
|
38
|
+
to `stable`** (`openframe.core.plugins.registry`, `openframe.core.runtime.bootstrap`,
|
|
39
|
+
and their respective package `__init__.py` re-exports). Graduation
|
|
40
|
+
criteria: (1) all 7 `openframe-local-validation-framework` services
|
|
41
|
+
already depend on both in production-shaped code across multiple
|
|
42
|
+
releases with no breaking change required of them; (2) v3.3.0's
|
|
43
|
+
`ApplicationBootstrap.compose()`/`get_all()`/`registry` additions landed
|
|
44
|
+
as pure additive extensions on top of the existing contract — no
|
|
45
|
+
breaking change was needed to add meaningful new functionality, which is
|
|
46
|
+
itself evidence the contract shape is right. This is the first module in
|
|
47
|
+
`openframe-core` to graduate via an explicit, written criteria set
|
|
48
|
+
(documented in each module's own docstring) rather than an unstated
|
|
49
|
+
judgment call — future modules going through the same
|
|
50
|
+
experimental→stable path should record their own criteria the same way.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
14
54
|
## v3.3.0 — 2026-09-28
|
|
15
55
|
|
|
16
56
|
### Added
|
|
@@ -385,7 +425,8 @@ Dev dependencies: `pytest>=8.0`, `pytest-asyncio>=0.23`, `httpx>=0.27`.
|
|
|
385
425
|
|
|
386
426
|
---
|
|
387
427
|
|
|
388
|
-
[Unreleased]: https://github.com/openframe-org/openframe-core/compare/v3.
|
|
428
|
+
[Unreleased]: https://github.com/openframe-org/openframe-core/compare/v3.4.0...HEAD
|
|
429
|
+
[3.4.0]: https://github.com/openframe-org/openframe-core/compare/v3.3.0...v3.4.0
|
|
389
430
|
[3.3.0]: https://github.com/openframe-org/openframe-core/compare/v3.2.1...v3.3.0
|
|
390
431
|
[3.2.1]: https://github.com/openframe-org/openframe-core/compare/v3.2.0...v3.2.1
|
|
391
432
|
[3.2.0]: https://github.com/openframe-org/openframe-core/compare/v3.1.0...v3.2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: openframe-core
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.0
|
|
4
4
|
Summary: OpenFrame Microservice Suite - platform kernel. Port contracts, testing utilities, plugin registry, and composition root.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Furious-Meteors/openframe-core
|
|
6
6
|
Project-URL: Documentation, https://furious-meteors.github.io/openframe-core/
|
|
@@ -9,10 +9,10 @@ plugin protocol. This package re-exports the ``ports`` types
|
|
|
9
9
|
:class:`~openframe.core.plugins.registry.PluginRegistry` operates on for
|
|
10
10
|
convenience, plus the registry itself.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
Stable as of v3.3.0 — see ``PluginRegistry``'s own module docstring
|
|
13
|
+
(``openframe.core.plugins.registry``) for the graduation rationale.
|
|
14
14
|
|
|
15
|
-
.. stability:
|
|
15
|
+
.. stability: stable
|
|
16
16
|
"""
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
@@ -17,10 +17,14 @@ correct shutdown ordering (ports then ``shutdown_telemetry()``). Use
|
|
|
17
17
|
initialization order or are building a framework layer on top of
|
|
18
18
|
openframe-core.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Stable as of v3.3.0 — graduated from experimental after all 7
|
|
21
|
+
``openframe-local-validation-framework`` services proved it in
|
|
22
|
+
production-shaped code across multiple releases with no breaking change,
|
|
23
|
+
and the v3.3.0 ``ApplicationBootstrap.compose()``/``get_all()``/``registry``
|
|
24
|
+
additions landed as pure additive extensions on top of this contract,
|
|
25
|
+
confirming the shape was already right.
|
|
21
26
|
|
|
22
|
-
.. stability:
|
|
23
|
-
Experimental — API may change or be removed in any version.
|
|
27
|
+
.. stability: stable
|
|
24
28
|
|
|
25
29
|
Dependency order:
|
|
26
30
|
ports → (apex; no plugins/registry-side imports)
|
|
@@ -52,7 +56,7 @@ if TYPE_CHECKING:
|
|
|
52
56
|
|
|
53
57
|
__all__ = ["PluginRegistry"]
|
|
54
58
|
|
|
55
|
-
__stability__ = "
|
|
59
|
+
__stability__ = "stable"
|
|
56
60
|
|
|
57
61
|
_log = logging.getLogger(__name__)
|
|
58
62
|
|
|
@@ -75,7 +79,7 @@ class PluginRegistry:
|
|
|
75
79
|
control over initialization order, or when building a framework layer
|
|
76
80
|
that manages the registry lifecycle itself.
|
|
77
81
|
|
|
78
|
-
.. stability:
|
|
82
|
+
.. stability: stable
|
|
79
83
|
|
|
80
84
|
Direct usage::
|
|
81
85
|
|
|
@@ -29,7 +29,7 @@ old ``openframe.core.contracts`` path — update all imports to
|
|
|
29
29
|
top of BasePort, defined in ``ports/outbound/`` (see that sub-module's
|
|
30
30
|
docstring for the intake rule on future capabilities)::
|
|
31
31
|
|
|
32
|
-
from openframe.core.ports import BaseRepository, BaseProducer, BaseConsumer
|
|
32
|
+
from openframe.core.ports import BaseRepository, BaseProducer, BaseConsumer, BaseVectorStore, BaseGraphStore
|
|
33
33
|
|
|
34
34
|
Runtime isinstance checks::
|
|
35
35
|
|
|
@@ -43,7 +43,13 @@ from openframe.core.ports.context import PrincipalContext, TenantContext
|
|
|
43
43
|
from openframe.core.ports.health import PluginContext, PluginHealth, PluginStatus
|
|
44
44
|
from openframe.core.ports.identity import Identity
|
|
45
45
|
from openframe.core.ports.lifecycle import Lifecycle
|
|
46
|
-
from openframe.core.ports.outbound import
|
|
46
|
+
from openframe.core.ports.outbound import (
|
|
47
|
+
BaseConsumer,
|
|
48
|
+
BaseGraphStore,
|
|
49
|
+
BaseProducer,
|
|
50
|
+
BaseRepository,
|
|
51
|
+
BaseVectorStore,
|
|
52
|
+
)
|
|
47
53
|
from openframe.core.ports.port import BasePort
|
|
48
54
|
|
|
49
55
|
__all__ = [
|
|
@@ -61,4 +67,6 @@ __all__ = [
|
|
|
61
67
|
"BaseRepository",
|
|
62
68
|
"BaseProducer",
|
|
63
69
|
"BaseConsumer",
|
|
70
|
+
"BaseVectorStore",
|
|
71
|
+
"BaseGraphStore",
|
|
64
72
|
]
|
|
@@ -14,6 +14,16 @@ outbound capability:
|
|
|
14
14
|
message publishing (``QUEUE`` capability).
|
|
15
15
|
- :class:`~openframe.core.ports.outbound.consumer.BaseConsumer` —
|
|
16
16
|
message consumption (``QUEUE`` capability).
|
|
17
|
+
- :class:`~openframe.core.ports.outbound.repository.BaseVectorStore` —
|
|
18
|
+
id-addressable CRUD plus similarity search (``SEARCH`` capability).
|
|
19
|
+
Extends ``BaseRepository`` directly (defined in the same module) rather
|
|
20
|
+
than sitting as a sibling of it — see ``repository.py``'s own
|
|
21
|
+
docstring for why.
|
|
22
|
+
- :class:`~openframe.core.ports.outbound.repository.BaseGraphStore` —
|
|
23
|
+
id-addressable node CRUD plus pattern-matching traversal (``SEARCH``
|
|
24
|
+
capability). Also extends ``BaseRepository`` directly (defined in the
|
|
25
|
+
same module) — see ``repository.py``'s own docstring for why, and for
|
|
26
|
+
why relationships/edges are not given their own port method.
|
|
17
27
|
|
|
18
28
|
Future outbound capabilities (openframe-infra: ``BaseSecretsProvider``,
|
|
19
29
|
``BaseObjectStore``, ``BaseFeatureFlagProvider``) will be added here
|
|
@@ -25,7 +35,7 @@ Implementations live in openframe-adapters.
|
|
|
25
35
|
|
|
26
36
|
Usage::
|
|
27
37
|
|
|
28
|
-
from openframe.core.ports import BaseRepository, BaseProducer, BaseConsumer
|
|
38
|
+
from openframe.core.ports import BaseRepository, BaseProducer, BaseConsumer, BaseVectorStore, BaseGraphStore
|
|
29
39
|
|
|
30
40
|
Or via explicit path if needed::
|
|
31
41
|
|
|
@@ -35,6 +45,16 @@ from __future__ import annotations
|
|
|
35
45
|
|
|
36
46
|
from openframe.core.ports.outbound.consumer import BaseConsumer
|
|
37
47
|
from openframe.core.ports.outbound.producer import BaseProducer
|
|
38
|
-
from openframe.core.ports.outbound.repository import
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
from openframe.core.ports.outbound.repository import (
|
|
49
|
+
BaseGraphStore,
|
|
50
|
+
BaseRepository,
|
|
51
|
+
BaseVectorStore,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"BaseRepository",
|
|
56
|
+
"BaseProducer",
|
|
57
|
+
"BaseConsumer",
|
|
58
|
+
"BaseVectorStore",
|
|
59
|
+
"BaseGraphStore",
|
|
60
|
+
]
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe/core/ports/outbound/repository.py
|
|
3
|
+
==============================================
|
|
4
|
+
Generic persistence port for the OpenFrame ecosystem (ADR-006).
|
|
5
|
+
|
|
6
|
+
``BaseRepository[T]`` extends
|
|
7
|
+
:class:`~openframe.core.ports.port.BasePort` directly — it is
|
|
8
|
+
``Identity + Lifecycle`` plus its own domain methods. There is exactly one
|
|
9
|
+
lifecycle-aware definition of this port; the pre-v3 lifecycle-free version
|
|
10
|
+
no longer exists.
|
|
11
|
+
|
|
12
|
+
Adapters satisfy this protocol by structural subtyping — no inheritance
|
|
13
|
+
from ``BaseRepository`` is required or desired. Any class whose async
|
|
14
|
+
method signatures match, and that also satisfies ``BasePort``
|
|
15
|
+
(``name``/``version``/``capability``/``initialize``/``shutdown``/
|
|
16
|
+
``health``), is accepted.
|
|
17
|
+
|
|
18
|
+
Runtime isinstance check::
|
|
19
|
+
|
|
20
|
+
isinstance(repo, BaseRepository) # ✓ works — checks method names
|
|
21
|
+
isinstance(repo, BaseRepository[str]) # ✗ raises TypeError — not supported
|
|
22
|
+
|
|
23
|
+
The parameterised form is a static-analysis-only annotation. At runtime,
|
|
24
|
+
always check against the unparameterised ``BaseRepository``.
|
|
25
|
+
|
|
26
|
+
This module also defines :class:`BaseVectorStore`, which extends
|
|
27
|
+
``BaseRepository`` directly rather than sitting in its own file or as a
|
|
28
|
+
disjoint sibling port (the way ``BaseProducer``/``BaseConsumer`` are
|
|
29
|
+
siblings of ``BaseRepository``, not subtypes). Most vector databases ARE
|
|
30
|
+
genuinely id-addressable — insert/upsert-with-id, fetch-by-id,
|
|
31
|
+
delete-by-id, list/scroll — so the five CRUD methods below are real
|
|
32
|
+
operations for them too, not fakes forced to satisfy an interface. The
|
|
33
|
+
only operation with no equivalent here is similarity search, so
|
|
34
|
+
``BaseVectorStore`` adds exactly one new method, :meth:`BaseVectorStore.search`,
|
|
35
|
+
on top of everything below. A vector store that satisfies
|
|
36
|
+
``BaseVectorStore`` also satisfies ``BaseRepository`` for free — existing
|
|
37
|
+
code written against ``BaseRepository`` keeps working unmodified against
|
|
38
|
+
a vector-backed implementation.
|
|
39
|
+
|
|
40
|
+
``BaseVectorStore`` is deliberately not a universal fit for every
|
|
41
|
+
vector backend. FAISS (a bare in-process index with no native
|
|
42
|
+
id→metadata store, and some index types that don't support deletion
|
|
43
|
+
without a full rebuild) was evaluated and explicitly excluded from this
|
|
44
|
+
port's intended adopters.
|
|
45
|
+
|
|
46
|
+
This module also defines :class:`BaseGraphStore`, which — like
|
|
47
|
+
``BaseVectorStore`` — extends ``BaseRepository`` directly. A graph
|
|
48
|
+
database's nodes ARE genuinely id-addressable: create/get/update/delete
|
|
49
|
+
map onto real `CREATE`/`MATCH ... WHERE n.<key> = $id`/`SET`/`DETACH
|
|
50
|
+
DELETE` operations, provided the adapter addresses nodes by an
|
|
51
|
+
application-level unique property (with a backing index/constraint the
|
|
52
|
+
adapter is responsible for creating), not a backend-internal node
|
|
53
|
+
identity. FalkorDB's own ``id()`` function is explicitly documented as
|
|
54
|
+
unstable — deleted nodes' higher internal ids can be migrated down to
|
|
55
|
+
fill vacated lower ones — so it is unsuitable as this port's addressing
|
|
56
|
+
key; this is exactly why adapters must key off their own property, not
|
|
57
|
+
why graph databases don't fit this port at all. The one operation with
|
|
58
|
+
no CRUD equivalent is traversal/pattern-matching, so ``BaseGraphStore``
|
|
59
|
+
adds exactly one new method, :meth:`BaseGraphStore.traverse`, which also
|
|
60
|
+
serves as the escape hatch for relationship/edge creation and querying
|
|
61
|
+
(Cypher's ``CREATE``/``MATCH`` handle nodes and edges in the same
|
|
62
|
+
statement) — this port does not model edges as their own first-class
|
|
63
|
+
method, the same way it does not invent a bespoke query language.
|
|
64
|
+
|
|
65
|
+
Dependency order:
|
|
66
|
+
ports/port → ports/identity + ports/lifecycle
|
|
67
|
+
ports/outbound/repository → ports/port
|
|
68
|
+
"""
|
|
69
|
+
from __future__ import annotations
|
|
70
|
+
|
|
71
|
+
from typing import Protocol, TypeVar, runtime_checkable
|
|
72
|
+
|
|
73
|
+
from openframe.core.ports.port import BasePort
|
|
74
|
+
|
|
75
|
+
__all__ = ["BaseRepository", "BaseVectorStore", "BaseGraphStore"]
|
|
76
|
+
|
|
77
|
+
T = TypeVar("T")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@runtime_checkable
|
|
81
|
+
class BaseRepository(BasePort, Protocol[T]):
|
|
82
|
+
"""
|
|
83
|
+
Generic persistence port. ``BasePort`` (Identity + Lifecycle) plus
|
|
84
|
+
domain-specific CRUD methods.
|
|
85
|
+
|
|
86
|
+
Adapters implement this protocol structurally — no inheritance needed.
|
|
87
|
+
Any class with matching async method signatures, plus the
|
|
88
|
+
``BasePort`` members, satisfies ``BaseRepository``.
|
|
89
|
+
|
|
90
|
+
Type parameter ``T`` is the domain entity the repository manages.
|
|
91
|
+
|
|
92
|
+
Runtime check::
|
|
93
|
+
|
|
94
|
+
isinstance(repo, BaseRepository) # ✓ works
|
|
95
|
+
isinstance(repo, BaseRepository[str]) # ✗ raises TypeError
|
|
96
|
+
|
|
97
|
+
Domain methods:
|
|
98
|
+
get: Retrieve a single entity by ID.
|
|
99
|
+
list: Return a paginated slice plus total count.
|
|
100
|
+
create: Persist a new entity and return it with any backend-assigned
|
|
101
|
+
fields populated (e.g. generated primary key, created_at).
|
|
102
|
+
update: Persist changes to an existing entity. Returns the updated
|
|
103
|
+
entity or None if the entity did not exist.
|
|
104
|
+
delete: Remove an entity by ID. Returns True if deleted, False if
|
|
105
|
+
the entity did not exist.
|
|
106
|
+
|
|
107
|
+
Plus, inherited from ``BasePort``: ``name``, ``version``, ``capability``,
|
|
108
|
+
``initialize``, ``shutdown``, ``health``.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
async def get(self, entity_id: str) -> T | None:
|
|
112
|
+
"""
|
|
113
|
+
Retrieve a single entity by its identifier.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
entity_id: The unique identifier of the entity.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The entity if found, None if it does not exist.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
AdapterConnectionError: If the backend is unreachable.
|
|
123
|
+
AdapterQueryError: If the query fails for any other reason.
|
|
124
|
+
AdapterTimeoutError: If the operation exceeds ``operation_timeout``.
|
|
125
|
+
"""
|
|
126
|
+
...
|
|
127
|
+
|
|
128
|
+
async def list(self, limit: int, offset: int) -> tuple[list[T], int]:
|
|
129
|
+
"""
|
|
130
|
+
Return a paginated slice of all entities plus the total count.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
limit: Maximum number of entities to return.
|
|
134
|
+
offset: Number of entities to skip from the beginning.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
A 2-tuple of (entities, total_count). ``total_count`` is the
|
|
138
|
+
count of all matching entities, not just the returned slice.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
AdapterConnectionError: If the backend is unreachable.
|
|
142
|
+
AdapterQueryError: If the query fails.
|
|
143
|
+
AdapterTimeoutError: If the operation exceeds ``operation_timeout``.
|
|
144
|
+
"""
|
|
145
|
+
...
|
|
146
|
+
|
|
147
|
+
async def create(self, entity: T) -> T:
|
|
148
|
+
"""
|
|
149
|
+
Persist a new entity and return it with backend-assigned fields.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
entity: The entity to create. Backend may assign fields like
|
|
153
|
+
primary key or timestamps.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The entity as stored, including any backend-assigned fields.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
AdapterConnectionError: If the backend is unreachable.
|
|
160
|
+
AdapterQueryError: If the insert fails (e.g. constraint violation).
|
|
161
|
+
AdapterTimeoutError: If the operation exceeds ``operation_timeout``.
|
|
162
|
+
"""
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
async def update(self, entity: T) -> T | None:
|
|
166
|
+
"""
|
|
167
|
+
Persist changes to an existing entity.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
entity: The entity with updated fields. Must have a valid identifier.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The updated entity as stored, or None if the entity did not exist.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
AdapterConnectionError: If the backend is unreachable.
|
|
177
|
+
AdapterQueryError: If the update fails.
|
|
178
|
+
AdapterTimeoutError: If the operation exceeds ``operation_timeout``.
|
|
179
|
+
"""
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
async def delete(self, entity_id: str) -> bool:
|
|
183
|
+
"""
|
|
184
|
+
Remove an entity by its identifier.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
entity_id: The unique identifier of the entity to delete.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
True if the entity was deleted, False if it did not exist.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
AdapterConnectionError: If the backend is unreachable.
|
|
194
|
+
AdapterQueryError: If the deletion fails.
|
|
195
|
+
AdapterTimeoutError: If the operation exceeds ``operation_timeout``.
|
|
196
|
+
"""
|
|
197
|
+
...
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@runtime_checkable
|
|
201
|
+
class BaseVectorStore(BaseRepository[T], Protocol[T]):
|
|
202
|
+
"""
|
|
203
|
+
Generic vector store port. ``BaseRepository[T]`` (id-addressable CRUD)
|
|
204
|
+
plus one additional domain method, :meth:`search`.
|
|
205
|
+
|
|
206
|
+
See this module's own docstring for why this extends ``BaseRepository``
|
|
207
|
+
directly rather than being a disjoint sibling port, and why FAISS/
|
|
208
|
+
FalkorDB are explicitly excluded from this port's intended adopters.
|
|
209
|
+
|
|
210
|
+
Adapters implement this protocol structurally — no inheritance needed.
|
|
211
|
+
Any class with matching async method signatures, plus everything
|
|
212
|
+
``BaseRepository``/``BasePort`` require, satisfies ``BaseVectorStore``.
|
|
213
|
+
|
|
214
|
+
Type parameter ``T`` is the domain entity the vector store manages —
|
|
215
|
+
typically a dataclass or Pydantic model carrying an id, an embedding
|
|
216
|
+
vector, and whatever metadata fields the caller's domain needs.
|
|
217
|
+
|
|
218
|
+
.. stability: experimental
|
|
219
|
+
New port, added alongside the first vector-store adapter packages.
|
|
220
|
+
API may change based on real usage. ``BaseRepository`` above is
|
|
221
|
+
unaffected by this — it remains stable; only ``BaseVectorStore``
|
|
222
|
+
itself is experimental.
|
|
223
|
+
|
|
224
|
+
Runtime check::
|
|
225
|
+
|
|
226
|
+
isinstance(store, BaseVectorStore) # ✓ works
|
|
227
|
+
isinstance(store, BaseRepository) # ✓ also true
|
|
228
|
+
|
|
229
|
+
Inherited from ``BaseRepository[T]`` — id-addressable CRUD:
|
|
230
|
+
get: Fetch a single entity by id.
|
|
231
|
+
list: Return a paginated slice plus total count.
|
|
232
|
+
create: Upsert a new entity (most vector DBs treat insert as
|
|
233
|
+
upsert-by-id natively; adapters document their own
|
|
234
|
+
create-vs-upsert semantics if they diverge).
|
|
235
|
+
update: Upsert an existing entity's vector/metadata.
|
|
236
|
+
delete: Remove an entity by id.
|
|
237
|
+
|
|
238
|
+
New in this port:
|
|
239
|
+
search: Similarity search — the operation ``BaseRepository`` has
|
|
240
|
+
no equivalent for.
|
|
241
|
+
|
|
242
|
+
Plus, inherited from ``BasePort`` (via ``BaseRepository``): ``name``,
|
|
243
|
+
``version``, ``capability``, ``initialize``, ``shutdown``, ``health``.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
async def search(self, query_vector: list[float], k: int) -> list[T]:
|
|
247
|
+
"""
|
|
248
|
+
Return the ``k`` entities whose vectors are most similar to
|
|
249
|
+
``query_vector``.
|
|
250
|
+
|
|
251
|
+
The distance/similarity metric (cosine, L2, dot product, ...) is
|
|
252
|
+
an adapter/backend-level configuration concern, not part of this
|
|
253
|
+
port's signature — different backends default to different
|
|
254
|
+
metrics, and some let it be configured per-collection at index
|
|
255
|
+
creation time. Document the active metric in the adapter's own
|
|
256
|
+
docstring.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
query_vector: The embedding to search against. Must match the
|
|
260
|
+
dimensionality the backend collection/index was
|
|
261
|
+
created with — a mismatch is a configuration
|
|
262
|
+
error, not a query error, but adapters may
|
|
263
|
+
surface it as either depending on when the
|
|
264
|
+
backend itself detects it.
|
|
265
|
+
k: Maximum number of results to return, ordered from most to
|
|
266
|
+
least similar. Returning fewer than ``k`` results (if the
|
|
267
|
+
collection has fewer than ``k`` entities, or the backend
|
|
268
|
+
applies its own relevance cutoff) is not an error.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Up to ``k`` entities, ordered from most to least similar.
|
|
272
|
+
Empty list if nothing matches (never ``None`` — unlike
|
|
273
|
+
``get()``, "no results" is a normal outcome of a search, not
|
|
274
|
+
a not-found case).
|
|
275
|
+
|
|
276
|
+
Raises:
|
|
277
|
+
AdapterConnectionError: If the backend is unreachable.
|
|
278
|
+
AdapterQueryError: If the query fails (e.g.
|
|
279
|
+
``query_vector``'s dimensionality
|
|
280
|
+
doesn't match the collection).
|
|
281
|
+
AdapterTimeoutError: If the operation exceeds
|
|
282
|
+
``operation_timeout``.
|
|
283
|
+
"""
|
|
284
|
+
...
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@runtime_checkable
|
|
288
|
+
class BaseGraphStore(BaseRepository[T], Protocol[T]):
|
|
289
|
+
"""
|
|
290
|
+
Generic graph store port. ``BaseRepository[T]`` (id-addressable node
|
|
291
|
+
CRUD) plus one additional domain method, :meth:`traverse`.
|
|
292
|
+
|
|
293
|
+
See this module's own docstring for why this extends ``BaseRepository``
|
|
294
|
+
directly rather than being a disjoint sibling port, why FalkorDB's
|
|
295
|
+
internal ``id()`` is not a suitable addressing key, and why
|
|
296
|
+
relationships/edges are not given their own port method.
|
|
297
|
+
|
|
298
|
+
Adapters implement this protocol structurally — no inheritance needed.
|
|
299
|
+
Any class with matching async method signatures, plus everything
|
|
300
|
+
``BaseRepository``/``BasePort`` require, satisfies ``BaseGraphStore``.
|
|
301
|
+
|
|
302
|
+
Type parameter ``T`` is the domain entity representing a **node** —
|
|
303
|
+
typically a dataclass or Pydantic model carrying an application-level
|
|
304
|
+
unique id, labels, and whatever properties the caller's domain needs.
|
|
305
|
+
Edges are not modeled by ``T`` or by any method on this port; they are
|
|
306
|
+
created and queried exclusively through :meth:`traverse`.
|
|
307
|
+
|
|
308
|
+
.. stability: experimental
|
|
309
|
+
New port, added alongside the first graph-store adapter packages.
|
|
310
|
+
API may change based on real usage. ``BaseRepository`` above is
|
|
311
|
+
unaffected by this — it remains stable; only ``BaseGraphStore``
|
|
312
|
+
itself is experimental.
|
|
313
|
+
|
|
314
|
+
Runtime check::
|
|
315
|
+
|
|
316
|
+
isinstance(store, BaseGraphStore) # ✓ works
|
|
317
|
+
isinstance(store, BaseRepository) # ✓ also true
|
|
318
|
+
|
|
319
|
+
Inherited from ``BaseRepository[T]`` — id-addressable node CRUD:
|
|
320
|
+
get: Fetch a single node by its application-level id.
|
|
321
|
+
list: Return a paginated slice of nodes plus total count.
|
|
322
|
+
create: Create a new node. Adapters are responsible for ensuring
|
|
323
|
+
a unique constraint/index exists on the id property they
|
|
324
|
+
address nodes by — without one, every get/update/delete
|
|
325
|
+
degrades to an unindexed label scan.
|
|
326
|
+
update: Update an existing node's properties.
|
|
327
|
+
delete: Remove a node by id. Adapters should detach (remove
|
|
328
|
+
incident edges) before deleting, since most graph backends
|
|
329
|
+
reject deleting a node that still has relationships.
|
|
330
|
+
|
|
331
|
+
New in this port:
|
|
332
|
+
traverse: Arbitrary pattern-matching/traversal query — the
|
|
333
|
+
operation ``BaseRepository`` has no equivalent for, and
|
|
334
|
+
also the only way to create, update, or query edges.
|
|
335
|
+
|
|
336
|
+
Plus, inherited from ``BasePort`` (via ``BaseRepository``): ``name``,
|
|
337
|
+
``version``, ``capability``, ``initialize``, ``shutdown``, ``health``.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
async def traverse(
|
|
341
|
+
self, query: str, params: dict[str, object] | None = None
|
|
342
|
+
) -> list[T]:
|
|
343
|
+
"""
|
|
344
|
+
Execute an arbitrary pattern-matching/traversal query against the
|
|
345
|
+
graph and return the matching entities.
|
|
346
|
+
|
|
347
|
+
This is the escape hatch for everything ``BaseRepository``'s CRUD
|
|
348
|
+
methods cannot express: multi-hop traversals, relationship/edge
|
|
349
|
+
creation and querying, variable-length paths, aggregations. The
|
|
350
|
+
query language itself (Cypher, Gremlin, ...) is an adapter/backend
|
|
351
|
+
concern, not part of this port's signature — adapters document
|
|
352
|
+
which language their ``query`` argument expects.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
query: A query string in the backend's native query language.
|
|
356
|
+
params: Named parameters substituted into the query, keyed by
|
|
357
|
+
name as the backend's driver expects. None if the
|
|
358
|
+
query has no parameters.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
The entities the query's `RETURN`-equivalent clause produces.
|
|
362
|
+
Empty list if the query matches nothing (not an error).
|
|
363
|
+
|
|
364
|
+
Raises:
|
|
365
|
+
AdapterConnectionError: If the backend is unreachable.
|
|
366
|
+
AdapterQueryError: If the query is malformed or fails
|
|
367
|
+
(e.g. a syntax error, or a
|
|
368
|
+
constraint violation on a write).
|
|
369
|
+
AdapterTimeoutError: If the operation exceeds
|
|
370
|
+
``operation_timeout``.
|
|
371
|
+
"""
|
|
372
|
+
...
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe/core/resilience/
|
|
3
|
+
============================
|
|
4
|
+
Resilience patterns for the OpenFrame ecosystem.
|
|
5
|
+
|
|
6
|
+
Modules
|
|
7
|
+
-------
|
|
8
|
+
``CircuitBreakerProxy``
|
|
9
|
+
Zero-code async circuit breaker. Wraps any object and short-circuits
|
|
10
|
+
its async methods after a run of consecutive failures, without
|
|
11
|
+
requiring the adapter or service layer to know it exists. Mirrors
|
|
12
|
+
``openframe.core.tracing.TracingProxy``'s wrapping mechanism, so the
|
|
13
|
+
two proxies compose predictably.
|
|
14
|
+
|
|
15
|
+
Usage::
|
|
16
|
+
|
|
17
|
+
from openframe.core.resilience import CircuitBreakerProxy
|
|
18
|
+
|
|
19
|
+
raw_repo = PostgresRepository(settings)
|
|
20
|
+
guarded_repo = CircuitBreakerProxy(raw_repo, failure_threshold=5, reset_timeout=30.0)
|
|
21
|
+
# After 5 consecutive failures, calls raise AdapterConnectionError
|
|
22
|
+
# ("circuit open") immediately instead of blocking on operation_timeout.
|
|
23
|
+
|
|
24
|
+
Composing with ``TracingProxy`` — prefer wrapping the traced object, not
|
|
25
|
+
the other way around, so a short-circuited call never produces a
|
|
26
|
+
misleading adapter span for a call that never reached the adapter::
|
|
27
|
+
|
|
28
|
+
from openframe.core.tracing import TracingProxy
|
|
29
|
+
|
|
30
|
+
traced = TracingProxy(raw_repo, prefix="repository.item")
|
|
31
|
+
guarded = CircuitBreakerProxy(traced, failure_threshold=5, reset_timeout=30.0)
|
|
32
|
+
|
|
33
|
+
.. stability: experimental
|
|
34
|
+
New in v3.4.0. Experimental — API may change based on real usage,
|
|
35
|
+
consistent with how ``ApplicationBootstrap``/``PluginRegistry`` started
|
|
36
|
+
experimental and graduated once proven (see their own module docstrings).
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from openframe.core.resilience.circuit_breaker import CircuitBreakerProxy, CircuitState
|
|
41
|
+
|
|
42
|
+
__all__ = ["CircuitBreakerProxy", "CircuitState"]
|
|
43
|
+
|
|
44
|
+
__stability__ = "experimental"
|