intersect-sdk 0.6.4__tar.gz → 0.8.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.
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/PKG-INFO +5 -1
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/pyproject.toml +14 -2
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/__init__.py +13 -5
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/control_plane_manager.py +1 -1
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/function_metadata.py +4 -0
- intersect_sdk-0.8.0/src/intersect_sdk/_internal/interfaces.py +51 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/messages/userspace.py +19 -5
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/schema.py +110 -51
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/capability/base.py +60 -6
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/client.py +12 -57
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/client_callback_definitions.py +8 -60
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/config/shared.py +1 -0
- intersect_sdk-0.8.0/src/intersect_sdk/schema.py +70 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/service.py +406 -39
- intersect_sdk-0.8.0/src/intersect_sdk/service_callback_definitions.py +24 -0
- intersect_sdk-0.8.0/src/intersect_sdk/shared_callback_definitions.py +67 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/version.py +1 -1
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/e2e/test_examples.py +18 -7
- intersect_sdk-0.8.0/tests/fixtures/example_schema.json +1067 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/fixtures/example_schema.py +2 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/integration/test_return_type_mismatch.py +7 -4
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/integration/test_service.py +32 -30
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_base_capability_implementation.py +76 -2
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_invalid_schema_runtime.py +4 -2
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_schema_invalids.py +235 -52
- intersect_sdk-0.8.0/tests/unit/test_schema_valid.py +91 -0
- intersect_sdk-0.6.4/src/intersect_sdk/_internal/interfaces.py +0 -20
- intersect_sdk-0.6.4/src/intersect_sdk/schema.py +0 -77
- intersect_sdk-0.6.4/tests/fixtures/example_schema.json +0 -1062
- intersect_sdk-0.6.4/tests/unit/test_schema_valid.py +0 -79
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/LICENSE +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/README.md +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/__init__.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/constants.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/__init__.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/brokers/__init__.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/brokers/amqp_client.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/brokers/broker_client.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/brokers/mqtt_client.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/discovery_service.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/topic_handler.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/data_plane/__init__.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/data_plane/data_plane_manager.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/data_plane/minio_utils.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/event_metadata.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/exceptions.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/logger.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/messages/__init__.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/messages/event.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/messages/lifecycle.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/multi_flag_thread_event.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/pydantic_schema_generator.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/stoppable_thread.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/utils.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/version.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/version_resolver.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/app_lifecycle.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/capability/__init__.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/config/__init__.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/config/client.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/config/service.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/constants.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/core_definitions.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/py.typed +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/service_definitions.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/__init__.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/conftest.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/e2e/__init__.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/fixtures/__init__.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/fixtures/return_type_mismatch.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/integration/__init__.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/__init__.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_annotations.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_config.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_lifecycle_message.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_userspace_message.py +0 -0
- {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_version_resolver.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: intersect-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Python SDK to interact with INTERSECT
|
|
5
5
|
Keywords: intersect
|
|
6
6
|
Author-Email: Lance Drane <dranelt@ornl.gov>, Marshall McDonnell <mcdonnellmt@ornl.gov>, Seth Hitefield <hitefieldsd@ornl.gov>, Andrew Ayres <ayresaf@ornl.gov>, Gregory Cage <cagege@ornl.gov>, Jesse McGaha <mcgahajr@ornl.gov>, Robert Smith <smithrw@ornl.gov>, Gavin Wiggins <wigginsg@ornl.gov>, Michael Brim <brimmj@ornl.gov>, Rick Archibald <archibaldrk@ornl.gov>, Addi Malviya Thakur <malviyaa@ornl.gov>
|
|
@@ -10,6 +10,10 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.9
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Project-URL: Homepage, https://github.com/INTERSECT-SDK/python-sdk/
|
|
14
|
+
Project-URL: Changelog, https://github.com/INTERSECT-SDK/python-sdk/blob/main/CHANGELOG.md
|
|
15
|
+
Project-URL: Documentation, https://intersect-python-sdk.readthedocs.io/en/latest/
|
|
16
|
+
Project-URL: Issues, https://github.com/INTERSECT-SDK/python-sdk/issues
|
|
13
17
|
Requires-Python: <4.0,>=3.8.10
|
|
14
18
|
Requires-Dist: pydantic>=2.7.0
|
|
15
19
|
Requires-Dist: retrying<2.0.0,>=1.3.4
|
|
@@ -35,11 +35,17 @@ dependencies = [
|
|
|
35
35
|
"jsonschema[format-nongpl]>=4.21.1",
|
|
36
36
|
"eval-type-backport>=0.1.3;python_version<'3.10'",
|
|
37
37
|
]
|
|
38
|
-
version = "0.
|
|
38
|
+
version = "0.8.0"
|
|
39
39
|
|
|
40
40
|
[project.license]
|
|
41
41
|
text = "BSD-3-Clause"
|
|
42
42
|
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/INTERSECT-SDK/python-sdk/"
|
|
45
|
+
Changelog = "https://github.com/INTERSECT-SDK/python-sdk/blob/main/CHANGELOG.md"
|
|
46
|
+
Documentation = "https://intersect-python-sdk.readthedocs.io/en/latest/"
|
|
47
|
+
Issues = "https://github.com/INTERSECT-SDK/python-sdk/issues"
|
|
48
|
+
|
|
43
49
|
[project.optional-dependencies]
|
|
44
50
|
amqp = [
|
|
45
51
|
"pika>=1.3.2,<2.0.0",
|
|
@@ -52,7 +58,7 @@ docs = [
|
|
|
52
58
|
[tool.pdm.dev-dependencies]
|
|
53
59
|
lint = [
|
|
54
60
|
"pre-commit>=3.3.1",
|
|
55
|
-
"ruff
|
|
61
|
+
"ruff==0.5.7",
|
|
56
62
|
"mypy>=1.10.0",
|
|
57
63
|
"types-paho-mqtt>=1.6.0.20240106",
|
|
58
64
|
]
|
|
@@ -66,6 +72,7 @@ test = [
|
|
|
66
72
|
test-all = "pytest tests/ --cov=src/intersect_sdk/ --cov-fail-under=80 --cov-report=html:reports/htmlcov/ --cov-report=xml:reports/coverage_report.xml --junitxml=reports/junit.xml"
|
|
67
73
|
test-all-debug = "pytest tests/ --cov=src/intersect_sdk/ --cov-fail-under=80 --cov-report=html:reports/htmlcov/ --cov-report=xml:reports/coverage_report.xml --junitxml=reports/junit.xml -s"
|
|
68
74
|
test-unit = "pytest tests/unit --cov=src/intersect_sdk/"
|
|
75
|
+
test-e2e = "pytest tests/e2e --cov=src/intersect_sdk/"
|
|
69
76
|
lint-format = "ruff format"
|
|
70
77
|
lint-ruff = "ruff check --fix"
|
|
71
78
|
lint-mypy = "mypy src/intersect_sdk/"
|
|
@@ -162,6 +169,7 @@ max-complexity = 20
|
|
|
162
169
|
max-args = 10
|
|
163
170
|
max-branches = 20
|
|
164
171
|
max-returns = 10
|
|
172
|
+
max-statements = 75
|
|
165
173
|
|
|
166
174
|
[tool.ruff.lint.extend-per-file-ignores]
|
|
167
175
|
"__init__.py" = [
|
|
@@ -212,11 +220,15 @@ addopts = "-ra"
|
|
|
212
220
|
[tool.coverage.report]
|
|
213
221
|
omit = [
|
|
214
222
|
"*__init__*",
|
|
223
|
+
"*/discovery_service.py",
|
|
215
224
|
]
|
|
216
225
|
exclude_also = [
|
|
217
226
|
"pragma: no-cover",
|
|
218
227
|
"if (typing\\\\.)?TYPE_CHECKING:",
|
|
219
228
|
"@(abc\\\\.)?abstractmethod",
|
|
229
|
+
"class .*\\bProtocol\\):",
|
|
230
|
+
"raise NotImplementedError",
|
|
231
|
+
"except.* ImportError",
|
|
220
232
|
]
|
|
221
233
|
|
|
222
234
|
[build-system]
|
|
@@ -13,27 +13,33 @@ from .client import IntersectClient
|
|
|
13
13
|
from .client_callback_definitions import (
|
|
14
14
|
INTERSECT_CLIENT_EVENT_CALLBACK_TYPE,
|
|
15
15
|
INTERSECT_CLIENT_RESPONSE_CALLBACK_TYPE,
|
|
16
|
-
INTERSECT_JSON_VALUE,
|
|
17
16
|
IntersectClientCallback,
|
|
18
|
-
IntersectClientMessageParams,
|
|
19
17
|
)
|
|
20
18
|
from .config.client import IntersectClientConfig
|
|
21
19
|
from .config.service import IntersectServiceConfig
|
|
22
20
|
from .config.shared import (
|
|
23
21
|
ControlPlaneConfig,
|
|
22
|
+
ControlProvider,
|
|
24
23
|
DataStoreConfig,
|
|
25
24
|
DataStoreConfigMap,
|
|
26
25
|
HierarchyConfig,
|
|
27
26
|
)
|
|
28
27
|
from .core_definitions import IntersectDataHandler, IntersectMimeType
|
|
29
|
-
from .schema import
|
|
28
|
+
from .schema import get_schema_from_capability_implementations
|
|
30
29
|
from .service import IntersectService
|
|
30
|
+
from .service_callback_definitions import (
|
|
31
|
+
INTERSECT_SERVICE_RESPONSE_CALLBACK_TYPE,
|
|
32
|
+
)
|
|
31
33
|
from .service_definitions import (
|
|
32
34
|
IntersectEventDefinition,
|
|
33
35
|
intersect_event,
|
|
34
36
|
intersect_message,
|
|
35
37
|
intersect_status,
|
|
36
38
|
)
|
|
39
|
+
from .shared_callback_definitions import (
|
|
40
|
+
INTERSECT_JSON_VALUE,
|
|
41
|
+
IntersectDirectMessageParams,
|
|
42
|
+
)
|
|
37
43
|
from .version import __version__, version_info, version_string
|
|
38
44
|
|
|
39
45
|
__all__ = [
|
|
@@ -43,20 +49,22 @@ __all__ = [
|
|
|
43
49
|
'intersect_event',
|
|
44
50
|
'intersect_message',
|
|
45
51
|
'intersect_status',
|
|
46
|
-
'
|
|
52
|
+
'get_schema_from_capability_implementations',
|
|
47
53
|
'IntersectService',
|
|
48
54
|
'IntersectClient',
|
|
49
55
|
'IntersectClientCallback',
|
|
50
|
-
'
|
|
56
|
+
'IntersectDirectMessageParams',
|
|
51
57
|
'INTERSECT_CLIENT_RESPONSE_CALLBACK_TYPE',
|
|
52
58
|
'INTERSECT_CLIENT_EVENT_CALLBACK_TYPE',
|
|
53
59
|
'INTERSECT_JSON_VALUE',
|
|
60
|
+
'INTERSECT_SERVICE_RESPONSE_CALLBACK_TYPE',
|
|
54
61
|
'IntersectBaseCapabilityImplementation',
|
|
55
62
|
'default_intersect_lifecycle_loop',
|
|
56
63
|
'IntersectClientConfig',
|
|
57
64
|
'IntersectServiceConfig',
|
|
58
65
|
'HierarchyConfig',
|
|
59
66
|
'ControlPlaneConfig',
|
|
67
|
+
'ControlProvider',
|
|
60
68
|
'DataStoreConfig',
|
|
61
69
|
'DataStoreConfigMap',
|
|
62
70
|
'__version__',
|
|
@@ -66,7 +66,7 @@ class ControlPlaneManager:
|
|
|
66
66
|
"""
|
|
67
67
|
if control_configs == 'discovery':
|
|
68
68
|
msg = 'Discovery service not implemented yet'
|
|
69
|
-
raise
|
|
69
|
+
raise NotImplementedError(msg)
|
|
70
70
|
self._control_providers = [
|
|
71
71
|
create_control_provider(config, self.get_subscription_channels)
|
|
72
72
|
for config in control_configs
|
{intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/function_metadata.py
RENAMED
|
@@ -12,6 +12,10 @@ class FunctionMetadata(NamedTuple):
|
|
|
12
12
|
NOTE: both this class and all properties in it should remain immutable after creation
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
+
capability: type
|
|
16
|
+
"""
|
|
17
|
+
The type of the class that implements the target method.
|
|
18
|
+
"""
|
|
15
19
|
method: Callable[[Any], Any]
|
|
16
20
|
"""
|
|
17
21
|
The raw method of the function. The function itself is useless and should not be called,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from ..service_callback_definitions import (
|
|
10
|
+
INTERSECT_SERVICE_RESPONSE_CALLBACK_TYPE,
|
|
11
|
+
)
|
|
12
|
+
from ..shared_callback_definitions import (
|
|
13
|
+
IntersectDirectMessageParams,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IntersectEventObserver(ABC):
|
|
18
|
+
"""Abstract definition of an entity which observes an INTERSECT event (i.e. IntersectService).
|
|
19
|
+
|
|
20
|
+
Used as the common interface for event emitters (i.e. CapabilityImplementations).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def _on_observe_event(self, event_name: str, event_value: Any, operation: str) -> None:
|
|
25
|
+
"""How to react to an event being fired.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
event_name: The key of the event which is fired.
|
|
29
|
+
event_value: The value of the event which is fired.
|
|
30
|
+
operation: The source of the event (generally the function name, not directly invoked by application devs)
|
|
31
|
+
"""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def create_external_request(
|
|
36
|
+
self,
|
|
37
|
+
request: IntersectDirectMessageParams,
|
|
38
|
+
response_handler: INTERSECT_SERVICE_RESPONSE_CALLBACK_TYPE | None = None,
|
|
39
|
+
timeout: float = 300.0,
|
|
40
|
+
) -> UUID:
|
|
41
|
+
"""Observed entity (capabilitiy) tells observer (i.e. service) to send an external request.
|
|
42
|
+
|
|
43
|
+
Params:
|
|
44
|
+
- request: the request we want to send out, encapsulated as an IntersectClientMessageParams object
|
|
45
|
+
- response_handler: optional callback for how we want to handle the response from this request.
|
|
46
|
+
- timeout: optional value for how long we should wait on the request, in seconds (default: 300 seconds)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
- generated RequestID associated with your request
|
|
50
|
+
"""
|
|
51
|
+
...
|
{intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/messages/userspace.py
RENAMED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
This module is internal-facing and should not be used directly by users.
|
|
4
4
|
|
|
5
|
+
Services have two associated channels which handle userspace messages: their request channel
|
|
6
|
+
and their response channel. Services always CONSUME messages from these channels, but never PRODUCE messages
|
|
7
|
+
on these channels. (A message is always sent in the receiver's namespace).
|
|
8
|
+
|
|
9
|
+
The response channel is how the service handles external requests, the request channel is used when this service itself
|
|
10
|
+
needs to make external requests through INTERSECT.
|
|
11
|
+
|
|
5
12
|
Services should ALWAYS be CONSUMING from their userspace channel.
|
|
6
13
|
They should NEVER be PRODUCING messages on their userspace channel.
|
|
7
14
|
|
|
@@ -9,6 +16,8 @@ Clients should be CONSUMING from their userspace channel, but should only get me
|
|
|
9
16
|
from services they explicitly messaged.
|
|
10
17
|
"""
|
|
11
18
|
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
12
21
|
import datetime
|
|
13
22
|
import uuid
|
|
14
23
|
from typing import Any, Union
|
|
@@ -16,10 +25,13 @@ from typing import Any, Union
|
|
|
16
25
|
from pydantic import AwareDatetime, Field, TypeAdapter
|
|
17
26
|
from typing_extensions import Annotated, TypedDict
|
|
18
27
|
|
|
19
|
-
from ...constants import SYSTEM_OF_SYSTEM_REGEX
|
|
20
|
-
from ...core_definitions import
|
|
28
|
+
from ...constants import SYSTEM_OF_SYSTEM_REGEX # noqa: TCH001 (this is runtime checked)
|
|
29
|
+
from ...core_definitions import ( # noqa: TCH001 (this is runtime checked)
|
|
30
|
+
IntersectDataHandler,
|
|
31
|
+
IntersectMimeType,
|
|
32
|
+
)
|
|
21
33
|
from ...version import version_string
|
|
22
|
-
from ..data_plane.minio_utils import MinioPayload
|
|
34
|
+
from ..data_plane.minio_utils import MinioPayload # noqa: TCH001 (this is runtime checked)
|
|
23
35
|
|
|
24
36
|
|
|
25
37
|
class UserspaceMessageHeader(TypedDict):
|
|
@@ -115,7 +127,7 @@ class UserspaceMessage(TypedDict):
|
|
|
115
127
|
the headers of the message
|
|
116
128
|
"""
|
|
117
129
|
|
|
118
|
-
payload: Union[bytes, MinioPayload] # noqa:
|
|
130
|
+
payload: Union[bytes, MinioPayload] # noqa: UP007 (Pydantic uses runtime annotations)
|
|
119
131
|
"""
|
|
120
132
|
main payload of the message. Needs to match the schema format, including the content type.
|
|
121
133
|
|
|
@@ -141,11 +153,13 @@ def create_userspace_message(
|
|
|
141
153
|
content_type: IntersectMimeType,
|
|
142
154
|
data_handler: IntersectDataHandler,
|
|
143
155
|
payload: Any,
|
|
156
|
+
message_id: uuid.UUID | None = None,
|
|
144
157
|
has_error: bool = False,
|
|
145
158
|
) -> UserspaceMessage:
|
|
146
159
|
"""Payloads depend on the data_handler and has_error."""
|
|
160
|
+
msg_id = message_id if message_id else uuid.uuid4()
|
|
147
161
|
return UserspaceMessage(
|
|
148
|
-
messageId=
|
|
162
|
+
messageId=msg_id,
|
|
149
163
|
operationId=operation_id,
|
|
150
164
|
contentType=content_type,
|
|
151
165
|
payload=payload,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import inspect
|
|
6
|
+
import re
|
|
6
7
|
from enum import Enum
|
|
7
8
|
from typing import (
|
|
8
9
|
TYPE_CHECKING,
|
|
@@ -55,11 +56,14 @@ For networking types, https://docs.pydantic.dev/latest/api/networks/
|
|
|
55
56
|
For a complete reference, https://docs.pydantic.dev/latest/concepts/conversion_table
|
|
56
57
|
"""
|
|
57
58
|
|
|
59
|
+
CAPABILITY_NAME_PATTERN = r'[\w-]+'
|
|
60
|
+
"""Regular expression we use to check valid capability names. Since capability namespacing only occurs in services, we can be more lax than for how we name services/systems/etc. """
|
|
61
|
+
|
|
58
62
|
|
|
59
63
|
class _FunctionAnalysisResult(NamedTuple):
|
|
60
64
|
"""private class generated from static analysis of function."""
|
|
61
65
|
|
|
62
|
-
|
|
66
|
+
class_name: str
|
|
63
67
|
method_name: str
|
|
64
68
|
method: Callable[[Any], Any]
|
|
65
69
|
"""raw method is for inspecting attributes"""
|
|
@@ -218,18 +222,18 @@ def _status_fn_schema(
|
|
|
218
222
|
- The status function's schema
|
|
219
223
|
- The TypeAdapter to use for serializing outgoing responses
|
|
220
224
|
"""
|
|
221
|
-
|
|
225
|
+
class_name, status_fn_name, status_fn, min_params = status_info
|
|
222
226
|
status_signature = inspect.signature(status_fn)
|
|
223
227
|
method_params = tuple(status_signature.parameters.values())
|
|
224
228
|
if len(method_params) != min_params or any(
|
|
225
229
|
p.kind not in (p.POSITIONAL_OR_KEYWORD, p.POSITIONAL_ONLY) for p in method_params
|
|
226
230
|
):
|
|
227
231
|
die(
|
|
228
|
-
f"On capability '{
|
|
232
|
+
f"On capability '{class_name}', capability status function '{status_fn_name}' should have no parameters other than 'self' (unless a staticmethod), and should not use keyword or variable length arguments (i.e. '*', *args, **kwargs)."
|
|
229
233
|
)
|
|
230
234
|
if status_signature.return_annotation is inspect.Signature.empty:
|
|
231
235
|
die(
|
|
232
|
-
f"On capability '{
|
|
236
|
+
f"On capability '{class_name}', capability status function '{status_fn_name}' should have a valid return annotation."
|
|
233
237
|
)
|
|
234
238
|
try:
|
|
235
239
|
status_adapter = TypeAdapter(status_signature.return_annotation)
|
|
@@ -246,12 +250,12 @@ def _status_fn_schema(
|
|
|
246
250
|
)
|
|
247
251
|
except PydanticUserError as e:
|
|
248
252
|
die(
|
|
249
|
-
f"On capability '{
|
|
253
|
+
f"On capability '{class_name}', return annotation '{status_signature.return_annotation}' on function '{status_fn_name}' is invalid.\n{e}"
|
|
250
254
|
)
|
|
251
255
|
|
|
252
256
|
|
|
253
257
|
def _add_events(
|
|
254
|
-
|
|
258
|
+
class_name: str,
|
|
255
259
|
function_name: str,
|
|
256
260
|
schemas: dict[str, Any],
|
|
257
261
|
event_schemas: dict[str, Any],
|
|
@@ -272,13 +276,13 @@ def _add_events(
|
|
|
272
276
|
for d in differences_from_cache
|
|
273
277
|
)
|
|
274
278
|
die(
|
|
275
|
-
f"On capability '{
|
|
279
|
+
f"On capability '{class_name}', event key '{event_key}' on function '{function_name}' was previously defined differently. \n{diff_str}\n"
|
|
276
280
|
)
|
|
277
281
|
metadata_value.operations.add(function_name)
|
|
278
282
|
else:
|
|
279
283
|
if event_definition.data_handler in excluded_data_handlers:
|
|
280
284
|
die(
|
|
281
|
-
f"On capability '{
|
|
285
|
+
f"On capability '{class_name}', function '{function_name}' should not set data_handler as {event_definition.data_handler} unless an instance is configured in IntersectConfig.data_stores ."
|
|
282
286
|
)
|
|
283
287
|
try:
|
|
284
288
|
event_adapter: TypeAdapter[Any] = TypeAdapter(event_definition.event_type)
|
|
@@ -297,7 +301,7 @@ def _add_events(
|
|
|
297
301
|
)
|
|
298
302
|
except PydanticUserError as e:
|
|
299
303
|
die(
|
|
300
|
-
f"On capability '{
|
|
304
|
+
f"On capability '{class_name}', event key '{event_key}' on function '{function_name}' has an invalid value in the events mapping.\n{e}"
|
|
301
305
|
)
|
|
302
306
|
|
|
303
307
|
|
|
@@ -332,10 +336,14 @@ def _introspection_baseline(
|
|
|
332
336
|
function_map = {}
|
|
333
337
|
event_metadatas: dict[str, EventMetadata] = {}
|
|
334
338
|
|
|
339
|
+
# capability_name should have already been checked before calling this function
|
|
340
|
+
cap_name = capability.intersect_sdk_capability_name
|
|
335
341
|
status_func, response_funcs, event_funcs = _get_functions(capability)
|
|
336
342
|
|
|
337
343
|
# parse functions
|
|
338
|
-
for
|
|
344
|
+
for class_name, name, method, min_params in response_funcs:
|
|
345
|
+
public_name = f'{cap_name}.{name}'
|
|
346
|
+
|
|
339
347
|
# TODO - I'm placing this here for now because we'll eventually want to capture data plane and broker configs in the schema.
|
|
340
348
|
# (It's possible we may want to separate the backing service schema from the application logic, but it's unlikely.)
|
|
341
349
|
# At the moment, we're just validating that users can support their response_data_handler property.
|
|
@@ -343,7 +351,7 @@ def _introspection_baseline(
|
|
|
343
351
|
data_handler = getattr(method, RESPONSE_DATA)
|
|
344
352
|
if data_handler in excluded_data_handlers:
|
|
345
353
|
die(
|
|
346
|
-
f"On capability '{
|
|
354
|
+
f"On capability '{class_name}', function '{name}' should not set response_data_type as {data_handler} unless an instance is configured in IntersectConfig.data_stores ."
|
|
347
355
|
)
|
|
348
356
|
|
|
349
357
|
docstring = inspect.cleandoc(method.__doc__) if method.__doc__ else None
|
|
@@ -358,7 +366,7 @@ def _introspection_baseline(
|
|
|
358
366
|
)
|
|
359
367
|
):
|
|
360
368
|
die(
|
|
361
|
-
f"On capability '{
|
|
369
|
+
f"On capability '{class_name}', function '{name}' should have 'self' (unless a staticmethod) and zero or one additional parameters, and should not use keyword or variable length arguments (i.e. '*', *args, **kwargs)."
|
|
362
370
|
)
|
|
363
371
|
|
|
364
372
|
# The schema format should be hard-coded and determined based on how Pydantic parses the schema.
|
|
@@ -390,7 +398,7 @@ def _introspection_baseline(
|
|
|
390
398
|
# Pydantic BaseModels require annotations even if using a default value, so we'll remain consistent.
|
|
391
399
|
if annotation is inspect.Parameter.empty:
|
|
392
400
|
die(
|
|
393
|
-
f"On capability '{
|
|
401
|
+
f"On capability '{class_name}', parameter '{parameter.name}' type annotation on function '{name}' missing. {SCHEMA_HELP_MSG}"
|
|
394
402
|
)
|
|
395
403
|
# rationale for disallowing function default values:
|
|
396
404
|
# https://docs.pydantic.dev/latest/concepts/validation_decorator/#using-field-to-describe-function-arguments
|
|
@@ -398,7 +406,7 @@ def _introspection_baseline(
|
|
|
398
406
|
# also makes TypeAdapters considerably easier to construct
|
|
399
407
|
if parameter.default is not inspect.Parameter.empty:
|
|
400
408
|
die(
|
|
401
|
-
f"On capability '{
|
|
409
|
+
f"On capability '{class_name}', parameter '{parameter.name}' should not use a default value in the function parameter (use 'typing_extensions.Annotated[TYPE, pydantic.Field(default=<DEFAULT>)]' instead - 'default_factory' is an acceptable, mutually exclusive argument to 'Field')."
|
|
402
410
|
)
|
|
403
411
|
try:
|
|
404
412
|
function_cache_request_adapter = TypeAdapter(annotation)
|
|
@@ -410,7 +418,7 @@ def _introspection_baseline(
|
|
|
410
418
|
)
|
|
411
419
|
except PydanticUserError as e:
|
|
412
420
|
die(
|
|
413
|
-
f"On capability '{
|
|
421
|
+
f"On capability '{class_name}', parameter '{parameter.name}' type annotation '{annotation}' on function '{name}' is invalid\n{e}"
|
|
414
422
|
)
|
|
415
423
|
|
|
416
424
|
else:
|
|
@@ -419,7 +427,7 @@ def _introspection_baseline(
|
|
|
419
427
|
# this block handles response parameters
|
|
420
428
|
if return_annotation is inspect.Signature.empty:
|
|
421
429
|
die(
|
|
422
|
-
f"On capability '{
|
|
430
|
+
f"On capability '{class_name}', return type annotation on function '{name}' missing. {SCHEMA_HELP_MSG}"
|
|
423
431
|
)
|
|
424
432
|
try:
|
|
425
433
|
function_cache_response_adapter = TypeAdapter(return_annotation)
|
|
@@ -431,11 +439,12 @@ def _introspection_baseline(
|
|
|
431
439
|
)
|
|
432
440
|
except PydanticUserError as e:
|
|
433
441
|
die(
|
|
434
|
-
f"On capability '{
|
|
442
|
+
f"On capability '{class_name}', return annotation '{return_annotation}' on function '{name}' is invalid.\n{e}"
|
|
435
443
|
)
|
|
436
444
|
|
|
437
445
|
# final function mapping
|
|
438
|
-
function_map[
|
|
446
|
+
function_map[public_name] = FunctionMetadata(
|
|
447
|
+
capability,
|
|
439
448
|
method,
|
|
440
449
|
function_cache_request_adapter,
|
|
441
450
|
function_cache_response_adapter,
|
|
@@ -444,7 +453,7 @@ def _introspection_baseline(
|
|
|
444
453
|
# this block handles events associated with intersect_messages (implies command pattern)
|
|
445
454
|
function_events: dict[str, IntersectEventDefinition] = getattr(method, EVENT_ATTR_KEY)
|
|
446
455
|
_add_events(
|
|
447
|
-
|
|
456
|
+
class_name,
|
|
448
457
|
name,
|
|
449
458
|
schemas,
|
|
450
459
|
event_schemas,
|
|
@@ -455,9 +464,9 @@ def _introspection_baseline(
|
|
|
455
464
|
channels[name]['events'] = list(function_events.keys())
|
|
456
465
|
|
|
457
466
|
# parse global schemas
|
|
458
|
-
for
|
|
467
|
+
for class_name, name, method, _ in event_funcs:
|
|
459
468
|
_add_events(
|
|
460
|
-
|
|
469
|
+
class_name,
|
|
461
470
|
name,
|
|
462
471
|
schemas,
|
|
463
472
|
event_schemas,
|
|
@@ -471,7 +480,9 @@ def _introspection_baseline(
|
|
|
471
480
|
)
|
|
472
481
|
# this conditional allows for the status function to also be called like a message
|
|
473
482
|
if status_fn_type_adapter and status_fn and status_fn_name:
|
|
474
|
-
|
|
483
|
+
public_status_name = f'{cap_name}.{status_fn_name}'
|
|
484
|
+
function_map[public_status_name] = FunctionMetadata(
|
|
485
|
+
capability,
|
|
475
486
|
status_fn,
|
|
476
487
|
None,
|
|
477
488
|
status_fn_type_adapter,
|
|
@@ -487,44 +498,99 @@ def _introspection_baseline(
|
|
|
487
498
|
)
|
|
488
499
|
|
|
489
500
|
|
|
490
|
-
def
|
|
491
|
-
|
|
492
|
-
|
|
501
|
+
def get_schema_and_functions_from_capability_implementations(
|
|
502
|
+
capabilities: list[type[IntersectBaseCapabilityImplementation]],
|
|
503
|
+
service_name: HierarchyConfig,
|
|
493
504
|
excluded_data_handlers: set[IntersectDataHandler],
|
|
494
505
|
) -> tuple[
|
|
495
506
|
dict[str, Any],
|
|
496
507
|
dict[str, FunctionMetadata],
|
|
497
508
|
dict[str, EventMetadata],
|
|
509
|
+
type[IntersectBaseCapabilityImplementation] | None,
|
|
498
510
|
str | None,
|
|
499
511
|
TypeAdapter[Any] | None,
|
|
500
512
|
]:
|
|
501
513
|
"""This function generates the core AsyncAPI schema, and the core mappings which are derived from the schema.
|
|
502
514
|
|
|
515
|
+
Importantly, this function needs to be able to work with static classes, and not instances. This is because users
|
|
516
|
+
should be free to define their constructor as they wish, with any arbitrary parameters. Users should also be allowed
|
|
517
|
+
to execute whatever code they'd like in their constructor, such as establishing remote connections. At the same time,
|
|
518
|
+
we want to allow users to quickly generate an INTERSECT schema, without having to worry about any dependencies from their constructor code.
|
|
519
|
+
|
|
503
520
|
In-depth introspection is handled later on.
|
|
504
521
|
"""
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
522
|
+
status_function_cap: type[IntersectBaseCapabilityImplementation] | None = None
|
|
523
|
+
status_function_name: str | None = None
|
|
524
|
+
status_function_schema: dict[str, Any] | None = None
|
|
525
|
+
status_function_adapter: TypeAdapter[Any] | None = None
|
|
526
|
+
shared_schemas: dict[Any, Any] = {} # "shared" schemas which get put in $defs
|
|
527
|
+
capability_schemas: dict[str, Any] = {} # endpoint schemas
|
|
528
|
+
function_map: dict[str, FunctionMetadata] = {} # endpoint functionality
|
|
529
|
+
events: dict[
|
|
530
|
+
str, Any
|
|
531
|
+
] = {} # event schemas - TODO event names are currently "global" across capabilities, may want to change this?
|
|
532
|
+
event_map: dict[str, EventMetadata] = {} # event functionality
|
|
533
|
+
for capability_type in capabilities:
|
|
534
|
+
cap_name = capability_type.intersect_sdk_capability_name
|
|
535
|
+
if (
|
|
536
|
+
not cap_name
|
|
537
|
+
or not isinstance(cap_name, str)
|
|
538
|
+
or not re.fullmatch(CAPABILITY_NAME_PATTERN, cap_name)
|
|
539
|
+
):
|
|
540
|
+
die(
|
|
541
|
+
f'Invalid intersect_sdk_capability_name on capability {capability_type.__name__} - must be a non-empty string with only alphanumeric characters and hyphens (you must explicitly set this, and do so on the class and not an instance).'
|
|
542
|
+
)
|
|
543
|
+
if cap_name in capability_schemas:
|
|
544
|
+
die(
|
|
545
|
+
f'Invalid intersect_sdk_capability_name on capability {capability_type.__name__} - value "{cap_name}" is a duplicate and has already appeared in another capability.'
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
(
|
|
549
|
+
subschemas,
|
|
550
|
+
(cap_status_fn_name, cap_status_schema, cap_status_type_adapter),
|
|
551
|
+
cap_functions,
|
|
552
|
+
cap_function_map,
|
|
553
|
+
cap_events,
|
|
554
|
+
cap_event_map,
|
|
555
|
+
) = _introspection_baseline(capability_type, excluded_data_handlers)
|
|
556
|
+
|
|
557
|
+
if cap_status_fn_name and cap_status_schema and cap_status_type_adapter:
|
|
558
|
+
if status_function_name is not None:
|
|
559
|
+
# TODO may want to change this later
|
|
560
|
+
die('Only one capabilitiy may have an @intersect_status function')
|
|
561
|
+
status_function_cap = capability_type
|
|
562
|
+
status_function_name = cap_status_fn_name
|
|
563
|
+
status_function_schema = cap_status_schema
|
|
564
|
+
status_function_adapter = cap_status_type_adapter
|
|
565
|
+
|
|
566
|
+
shared_schemas.update(subschemas)
|
|
567
|
+
# NOTE: we will still add the capability to the schema, even if there are no @intersect_message annotations
|
|
568
|
+
capability_schemas[cap_name] = {
|
|
569
|
+
'channels': cap_functions,
|
|
570
|
+
}
|
|
571
|
+
# add documentation for the capabilities
|
|
572
|
+
if capability_type.__doc__:
|
|
573
|
+
capability_schemas[cap_name]['description'] = inspect.cleandoc(capability_type.__doc__)
|
|
574
|
+
function_map.update(cap_function_map)
|
|
575
|
+
events.update(cap_events)
|
|
576
|
+
event_map.update(cap_event_map)
|
|
513
577
|
|
|
514
578
|
asyncapi_spec = {
|
|
515
579
|
'asyncapi': ASYNCAPI_VERSION,
|
|
516
580
|
'x-intersect-version': version_string,
|
|
517
581
|
'info': {
|
|
518
|
-
'title':
|
|
582
|
+
'title': service_name.hierarchy_string('.'),
|
|
583
|
+
'description': 'INTERSECT schema',
|
|
519
584
|
'version': '0.0.0', # NOTE: this will be modified by INTERSECT CORE, users do not manage their schema versions
|
|
520
585
|
},
|
|
521
586
|
# applies to how an incoming message payload will be parsed.
|
|
522
587
|
# can be changed per channel
|
|
523
588
|
'defaultContentType': 'application/json',
|
|
524
|
-
'
|
|
589
|
+
'capabilities': capability_schemas,
|
|
525
590
|
'events': events,
|
|
591
|
+
'status': status_function_schema if status_function_schema else {'type': 'null'},
|
|
526
592
|
'components': {
|
|
527
|
-
'schemas':
|
|
593
|
+
'schemas': shared_schemas,
|
|
528
594
|
'messageTraits': {
|
|
529
595
|
# this is where we can define our message headers
|
|
530
596
|
'commonHeaders': {
|
|
@@ -540,19 +606,12 @@ def get_schema_and_functions_from_capability_implementation(
|
|
|
540
606
|
},
|
|
541
607
|
},
|
|
542
608
|
}
|
|
543
|
-
if capability_type.__doc__:
|
|
544
|
-
asyncapi_spec['info']['description'] = inspect.cleandoc(capability_type.__doc__) # type: ignore[index]
|
|
545
609
|
|
|
546
|
-
|
|
547
|
-
asyncapi_spec
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
"url": "https://example.com", # REQUIRED
|
|
555
|
-
},
|
|
556
|
-
"""
|
|
557
|
-
|
|
558
|
-
return asyncapi_spec, function_map, event_map, status_fn_name, status_type_adapter
|
|
610
|
+
return (
|
|
611
|
+
asyncapi_spec,
|
|
612
|
+
function_map,
|
|
613
|
+
event_map,
|
|
614
|
+
status_function_cap,
|
|
615
|
+
status_function_name,
|
|
616
|
+
status_function_adapter,
|
|
617
|
+
)
|