openframe-core 3.4.0__tar.gz → 3.7.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 (158) hide show
  1. {openframe_core-3.4.0 → openframe_core-3.7.0}/.github/CHANGELOG.md +128 -1
  2. {openframe_core-3.4.0 → openframe_core-3.7.0}/PKG-INFO +1 -1
  3. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/__init__.py +10 -2
  4. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/outbound/__init__.py +24 -4
  5. openframe_core-3.7.0/openframe/core/ports/outbound/repository.py +372 -0
  6. openframe_core-3.7.0/openframe/core/remote/__init__.py +23 -0
  7. openframe_core-3.7.0/openframe/core/remote/client.py +135 -0
  8. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/__init__.py +14 -0
  9. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/__init__.py +16 -0
  10. openframe_core-3.7.0/openframe/core/testing/contracts/graph_store.py +143 -0
  11. openframe_core-3.7.0/openframe/core/testing/contracts/remote.py +79 -0
  12. openframe_core-3.7.0/openframe/core/testing/contracts/vector_store.py +148 -0
  13. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/fakes/__init__.py +4 -0
  14. openframe_core-3.7.0/openframe/core/testing/fakes/graph_store.py +120 -0
  15. openframe_core-3.7.0/openframe/core/testing/fakes/vector_store.py +128 -0
  16. {openframe_core-3.4.0 → openframe_core-3.7.0}/pyproject.toml +1 -1
  17. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_testing_fakes.py +79 -0
  18. openframe_core-3.4.0/openframe/core/ports/outbound/repository.py +0 -158
  19. {openframe_core-3.4.0 → openframe_core-3.7.0}/.github/requirements/docs.txt +0 -0
  20. {openframe_core-3.4.0 → openframe_core-3.7.0}/.github/requirements/test.txt +0 -0
  21. {openframe_core-3.4.0 → openframe_core-3.7.0}/.github/workflows/app-test.yml +0 -0
  22. {openframe_core-3.4.0 → openframe_core-3.7.0}/.github/workflows/auto-docs.yml +0 -0
  23. {openframe_core-3.4.0 → openframe_core-3.7.0}/.github/workflows/python-build.yml +0 -0
  24. {openframe_core-3.4.0 → openframe_core-3.7.0}/.gitignore +0 -0
  25. {openframe_core-3.4.0 → openframe_core-3.7.0}/.vscode/settings.json +0 -0
  26. {openframe_core-3.4.0 → openframe_core-3.7.0}/LICENSE +0 -0
  27. {openframe_core-3.4.0 → openframe_core-3.7.0}/README.md +0 -0
  28. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/assets/images/banner.png +0 -0
  29. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/assets/images/favicon.png +0 -0
  30. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/assets/images/logo-dark.png +0 -0
  31. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/assets/images/logo-light.png +0 -0
  32. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/assets/javascripts/extra.js +0 -0
  33. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/assets/stylesheets/extra.css +0 -0
  34. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/cicd/pipeline.md +0 -0
  35. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/getting-started/index.md +0 -0
  36. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/config.md +0 -0
  37. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/exceptions.md +0 -0
  38. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/health.md +0 -0
  39. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/inbound.md +0 -0
  40. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/index.md +0 -0
  41. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/middleware.md +0 -0
  42. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/plugins.md +0 -0
  43. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/ports.md +0 -0
  44. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/runtime.md +0 -0
  45. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/telemetry.md +0 -0
  46. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/testing.md +0 -0
  47. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/tracing.md +0 -0
  48. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/developer-guide/debugging.md +0 -0
  49. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/developer-guide/explore.md +0 -0
  50. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/developer-guide/first-change.md +0 -0
  51. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/developer-guide/how-it-works.md +0 -0
  52. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/developer-guide/local-dev.md +0 -0
  53. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/developer-guide/quick-start/index.md +0 -0
  54. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/index.md +0 -0
  55. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
  56. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
  57. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
  58. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
  59. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
  60. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +0 -0
  61. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-007-schema-contracts.md +0 -0
  62. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/index.md +0 -0
  63. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/capability-taxonomy.md +0 -0
  64. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/component-lifecycle.md +0 -0
  65. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/data-flows.md +0 -0
  66. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/design-decisions.md +0 -0
  67. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/error-taxonomy.md +0 -0
  68. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/package-journey/adapter-lifecycle.md +0 -0
  69. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/package-journey/error-propagation.md +0 -0
  70. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/package-journey/index.md +0 -0
  71. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/package-journey/template-wiring.md +0 -0
  72. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
  73. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/system-architecture.md +0 -0
  74. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/deployment/cicd.md +0 -0
  75. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/deployment/environments.md +0 -0
  76. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/deployment/secrets-and-config.md +0 -0
  77. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/infrastructure/networking.md +0 -0
  78. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/infrastructure/platform.md +0 -0
  79. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/infrastructure/storage.md +0 -0
  80. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/index.md +0 -0
  81. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/monitoring.md +0 -0
  82. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/runbooks/adapter-connection-failure.md +0 -0
  83. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
  84. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +0 -0
  85. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/runbooks/queue-consumer-stuck.md +0 -0
  86. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/runbooks/redis-connection-lost.md +0 -0
  87. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
  88. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/overview/glossary.md +0 -0
  89. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/overview/roadmap.md +0 -0
  90. {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/overview/system-overview.md +0 -0
  91. {openframe_core-3.4.0 → openframe_core-3.7.0}/mkdocs.yml +0 -0
  92. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/__init__.py +0 -0
  93. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/__init__.py +0 -0
  94. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/config/__init__.py +0 -0
  95. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/config/settings.py +0 -0
  96. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/exceptions/__init__.py +0 -0
  97. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/exceptions/adapter.py +0 -0
  98. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/exceptions/base.py +0 -0
  99. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/exceptions/codes.py +0 -0
  100. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/exceptions/plugin.py +0 -0
  101. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/inbound/__init__.py +0 -0
  102. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/inbound/command.py +0 -0
  103. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/inbound/context.py +0 -0
  104. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/inbound/query.py +0 -0
  105. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/inbound/usecase.py +0 -0
  106. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/middleware/__init__.py +0 -0
  107. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/middleware/telemetry.py +0 -0
  108. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/middleware/types.py +0 -0
  109. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/plugins/__init__.py +0 -0
  110. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/plugins/registry.py +0 -0
  111. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/capability.py +0 -0
  112. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/context.py +0 -0
  113. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/health.py +0 -0
  114. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/identity.py +0 -0
  115. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/lifecycle.py +0 -0
  116. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/outbound/consumer.py +0 -0
  117. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/outbound/producer.py +0 -0
  118. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/port.py +0 -0
  119. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/resilience/__init__.py +0 -0
  120. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/resilience/circuit_breaker.py +0 -0
  121. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/runtime/__init__.py +0 -0
  122. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/runtime/bootstrap.py +0 -0
  123. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/schemas/__init__.py +0 -0
  124. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/telemetry/__init__.py +0 -0
  125. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/telemetry/setup.py +0 -0
  126. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/consumer.py +0 -0
  127. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/inbound.py +0 -0
  128. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/lifecycle.py +0 -0
  129. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/port.py +0 -0
  130. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/producer.py +0 -0
  131. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/repository.py +0 -0
  132. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/fakes/consumer.py +0 -0
  133. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/fakes/inbound.py +0 -0
  134. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/fakes/producer.py +0 -0
  135. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/fakes/repository.py +0 -0
  136. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/fixtures.py +0 -0
  137. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/tracing/__init__.py +0 -0
  138. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/tracing/propagation.py +0 -0
  139. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/tracing/proxy.py +0 -0
  140. {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/py.typed +0 -0
  141. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/conftest.py +0 -0
  142. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_config.py +0 -0
  143. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_exceptions.py +0 -0
  144. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_inbound.py +0 -0
  145. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_inbound_contracts.py +0 -0
  146. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_middleware.py +0 -0
  147. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_middleware_types.py +0 -0
  148. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_plugins.py +0 -0
  149. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_ports.py +0 -0
  150. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_ports_primitives.py +0 -0
  151. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_record_error.py +0 -0
  152. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_resilience.py +0 -0
  153. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_runtime.py +0 -0
  154. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_schemas.py +0 -0
  155. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_shutdown_telemetry.py +0 -0
  156. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_telemetry_sidecar.py +0 -0
  157. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_tracing.py +0 -0
  158. {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_tracing_propagation.py +0 -0
@@ -11,6 +11,130 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ---
13
13
 
14
+ ## v3.7.0 — 2026-10-11
15
+
16
+ ### Added
17
+
18
+ - **`openframe.core.remote`** — `RemoteCommandClient[TIn]`/
19
+ `RemoteQueryClient[TIn, TOut]`, the outbound counterparts of
20
+ `openframe.core.inbound`'s `CommandHandler`/`QueryHandler`, for
21
+ `openframe-protocol`'s M1b (HTTP client, service-to-service calls).
22
+ Structurally identical `execute(input, context) -> result` shape to
23
+ the inbound pair, but deliberately separate Protocol classes in their
24
+ own module — direction matters even when the signature doesn't
25
+ differ, and conflating "something calls into me" with "I call out to
26
+ something" under one name would erase the distinction this
27
+ ecosystem's whole organising principle rests on. No `BasePort`/
28
+ `Identity`/`Capability` involvement: `openframe-protocol`'s own design
29
+ review (draft 8, decision D10) found no dynamic "give me whichever
30
+ remote client fills this role" lookup use case exists — a caller
31
+ always knows exactly which service it's calling — so none of the
32
+ machinery justifying `Capability`-based registry lookup applies here.
33
+ Wired by direct construction in a service's own `bootstrap`, the same
34
+ way `CommandHandler`/`QueryHandler` themselves are never looked up by
35
+ capability.
36
+ - **`openframe.core.testing.RemoteCommandClientContractTests`/
37
+ `RemoteQueryClientContractTests`** — extend the existing
38
+ `CommandHandlerContractTests`/`QueryHandlerContractTests` rather than
39
+ duplicating their test bodies (only the `isinstance` assertion
40
+ differs). No new fakes needed either — `SpyCommandHandler`/
41
+ `SpyQueryHandler` already satisfy the new protocols structurally,
42
+ confirmed directly rather than assumed.
43
+ - 12 new tests. Full suite: 422 passing (up from 410).
44
+
45
+ ---
46
+
47
+ ## v3.6.0 — 2026-10-09
48
+
49
+ ### Added
50
+
51
+ - **`openframe.core.ports.BaseGraphStore[T]`** — a new outbound port for
52
+ graph-store adapters (FalkorDB and similar), defined in
53
+ `ports/outbound/repository.py` alongside `BaseRepository`/
54
+ `BaseVectorStore` — same rationale as `BaseVectorStore` for staying in
55
+ this file rather than its own. Extends `BaseRepository[T]` directly:
56
+ a graph database's nodes are genuinely id-addressable
57
+ (create/get/update/delete map onto real `CREATE`/`MATCH ... WHERE
58
+ n.<key> = $id`/`SET`/`DETACH DELETE` operations), provided the adapter
59
+ addresses nodes by an application-level unique property (with a
60
+ backing index/constraint the adapter itself must create) rather than
61
+ a backend-internal node identity. FalkorDB's own `id()` function is
62
+ explicitly documented upstream as unstable — deleted nodes' higher
63
+ internal ids can be migrated down to fill vacated lower ones — which
64
+ is exactly why adapters must key off their own property, not a reason
65
+ graph databases don't fit this port. The one operation with no CRUD
66
+ equivalent is pattern-matching/traversal, so `BaseGraphStore` adds
67
+ exactly one new method, `traverse(query, params) -> list[T]`, which
68
+ also serves as the only way to create or query relationships/edges —
69
+ this port deliberately does not give edges their own method, the same
70
+ way it does not invent a bespoke query language. Marked
71
+ `.. stability: experimental`, same as `BaseVectorStore`;
72
+ `BaseRepository` itself is unaffected and remains stable.
73
+ - **`openframe.core.testing.GraphStoreContractTests`** — extends
74
+ `RepositoryContractTests` (inherits the full CRUD suite), adds four
75
+ `traverse()`-specific tests plus a `BaseGraphStore` protocol
76
+ conformance check. Deliberately query-language-agnostic: rather than
77
+ hardcoding Cypher syntax, it asks a `traverse_query_for(entity)`
78
+ fixture for a `(query, params)` pair in whatever language the adapter
79
+ under test actually speaks — mirroring how `VectorStoreContractTests`
80
+ asks for an `entity_vector` extractor instead of assuming a specific
81
+ similarity metric.
82
+ - **`openframe.core.testing.InMemoryGraphStore`** — the matching fake,
83
+ extending `InMemoryRepository`. Its own `traverse()` is a toy lookup
84
+ (recognizes a `params["id"]` convention; the `query` string itself is
85
+ accepted but not interpreted) proving the contract's *shape*, not a
86
+ query-engine simulation.
87
+ - 27 new tests (23 inherited `RepositoryContractTests`/`PortContractTests`
88
+ assertions re-run against `InMemoryGraphStore`, plus 4
89
+ `traverse()`-specific tests). Full suite: 410 passing (up from 383).
90
+
91
+ ---
92
+
93
+ ## v3.5.0 — 2026-10-08
94
+
95
+ ### Added
96
+
97
+ - **`openframe.core.ports.BaseVectorStore[T]`** — a new outbound port for
98
+ vector-store adapters (Milvus, Qdrant, ChromaDB), defined in
99
+ `ports/outbound/repository.py` alongside `BaseRepository` (not its own
100
+ file — deliberately, so the "why this extends `BaseRepository`"
101
+ rationale lives next to both classes it concerns). Extends
102
+ `BaseRepository[T]` directly rather than sitting as a disjoint sibling
103
+ port — most vector databases are genuinely id-addressable
104
+ (insert/upsert-with-id, fetch-by-id, delete-by-id, list/scroll), so the
105
+ five inherited CRUD methods are real operations for them, not fakes
106
+ forced to satisfy an interface. Adds exactly one new method,
107
+ `search(query_vector, k) -> list[T]`, the one operation with no
108
+ equivalent in `BaseRepository`. A vector store satisfying
109
+ `BaseVectorStore` also satisfies `BaseRepository` for free. Explicitly
110
+ does **not** target FAISS (a bare in-process index with no native
111
+ id→metadata store, and some index types that don't support deletion
112
+ without a full rebuild) or FalkorDB (fundamentally graph-oriented, not
113
+ id-addressable the same way) — both were evaluated and excluded;
114
+ graph databases need their own separate port design, not a forced fit
115
+ here.
116
+ - **`openframe.core.testing.contracts.VectorStoreContractTests`** — the
117
+ matching contract-test mixin, extending `RepositoryContractTests`
118
+ (inherits all its CRUD assertions, adds four search-specific ones).
119
+ Deliberately metric-agnostic: every search assertion uses an
120
+ exact-match query vector rather than asserting a specific similarity
121
+ score or ranking among near-ties, since backends default to different
122
+ metrics (cosine, L2, dot product) that would legitimately rank
123
+ approximately-similar vectors differently — an exact match ranks
124
+ first under any reasonable metric, so that's what the contract tests
125
+ rely on.
126
+ - **`openframe.core.testing.fakes.InMemoryVectorStore`** — the matching
127
+ fake, extending `InMemoryRepository` unchanged and adding a
128
+ brute-force Euclidean-distance `search()`. Proven against the full
129
+ `VectorStoreContractTests` suite in this release's own test suite
130
+ (27 new tests: 23 inherited CRUD/identity/lifecycle + 4 search-specific).
131
+ - All three pieces are `experimental` — new port, new contract-test
132
+ mixin, new fake, all added together ahead of the first real adapter
133
+ packages that will implement `BaseVectorStore` (Milvus, Qdrant,
134
+ ChromaDB).
135
+
136
+ ---
137
+
14
138
  ## v3.4.0 — 2026-09-28
15
139
 
16
140
  ### Added
@@ -425,7 +549,10 @@ Dev dependencies: `pytest>=8.0`, `pytest-asyncio>=0.23`, `httpx>=0.27`.
425
549
 
426
550
  ---
427
551
 
428
- [Unreleased]: https://github.com/openframe-org/openframe-core/compare/v3.4.0...HEAD
552
+ [Unreleased]: https://github.com/openframe-org/openframe-core/compare/v3.7.0...HEAD
553
+ [3.7.0]: https://github.com/openframe-org/openframe-core/compare/v3.6.0...v3.7.0
554
+ [3.6.0]: https://github.com/openframe-org/openframe-core/compare/v3.5.0...v3.6.0
555
+ [3.5.0]: https://github.com/openframe-org/openframe-core/compare/v3.4.0...v3.5.0
429
556
  [3.4.0]: https://github.com/openframe-org/openframe-core/compare/v3.3.0...v3.4.0
430
557
  [3.3.0]: https://github.com/openframe-org/openframe-core/compare/v3.2.1...v3.3.0
431
558
  [3.2.1]: https://github.com/openframe-org/openframe-core/compare/v3.2.0...v3.2.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: openframe-core
3
- Version: 3.4.0
3
+ Version: 3.7.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/
@@ -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,23 @@
1
+ """
2
+ openframe.core.remote
3
+ ========================
4
+ Outbound counterparts of openframe.core.inbound's CommandHandler/
5
+ QueryHandler — contracts for a service calling *another service built on
6
+ this platform*, as opposed to `openframe.core.ports.outbound` (third-party
7
+ backend adapters, Capability-tagged) or `openframe.core.inbound`
8
+ (callers reaching into this service).
9
+
10
+ Added for openframe-protocol's M1b (HTTP client) — see that repo's design
11
+ spec draft 8, section 5.6, decision D10. No BasePort/Capability
12
+ involvement; see `client.py`'s own docstring for why.
13
+
14
+ .. stability: experimental
15
+ New alongside the first concrete implementation
16
+ (`openframe-protocol-http`'s `HttpRemoteQueryClient`/
17
+ `HttpRemoteCommandClient`). API may change based on real usage.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from openframe.core.remote.client import RemoteCommandClient, RemoteQueryClient
22
+
23
+ __all__ = ["RemoteCommandClient", "RemoteQueryClient"]