openframe-core 3.4.0__tar.gz → 3.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openframe_core-3.4.0 → openframe_core-3.6.0}/PKG-INFO +1 -1
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/ports/__init__.py +10 -2
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/ports/outbound/__init__.py +24 -4
- openframe_core-3.6.0/openframe/core/ports/outbound/repository.py +372 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/__init__.py +8 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/__init__.py +10 -0
- openframe_core-3.6.0/openframe/core/testing/contracts/graph_store.py +143 -0
- openframe_core-3.6.0/openframe/core/testing/contracts/vector_store.py +148 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/__init__.py +4 -0
- openframe_core-3.6.0/openframe/core/testing/fakes/graph_store.py +120 -0
- openframe_core-3.6.0/openframe/core/testing/fakes/vector_store.py +128 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/pyproject.toml +1 -1
- {openframe_core-3.4.0 → openframe_core-3.6.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.6.0}/.github/CHANGELOG.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/.github/requirements/docs.txt +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/.github/requirements/test.txt +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/.github/workflows/app-test.yml +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/.github/workflows/auto-docs.yml +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/.github/workflows/python-build.yml +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/.gitignore +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/.vscode/settings.json +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/LICENSE +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/README.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/assets/images/banner.png +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/assets/images/favicon.png +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/assets/images/logo-dark.png +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/assets/images/logo-light.png +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/assets/javascripts/extra.js +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/assets/stylesheets/extra.css +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/cicd/pipeline.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/getting-started/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/modules/config.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/modules/exceptions.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/modules/health.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/modules/inbound.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/modules/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/modules/middleware.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/modules/plugins.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/modules/ports.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/modules/runtime.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/modules/telemetry.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/modules/testing.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/code/modules/tracing.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/developer-guide/debugging.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/developer-guide/explore.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/developer-guide/first-change.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/developer-guide/how-it-works.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/developer-guide/local-dev.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/developer-guide/quick-start/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/adr-007-schema-contracts.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/adrs/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/capability-taxonomy.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/component-lifecycle.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/data-flows.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/design-decisions.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/error-taxonomy.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/adapter-lifecycle.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/error-propagation.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/template-wiring.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/architecture/system-architecture.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/deployment/cicd.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/deployment/environments.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/deployment/secrets-and-config.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/infrastructure/networking.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/infrastructure/platform.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/infrastructure/storage.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/operations/index.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/operations/monitoring.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/adapter-connection-failure.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/queue-consumer-stuck.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/redis-connection-lost.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/overview/glossary.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/overview/roadmap.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/docs/technical/overview/system-overview.md +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/mkdocs.yml +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/config/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/config/settings.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/exceptions/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/exceptions/adapter.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/exceptions/base.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/exceptions/codes.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/exceptions/plugin.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/inbound/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/inbound/command.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/inbound/context.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/inbound/query.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/inbound/usecase.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/middleware/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/middleware/telemetry.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/middleware/types.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/plugins/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/plugins/registry.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/ports/capability.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/ports/context.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/ports/health.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/ports/identity.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/ports/lifecycle.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/ports/outbound/consumer.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/ports/outbound/producer.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/ports/port.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/resilience/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/resilience/circuit_breaker.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/runtime/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/runtime/bootstrap.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/schemas/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/telemetry/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/telemetry/setup.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/consumer.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/inbound.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/lifecycle.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/port.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/producer.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/contracts/repository.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/consumer.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/inbound.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/producer.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/fakes/repository.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/testing/fixtures.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/tracing/__init__.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/tracing/propagation.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/core/tracing/proxy.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/openframe/py.typed +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/conftest.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_config.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_exceptions.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_inbound.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_inbound_contracts.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_middleware.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_middleware_types.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_plugins.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_ports.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_ports_primitives.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_record_error.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_resilience.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_runtime.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_schemas.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_shutdown_telemetry.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_telemetry_sidecar.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_tracing.py +0 -0
- {openframe_core-3.4.0 → openframe_core-3.6.0}/tests/test_tracing_propagation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: openframe-core
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.0
|
|
4
4
|
Summary: OpenFrame Microservice Suite - platform kernel. Port contracts, testing utilities, plugin registry, and composition root.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Furious-Meteors/openframe-core
|
|
6
6
|
Project-URL: Documentation, https://furious-meteors.github.io/openframe-core/
|
|
@@ -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
|
+
...
|
|
@@ -33,6 +33,7 @@ Quick reference::
|
|
|
33
33
|
from __future__ import annotations
|
|
34
34
|
|
|
35
35
|
from openframe.core.testing.contracts.consumer import ConsumerContractTests
|
|
36
|
+
from openframe.core.testing.contracts.graph_store import GraphStoreContractTests
|
|
36
37
|
from openframe.core.testing.contracts.inbound import (
|
|
37
38
|
CommandHandlerContractTests,
|
|
38
39
|
QueryHandlerContractTests,
|
|
@@ -42,7 +43,9 @@ from openframe.core.testing.contracts.lifecycle import LifecycleContractTests
|
|
|
42
43
|
from openframe.core.testing.contracts.port import PortContractTests
|
|
43
44
|
from openframe.core.testing.contracts.producer import ProducerContractTests
|
|
44
45
|
from openframe.core.testing.contracts.repository import RepositoryContractTests
|
|
46
|
+
from openframe.core.testing.contracts.vector_store import VectorStoreContractTests
|
|
45
47
|
from openframe.core.testing.fakes.consumer import FakeConsumer
|
|
48
|
+
from openframe.core.testing.fakes.graph_store import InMemoryGraphStore
|
|
46
49
|
from openframe.core.testing.fakes.inbound import (
|
|
47
50
|
EchoUseCase,
|
|
48
51
|
SpyCommandHandler,
|
|
@@ -50,9 +53,12 @@ from openframe.core.testing.fakes.inbound import (
|
|
|
50
53
|
)
|
|
51
54
|
from openframe.core.testing.fakes.producer import FakeProducer
|
|
52
55
|
from openframe.core.testing.fakes.repository import InMemoryRepository
|
|
56
|
+
from openframe.core.testing.fakes.vector_store import InMemoryVectorStore
|
|
53
57
|
|
|
54
58
|
__all__ = [
|
|
55
59
|
"InMemoryRepository",
|
|
60
|
+
"InMemoryVectorStore",
|
|
61
|
+
"InMemoryGraphStore",
|
|
56
62
|
"FakeProducer",
|
|
57
63
|
"FakeConsumer",
|
|
58
64
|
"EchoUseCase",
|
|
@@ -61,6 +67,8 @@ __all__ = [
|
|
|
61
67
|
"LifecycleContractTests",
|
|
62
68
|
"PortContractTests",
|
|
63
69
|
"RepositoryContractTests",
|
|
70
|
+
"VectorStoreContractTests",
|
|
71
|
+
"GraphStoreContractTests",
|
|
64
72
|
"ProducerContractTests",
|
|
65
73
|
"ConsumerContractTests",
|
|
66
74
|
"UseCaseContractTests",
|
|
@@ -7,6 +7,12 @@ Outbound: ``PortContractTests``/``LifecycleContractTests`` cover the
|
|
|
7
7
|
shared ``BasePort`` identity/lifecycle suite; ``RepositoryContractTests``,
|
|
8
8
|
``ProducerContractTests``, ``ConsumerContractTests`` build on
|
|
9
9
|
``PortContractTests`` with domain-specific assertions.
|
|
10
|
+
``VectorStoreContractTests`` builds on ``RepositoryContractTests`` itself
|
|
11
|
+
(not directly on ``PortContractTests``), adding search-specific
|
|
12
|
+
assertions — mirroring how ``BaseVectorStore`` extends ``BaseRepository``.
|
|
13
|
+
``GraphStoreContractTests`` likewise builds on ``RepositoryContractTests``,
|
|
14
|
+
adding traverse-specific assertions — mirroring how ``BaseGraphStore``
|
|
15
|
+
extends ``BaseRepository``.
|
|
10
16
|
|
|
11
17
|
Inbound: ``UseCaseContractTests``, ``CommandHandlerContractTests``,
|
|
12
18
|
``QueryHandlerContractTests`` mirror the outbound contract classes for
|
|
@@ -22,6 +28,7 @@ pytest installed.
|
|
|
22
28
|
from __future__ import annotations
|
|
23
29
|
|
|
24
30
|
from openframe.core.testing.contracts.consumer import ConsumerContractTests
|
|
31
|
+
from openframe.core.testing.contracts.graph_store import GraphStoreContractTests
|
|
25
32
|
from openframe.core.testing.contracts.inbound import (
|
|
26
33
|
CommandHandlerContractTests,
|
|
27
34
|
QueryHandlerContractTests,
|
|
@@ -31,11 +38,14 @@ from openframe.core.testing.contracts.lifecycle import LifecycleContractTests
|
|
|
31
38
|
from openframe.core.testing.contracts.port import PortContractTests
|
|
32
39
|
from openframe.core.testing.contracts.producer import ProducerContractTests
|
|
33
40
|
from openframe.core.testing.contracts.repository import RepositoryContractTests
|
|
41
|
+
from openframe.core.testing.contracts.vector_store import VectorStoreContractTests
|
|
34
42
|
|
|
35
43
|
__all__ = [
|
|
36
44
|
"LifecycleContractTests",
|
|
37
45
|
"PortContractTests",
|
|
38
46
|
"RepositoryContractTests",
|
|
47
|
+
"VectorStoreContractTests",
|
|
48
|
+
"GraphStoreContractTests",
|
|
39
49
|
"ProducerContractTests",
|
|
40
50
|
"ConsumerContractTests",
|
|
41
51
|
"UseCaseContractTests",
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
openframe/core/testing/contracts/graph_store.py
|
|
3
|
+
==================================================
|
|
4
|
+
GraphStoreContractTests — reusable pytest base class.
|
|
5
|
+
|
|
6
|
+
Every ``openframe-adapters-db-{falkordb,...}`` package must inherit this
|
|
7
|
+
class and pass every test. Builds on
|
|
8
|
+
:class:`~openframe.core.testing.contracts.repository.RepositoryContractTests`
|
|
9
|
+
for the full id-addressable node CRUD contract (which itself builds on
|
|
10
|
+
:class:`~openframe.core.testing.contracts.port.PortContractTests` for
|
|
11
|
+
``BasePort`` identity + lifecycle), adding
|
|
12
|
+
:meth:`~openframe.core.ports.BaseGraphStore.traverse`-specific assertions
|
|
13
|
+
on top.
|
|
14
|
+
|
|
15
|
+
The traverse assertions are deliberately query-language-agnostic — they
|
|
16
|
+
never hardcode Cypher (or any other graph query language) syntax, since
|
|
17
|
+
different backends use different languages. Instead, each assertion asks
|
|
18
|
+
a fixture, ``traverse_query_for``, to produce a ``(query, params)`` pair
|
|
19
|
+
in whatever language the adapter under test actually speaks, that is
|
|
20
|
+
guaranteed to return exactly the one entity passed to it. This mirrors
|
|
21
|
+
how :class:`~openframe.core.testing.contracts.vector_store.VectorStoreContractTests`
|
|
22
|
+
asks for an ``entity_vector`` extractor rather than assuming a specific
|
|
23
|
+
similarity metric.
|
|
24
|
+
|
|
25
|
+
No top-level pytest import — this module can be imported without pytest
|
|
26
|
+
installed, matching every other module in this package.
|
|
27
|
+
|
|
28
|
+
.. stability: experimental
|
|
29
|
+
New alongside :class:`~openframe.core.ports.BaseGraphStore` itself.
|
|
30
|
+
|
|
31
|
+
Dependency order:
|
|
32
|
+
testing/contracts/graph_store → testing/contracts/repository + ports
|
|
33
|
+
|
|
34
|
+
Subclass usage::
|
|
35
|
+
|
|
36
|
+
class TestFalkorDBGraphStore(GraphStoreContractTests):
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
async def repository(self, falkordb_settings):
|
|
39
|
+
return FalkorDBNodeStore(falkordb_settings)
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def port(self, repository):
|
|
43
|
+
return repository
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def make_entity(self):
|
|
47
|
+
def _make(id: str, name: str = "test") -> Node:
|
|
48
|
+
return Node(id=id, name=name)
|
|
49
|
+
return _make
|
|
50
|
+
|
|
51
|
+
@pytest.fixture
|
|
52
|
+
def traverse_query_for(self):
|
|
53
|
+
# Given an entity, return a (query, params) pair that, run
|
|
54
|
+
# through traverse(), returns exactly that entity.
|
|
55
|
+
def _query_for(entity):
|
|
56
|
+
return "MATCH (n) WHERE n.id = $id RETURN n", {"id": entity.id}
|
|
57
|
+
return _query_for
|
|
58
|
+
"""
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
from openframe.core.testing.contracts.repository import RepositoryContractTests
|
|
62
|
+
|
|
63
|
+
__all__ = ["GraphStoreContractTests"]
|
|
64
|
+
|
|
65
|
+
__stability__ = "experimental"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class GraphStoreContractTests(RepositoryContractTests):
|
|
69
|
+
"""
|
|
70
|
+
Reusable pytest base class for graph store contract tests.
|
|
71
|
+
|
|
72
|
+
Subclasses must provide everything
|
|
73
|
+
:class:`~openframe.core.testing.contracts.repository.RepositoryContractTests`
|
|
74
|
+
requires (``repository``, ``port``, ``make_entity``), plus:
|
|
75
|
+
|
|
76
|
+
``traverse_query_for``
|
|
77
|
+
A callable ``(entity: T) -> tuple[str, dict[str, object]]`` that,
|
|
78
|
+
given an entity, returns a ``(query, params)`` pair in the
|
|
79
|
+
adapter's own query language that — once the entity has been
|
|
80
|
+
created — returns exactly that one entity when passed to
|
|
81
|
+
:meth:`~openframe.core.ports.BaseGraphStore.traverse`. Lets these
|
|
82
|
+
contract tests exercise ``traverse()`` without hardcoding Cypher
|
|
83
|
+
(or any other) syntax.
|
|
84
|
+
|
|
85
|
+
.. stability: experimental
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
# traverse
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
async def test_traverse_returns_empty_list_when_nothing_matches(
|
|
93
|
+
self, repository, make_entity, traverse_query_for
|
|
94
|
+
) -> None:
|
|
95
|
+
"""traverse() returns an empty list, never None, when nothing matches."""
|
|
96
|
+
entity = make_entity(id="traverse-absent")
|
|
97
|
+
query, params = traverse_query_for(entity)
|
|
98
|
+
|
|
99
|
+
results = await repository.traverse(query, params)
|
|
100
|
+
|
|
101
|
+
assert results == []
|
|
102
|
+
|
|
103
|
+
async def test_traverse_returns_created_entity(
|
|
104
|
+
self, repository, make_entity, traverse_query_for
|
|
105
|
+
) -> None:
|
|
106
|
+
"""traverse() returns an entity once it has been created."""
|
|
107
|
+
entity = make_entity(id="traverse-target")
|
|
108
|
+
await repository.create(entity)
|
|
109
|
+
query, params = traverse_query_for(entity)
|
|
110
|
+
|
|
111
|
+
results = await repository.traverse(query, params)
|
|
112
|
+
|
|
113
|
+
assert entity in results
|
|
114
|
+
|
|
115
|
+
async def test_traverse_result_is_a_list(
|
|
116
|
+
self, repository, make_entity, traverse_query_for
|
|
117
|
+
) -> None:
|
|
118
|
+
"""traverse() always returns a list, regardless of match count."""
|
|
119
|
+
entity = make_entity(id="traverse-shape")
|
|
120
|
+
query, params = traverse_query_for(entity)
|
|
121
|
+
|
|
122
|
+
results = await repository.traverse(query, params)
|
|
123
|
+
|
|
124
|
+
assert isinstance(results, list)
|
|
125
|
+
|
|
126
|
+
async def test_traverse_accepts_no_params(self, repository) -> None:
|
|
127
|
+
"""traverse() accepts params=None for queries with no parameters."""
|
|
128
|
+
results = await repository.traverse("RETURN 1", None)
|
|
129
|
+
assert isinstance(results, list)
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
# Protocol isinstance check
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
async def test_satisfies_base_graph_store_protocol(self, repository) -> None:
|
|
136
|
+
"""Graph store satisfies BaseGraphStore via structural subtyping."""
|
|
137
|
+
from openframe.core.ports import BaseGraphStore
|
|
138
|
+
|
|
139
|
+
assert isinstance(repository, BaseGraphStore), (
|
|
140
|
+
f"{type(repository).__name__} does not satisfy BaseGraphStore. "
|
|
141
|
+
"Ensure it implements traverse, plus everything BaseRepository/"
|
|
142
|
+
"BasePort require."
|
|
143
|
+
)
|