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.
Files changed (155) hide show
  1. {openframe_core-3.3.0 → openframe_core-3.6.0}/.github/CHANGELOG.md +42 -1
  2. {openframe_core-3.3.0 → openframe_core-3.6.0}/PKG-INFO +1 -1
  3. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/plugins/__init__.py +3 -3
  4. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/plugins/registry.py +9 -5
  5. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/__init__.py +10 -2
  6. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/outbound/__init__.py +24 -4
  7. openframe_core-3.6.0/openframe/core/ports/outbound/repository.py +372 -0
  8. openframe_core-3.6.0/openframe/core/resilience/__init__.py +44 -0
  9. openframe_core-3.6.0/openframe/core/resilience/circuit_breaker.py +260 -0
  10. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/runtime/__init__.py +4 -1
  11. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/runtime/bootstrap.py +9 -5
  12. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/__init__.py +8 -0
  13. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/__init__.py +10 -0
  14. openframe_core-3.6.0/openframe/core/testing/contracts/graph_store.py +143 -0
  15. openframe_core-3.6.0/openframe/core/testing/contracts/vector_store.py +148 -0
  16. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/__init__.py +4 -0
  17. openframe_core-3.6.0/openframe/core/testing/fakes/graph_store.py +120 -0
  18. openframe_core-3.6.0/openframe/core/testing/fakes/vector_store.py +128 -0
  19. {openframe_core-3.3.0 → openframe_core-3.6.0}/pyproject.toml +1 -1
  20. openframe_core-3.6.0/tests/test_resilience.py +233 -0
  21. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_testing_fakes.py +79 -0
  22. openframe_core-3.3.0/openframe/core/ports/outbound/repository.py +0 -158
  23. {openframe_core-3.3.0 → openframe_core-3.6.0}/.github/requirements/docs.txt +0 -0
  24. {openframe_core-3.3.0 → openframe_core-3.6.0}/.github/requirements/test.txt +0 -0
  25. {openframe_core-3.3.0 → openframe_core-3.6.0}/.github/workflows/app-test.yml +0 -0
  26. {openframe_core-3.3.0 → openframe_core-3.6.0}/.github/workflows/auto-docs.yml +0 -0
  27. {openframe_core-3.3.0 → openframe_core-3.6.0}/.github/workflows/python-build.yml +0 -0
  28. {openframe_core-3.3.0 → openframe_core-3.6.0}/.gitignore +0 -0
  29. {openframe_core-3.3.0 → openframe_core-3.6.0}/.vscode/settings.json +0 -0
  30. {openframe_core-3.3.0 → openframe_core-3.6.0}/LICENSE +0 -0
  31. {openframe_core-3.3.0 → openframe_core-3.6.0}/README.md +0 -0
  32. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/assets/images/banner.png +0 -0
  33. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/assets/images/favicon.png +0 -0
  34. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/assets/images/logo-dark.png +0 -0
  35. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/assets/images/logo-light.png +0 -0
  36. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/assets/javascripts/extra.js +0 -0
  37. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/assets/stylesheets/extra.css +0 -0
  38. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/cicd/pipeline.md +0 -0
  39. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/getting-started/index.md +0 -0
  40. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/config.md +0 -0
  41. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/exceptions.md +0 -0
  42. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/health.md +0 -0
  43. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/inbound.md +0 -0
  44. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/index.md +0 -0
  45. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/middleware.md +0 -0
  46. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/plugins.md +0 -0
  47. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/ports.md +0 -0
  48. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/runtime.md +0 -0
  49. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/telemetry.md +0 -0
  50. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/testing.md +0 -0
  51. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/code/modules/tracing.md +0 -0
  52. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/developer-guide/debugging.md +0 -0
  53. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/developer-guide/explore.md +0 -0
  54. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/developer-guide/first-change.md +0 -0
  55. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/developer-guide/how-it-works.md +0 -0
  56. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/developer-guide/local-dev.md +0 -0
  57. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/developer-guide/quick-start/index.md +0 -0
  58. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/index.md +0 -0
  59. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
  60. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
  61. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
  62. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
  63. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
  64. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +0 -0
  65. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-007-schema-contracts.md +0 -0
  66. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/index.md +0 -0
  67. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/capability-taxonomy.md +0 -0
  68. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/component-lifecycle.md +0 -0
  69. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/data-flows.md +0 -0
  70. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/design-decisions.md +0 -0
  71. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/error-taxonomy.md +0 -0
  72. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/adapter-lifecycle.md +0 -0
  73. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/error-propagation.md +0 -0
  74. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/index.md +0 -0
  75. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/template-wiring.md +0 -0
  76. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
  77. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/architecture/system-architecture.md +0 -0
  78. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/deployment/cicd.md +0 -0
  79. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/deployment/environments.md +0 -0
  80. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/deployment/secrets-and-config.md +0 -0
  81. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/infrastructure/networking.md +0 -0
  82. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/infrastructure/platform.md +0 -0
  83. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/infrastructure/storage.md +0 -0
  84. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/index.md +0 -0
  85. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/monitoring.md +0 -0
  86. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/adapter-connection-failure.md +0 -0
  87. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
  88. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +0 -0
  89. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/queue-consumer-stuck.md +0 -0
  90. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/redis-connection-lost.md +0 -0
  91. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
  92. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/overview/glossary.md +0 -0
  93. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/overview/roadmap.md +0 -0
  94. {openframe_core-3.3.0 → openframe_core-3.6.0}/docs/technical/overview/system-overview.md +0 -0
  95. {openframe_core-3.3.0 → openframe_core-3.6.0}/mkdocs.yml +0 -0
  96. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/__init__.py +0 -0
  97. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/__init__.py +0 -0
  98. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/config/__init__.py +0 -0
  99. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/config/settings.py +0 -0
  100. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/exceptions/__init__.py +0 -0
  101. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/exceptions/adapter.py +0 -0
  102. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/exceptions/base.py +0 -0
  103. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/exceptions/codes.py +0 -0
  104. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/exceptions/plugin.py +0 -0
  105. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/inbound/__init__.py +0 -0
  106. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/inbound/command.py +0 -0
  107. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/inbound/context.py +0 -0
  108. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/inbound/query.py +0 -0
  109. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/inbound/usecase.py +0 -0
  110. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/middleware/__init__.py +0 -0
  111. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/middleware/telemetry.py +0 -0
  112. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/middleware/types.py +0 -0
  113. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/capability.py +0 -0
  114. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/context.py +0 -0
  115. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/health.py +0 -0
  116. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/identity.py +0 -0
  117. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/lifecycle.py +0 -0
  118. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/outbound/consumer.py +0 -0
  119. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/outbound/producer.py +0 -0
  120. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/ports/port.py +0 -0
  121. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/schemas/__init__.py +0 -0
  122. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/telemetry/__init__.py +0 -0
  123. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/telemetry/setup.py +0 -0
  124. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/consumer.py +0 -0
  125. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/inbound.py +0 -0
  126. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/lifecycle.py +0 -0
  127. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/port.py +0 -0
  128. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/producer.py +0 -0
  129. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/repository.py +0 -0
  130. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/consumer.py +0 -0
  131. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/inbound.py +0 -0
  132. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/producer.py +0 -0
  133. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/repository.py +0 -0
  134. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/testing/fixtures.py +0 -0
  135. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/tracing/__init__.py +0 -0
  136. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/tracing/propagation.py +0 -0
  137. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/core/tracing/proxy.py +0 -0
  138. {openframe_core-3.3.0 → openframe_core-3.6.0}/openframe/py.typed +0 -0
  139. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/conftest.py +0 -0
  140. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_config.py +0 -0
  141. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_exceptions.py +0 -0
  142. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_inbound.py +0 -0
  143. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_inbound_contracts.py +0 -0
  144. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_middleware.py +0 -0
  145. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_middleware_types.py +0 -0
  146. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_plugins.py +0 -0
  147. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_ports.py +0 -0
  148. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_ports_primitives.py +0 -0
  149. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_record_error.py +0 -0
  150. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_runtime.py +0 -0
  151. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_schemas.py +0 -0
  152. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_shutdown_telemetry.py +0 -0
  153. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_telemetry_sidecar.py +0 -0
  154. {openframe_core-3.3.0 → openframe_core-3.6.0}/tests/test_tracing.py +0 -0
  155. {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.3.0...HEAD
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.0
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
- All symbols are **experimental** in v3.0 and will stabilise based on real
13
- usage.
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: experimental
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
- All symbols are **experimental** in v3.0.
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: experimental
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__ = "experimental"
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: experimental
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 BaseConsumer, BaseProducer, BaseRepository
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 BaseRepository
39
-
40
- __all__ = ["BaseRepository", "BaseProducer", "BaseConsumer"]
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"