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.
- {openframe_core-3.4.0 → openframe_core-3.7.0}/.github/CHANGELOG.md +128 -1
- {openframe_core-3.4.0 → openframe_core-3.7.0}/PKG-INFO +1 -1
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/__init__.py +10 -2
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/outbound/__init__.py +24 -4
- openframe_core-3.7.0/openframe/core/ports/outbound/repository.py +372 -0
- openframe_core-3.7.0/openframe/core/remote/__init__.py +23 -0
- openframe_core-3.7.0/openframe/core/remote/client.py +135 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/__init__.py +14 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/__init__.py +16 -0
- openframe_core-3.7.0/openframe/core/testing/contracts/graph_store.py +143 -0
- openframe_core-3.7.0/openframe/core/testing/contracts/remote.py +79 -0
- openframe_core-3.7.0/openframe/core/testing/contracts/vector_store.py +148 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/fakes/__init__.py +4 -0
- openframe_core-3.7.0/openframe/core/testing/fakes/graph_store.py +120 -0
- openframe_core-3.7.0/openframe/core/testing/fakes/vector_store.py +128 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/pyproject.toml +1 -1
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_testing_fakes.py +79 -0
- openframe_core-3.4.0/openframe/core/ports/outbound/repository.py +0 -158
- {openframe_core-3.4.0 → openframe_core-3.7.0}/.github/requirements/docs.txt +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/.github/requirements/test.txt +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/.github/workflows/app-test.yml +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/.github/workflows/auto-docs.yml +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/.github/workflows/python-build.yml +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/.gitignore +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/.vscode/settings.json +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/LICENSE +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/README.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/assets/images/banner.png +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/assets/images/favicon.png +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/assets/images/logo-dark.png +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/assets/images/logo-light.png +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/assets/javascripts/extra.js +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/assets/stylesheets/extra.css +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/cicd/pipeline.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/getting-started/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/config.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/exceptions.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/health.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/inbound.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/middleware.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/plugins.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/ports.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/runtime.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/telemetry.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/testing.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/code/modules/tracing.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/developer-guide/debugging.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/developer-guide/explore.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/developer-guide/first-change.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/developer-guide/how-it-works.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/developer-guide/local-dev.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/developer-guide/quick-start/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/adr-007-schema-contracts.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/adrs/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/capability-taxonomy.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/component-lifecycle.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/data-flows.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/design-decisions.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/error-taxonomy.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/package-journey/adapter-lifecycle.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/package-journey/error-propagation.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/package-journey/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/package-journey/template-wiring.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/architecture/system-architecture.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/deployment/cicd.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/deployment/environments.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/deployment/secrets-and-config.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/infrastructure/networking.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/infrastructure/platform.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/infrastructure/storage.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/monitoring.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/runbooks/adapter-connection-failure.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/runbooks/queue-consumer-stuck.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/runbooks/redis-connection-lost.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/overview/glossary.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/overview/roadmap.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/docs/technical/overview/system-overview.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/mkdocs.yml +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/config/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/config/settings.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/exceptions/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/exceptions/adapter.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/exceptions/base.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/exceptions/codes.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/exceptions/plugin.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/inbound/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/inbound/command.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/inbound/context.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/inbound/query.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/inbound/usecase.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/middleware/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/middleware/telemetry.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/middleware/types.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/plugins/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/plugins/registry.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/capability.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/context.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/health.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/identity.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/lifecycle.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/outbound/consumer.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/outbound/producer.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/ports/port.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/resilience/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/resilience/circuit_breaker.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/runtime/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/runtime/bootstrap.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/schemas/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/telemetry/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/telemetry/setup.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/consumer.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/inbound.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/lifecycle.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/port.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/producer.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/contracts/repository.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/fakes/consumer.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/fakes/inbound.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/fakes/producer.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/fakes/repository.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/testing/fixtures.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/tracing/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/tracing/propagation.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/core/tracing/proxy.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/openframe/py.typed +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/conftest.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_config.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_exceptions.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_inbound.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_inbound_contracts.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_middleware.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_middleware_types.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_plugins.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_ports.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_ports_primitives.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_record_error.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_resilience.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_runtime.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_schemas.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_shutdown_telemetry.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_telemetry_sidecar.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.7.0}/tests/test_tracing.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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,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"]
|