intersect-sdk 0.6.3__tar.gz → 0.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.
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/PKG-INFO +2 -3
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/pyproject.toml +4 -4
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/__init__.py +9 -3
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/control_plane_manager.py +1 -1
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/function_metadata.py +4 -0
- intersect_sdk-0.7.0/src/intersect_sdk/_internal/interfaces.py +49 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/messages/userspace.py +19 -5
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/schema.py +82 -41
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/capability/base.py +56 -4
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/client.py +12 -12
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/client_callback_definitions.py +7 -43
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/schema.py +7 -6
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/service.py +355 -39
- intersect_sdk-0.7.0/src/intersect_sdk/service_callback_definitions.py +16 -0
- intersect_sdk-0.7.0/src/intersect_sdk/shared_callback_definitions.py +67 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/version.py +1 -1
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/e2e/test_examples.py +16 -9
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/fixtures/example_schema.json +2 -2
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/fixtures/example_schema.py +1 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/integration/test_return_type_mismatch.py +6 -4
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/integration/test_service.py +32 -30
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/unit/test_base_capability_implementation.py +68 -2
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/unit/test_invalid_schema_runtime.py +1 -1
- intersect_sdk-0.7.0/tests/unit/test_schema_valid.py +93 -0
- intersect_sdk-0.6.3/src/intersect_sdk/_internal/interfaces.py +0 -20
- intersect_sdk-0.6.3/tests/unit/test_schema_valid.py +0 -79
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/LICENSE +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/README.md +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/__init__.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/constants.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/__init__.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/brokers/__init__.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/brokers/amqp_client.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/brokers/broker_client.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/brokers/mqtt_client.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/discovery_service.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/topic_handler.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/data_plane/__init__.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/data_plane/data_plane_manager.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/data_plane/minio_utils.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/event_metadata.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/exceptions.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/logger.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/messages/__init__.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/messages/event.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/messages/lifecycle.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/multi_flag_thread_event.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/pydantic_schema_generator.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/stoppable_thread.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/utils.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/version.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/version_resolver.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/app_lifecycle.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/capability/__init__.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/config/__init__.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/config/client.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/config/service.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/config/shared.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/constants.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/core_definitions.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/py.typed +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/src/intersect_sdk/service_definitions.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/__init__.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/conftest.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/e2e/__init__.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/fixtures/__init__.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/fixtures/return_type_mismatch.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/integration/__init__.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/unit/__init__.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/unit/test_annotations.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/unit/test_config.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/unit/test_lifecycle_message.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/unit/test_schema_invalids.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.0}/tests/unit/test_userspace_message.py +0 -0
- {intersect_sdk-0.6.3 → intersect_sdk-0.7.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.7.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>
|
|
@@ -16,12 +16,11 @@ Requires-Dist: retrying<2.0.0,>=1.3.4
|
|
|
16
16
|
Requires-Dist: paho-mqtt<2.0.0,>=1.6.1
|
|
17
17
|
Requires-Dist: minio>=7.2.3
|
|
18
18
|
Requires-Dist: jsonschema[format-nongpl]>=4.21.1
|
|
19
|
+
Requires-Dist: eval-type-backport>=0.1.3; python_version < "3.10"
|
|
19
20
|
Requires-Dist: pika<2.0.0,>=1.3.2; extra == "amqp"
|
|
20
|
-
Requires-Dist: eval-type-backport>=0.1.3; extra == "py38"
|
|
21
21
|
Requires-Dist: sphinx>=5.3.0; extra == "docs"
|
|
22
22
|
Requires-Dist: furo>=2023.3.27; extra == "docs"
|
|
23
23
|
Provides-Extra: amqp
|
|
24
|
-
Provides-Extra: py38
|
|
25
24
|
Provides-Extra: docs
|
|
26
25
|
Description-Content-Type: text/markdown
|
|
27
26
|
|
|
@@ -33,8 +33,9 @@ dependencies = [
|
|
|
33
33
|
"paho-mqtt>=1.6.1,<2.0.0",
|
|
34
34
|
"minio>=7.2.3",
|
|
35
35
|
"jsonschema[format-nongpl]>=4.21.1",
|
|
36
|
+
"eval-type-backport>=0.1.3;python_version<'3.10'",
|
|
36
37
|
]
|
|
37
|
-
version = "0.
|
|
38
|
+
version = "0.7.0"
|
|
38
39
|
|
|
39
40
|
[project.license]
|
|
40
41
|
text = "BSD-3-Clause"
|
|
@@ -43,9 +44,6 @@ text = "BSD-3-Clause"
|
|
|
43
44
|
amqp = [
|
|
44
45
|
"pika>=1.3.2,<2.0.0",
|
|
45
46
|
]
|
|
46
|
-
py38 = [
|
|
47
|
-
"eval-type-backport>=0.1.3",
|
|
48
|
-
]
|
|
49
47
|
docs = [
|
|
50
48
|
"sphinx>=5.3.0",
|
|
51
49
|
"furo>=2023.3.27",
|
|
@@ -68,6 +66,7 @@ test = [
|
|
|
68
66
|
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"
|
|
69
67
|
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"
|
|
70
68
|
test-unit = "pytest tests/unit --cov=src/intersect_sdk/"
|
|
69
|
+
test-e2e = "pytest tests/e2e --cov=src/intersect_sdk/"
|
|
71
70
|
lint-format = "ruff format"
|
|
72
71
|
lint-ruff = "ruff check --fix"
|
|
73
72
|
lint-mypy = "mypy src/intersect_sdk/"
|
|
@@ -164,6 +163,7 @@ max-complexity = 20
|
|
|
164
163
|
max-args = 10
|
|
165
164
|
max-branches = 20
|
|
166
165
|
max-returns = 10
|
|
166
|
+
max-statements = 75
|
|
167
167
|
|
|
168
168
|
[tool.ruff.lint.extend-per-file-ignores]
|
|
169
169
|
"__init__.py" = [
|
|
@@ -13,9 +13,7 @@ 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
|
|
@@ -28,12 +26,19 @@ from .config.shared import (
|
|
|
28
26
|
from .core_definitions import IntersectDataHandler, IntersectMimeType
|
|
29
27
|
from .schema import get_schema_from_capability_implementation
|
|
30
28
|
from .service import IntersectService
|
|
29
|
+
from .service_callback_definitions import (
|
|
30
|
+
INTERSECT_SERVICE_RESPONSE_CALLBACK_TYPE,
|
|
31
|
+
)
|
|
31
32
|
from .service_definitions import (
|
|
32
33
|
IntersectEventDefinition,
|
|
33
34
|
intersect_event,
|
|
34
35
|
intersect_message,
|
|
35
36
|
intersect_status,
|
|
36
37
|
)
|
|
38
|
+
from .shared_callback_definitions import (
|
|
39
|
+
INTERSECT_JSON_VALUE,
|
|
40
|
+
IntersectDirectMessageParams,
|
|
41
|
+
)
|
|
37
42
|
from .version import __version__, version_info, version_string
|
|
38
43
|
|
|
39
44
|
__all__ = [
|
|
@@ -47,10 +52,11 @@ __all__ = [
|
|
|
47
52
|
'IntersectService',
|
|
48
53
|
'IntersectClient',
|
|
49
54
|
'IntersectClientCallback',
|
|
50
|
-
'
|
|
55
|
+
'IntersectDirectMessageParams',
|
|
51
56
|
'INTERSECT_CLIENT_RESPONSE_CALLBACK_TYPE',
|
|
52
57
|
'INTERSECT_CLIENT_EVENT_CALLBACK_TYPE',
|
|
53
58
|
'INTERSECT_JSON_VALUE',
|
|
59
|
+
'INTERSECT_SERVICE_RESPONSE_CALLBACK_TYPE',
|
|
54
60
|
'IntersectBaseCapabilityImplementation',
|
|
55
61
|
'default_intersect_lifecycle_loop',
|
|
56
62
|
'IntersectClientConfig',
|
|
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
|
|
13
13
|
from ...config.shared import ControlPlaneConfig
|
|
14
14
|
from .brokers.broker_client import BrokerClient
|
|
15
15
|
|
|
16
|
-
GENERIC_MESSAGE_SERIALIZER = TypeAdapter(Any)
|
|
16
|
+
GENERIC_MESSAGE_SERIALIZER: TypeAdapter[Any] = TypeAdapter(Any)
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def serialize_message(message: Any) -> bytes:
|
{intersect_sdk-0.6.3 → intersect_sdk-0.7.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,49 @@
|
|
|
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
|
+
) -> UUID:
|
|
40
|
+
"""Observed entity (capabilitiy) tells observer (i.e. service) to send an external request.
|
|
41
|
+
|
|
42
|
+
Params:
|
|
43
|
+
- request: the request we want to send out, encapsulated as an IntersectClientMessageParams object
|
|
44
|
+
- response_handler: optional callback for how we want to handle the response from this request.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
- generated RequestID associated with your request
|
|
48
|
+
"""
|
|
49
|
+
...
|
{intersect_sdk-0.6.3 → intersect_sdk-0.7.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,
|
|
@@ -59,7 +59,7 @@ For a complete reference, https://docs.pydantic.dev/latest/concepts/conversion_t
|
|
|
59
59
|
class _FunctionAnalysisResult(NamedTuple):
|
|
60
60
|
"""private class generated from static analysis of function."""
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
class_name: str
|
|
63
63
|
method_name: str
|
|
64
64
|
method: Callable[[Any], Any]
|
|
65
65
|
"""raw method is for inspecting attributes"""
|
|
@@ -218,18 +218,18 @@ def _status_fn_schema(
|
|
|
218
218
|
- The status function's schema
|
|
219
219
|
- The TypeAdapter to use for serializing outgoing responses
|
|
220
220
|
"""
|
|
221
|
-
|
|
221
|
+
class_name, status_fn_name, status_fn, min_params = status_info
|
|
222
222
|
status_signature = inspect.signature(status_fn)
|
|
223
223
|
method_params = tuple(status_signature.parameters.values())
|
|
224
224
|
if len(method_params) != min_params or any(
|
|
225
225
|
p.kind not in (p.POSITIONAL_OR_KEYWORD, p.POSITIONAL_ONLY) for p in method_params
|
|
226
226
|
):
|
|
227
227
|
die(
|
|
228
|
-
f"On capability '{
|
|
228
|
+
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
229
|
)
|
|
230
230
|
if status_signature.return_annotation is inspect.Signature.empty:
|
|
231
231
|
die(
|
|
232
|
-
f"On capability '{
|
|
232
|
+
f"On capability '{class_name}', capability status function '{status_fn_name}' should have a valid return annotation."
|
|
233
233
|
)
|
|
234
234
|
try:
|
|
235
235
|
status_adapter = TypeAdapter(status_signature.return_annotation)
|
|
@@ -246,12 +246,12 @@ def _status_fn_schema(
|
|
|
246
246
|
)
|
|
247
247
|
except PydanticUserError as e:
|
|
248
248
|
die(
|
|
249
|
-
f"On capability '{
|
|
249
|
+
f"On capability '{class_name}', return annotation '{status_signature.return_annotation}' on function '{status_fn_name}' is invalid.\n{e}"
|
|
250
250
|
)
|
|
251
251
|
|
|
252
252
|
|
|
253
253
|
def _add_events(
|
|
254
|
-
|
|
254
|
+
class_name: str,
|
|
255
255
|
function_name: str,
|
|
256
256
|
schemas: dict[str, Any],
|
|
257
257
|
event_schemas: dict[str, Any],
|
|
@@ -272,13 +272,13 @@ def _add_events(
|
|
|
272
272
|
for d in differences_from_cache
|
|
273
273
|
)
|
|
274
274
|
die(
|
|
275
|
-
f"On capability '{
|
|
275
|
+
f"On capability '{class_name}', event key '{event_key}' on function '{function_name}' was previously defined differently. \n{diff_str}\n"
|
|
276
276
|
)
|
|
277
277
|
metadata_value.operations.add(function_name)
|
|
278
278
|
else:
|
|
279
279
|
if event_definition.data_handler in excluded_data_handlers:
|
|
280
280
|
die(
|
|
281
|
-
f"On capability '{
|
|
281
|
+
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
282
|
)
|
|
283
283
|
try:
|
|
284
284
|
event_adapter: TypeAdapter[Any] = TypeAdapter(event_definition.event_type)
|
|
@@ -297,12 +297,12 @@ def _add_events(
|
|
|
297
297
|
)
|
|
298
298
|
except PydanticUserError as e:
|
|
299
299
|
die(
|
|
300
|
-
f"On capability '{
|
|
300
|
+
f"On capability '{class_name}', event key '{event_key}' on function '{function_name}' has an invalid value in the events mapping.\n{e}"
|
|
301
301
|
)
|
|
302
302
|
|
|
303
303
|
|
|
304
304
|
def _introspection_baseline(
|
|
305
|
-
capability:
|
|
305
|
+
capability: IntersectBaseCapabilityImplementation,
|
|
306
306
|
excluded_data_handlers: set[IntersectDataHandler],
|
|
307
307
|
) -> tuple[
|
|
308
308
|
dict[Any, Any], # $defs for schemas (common)
|
|
@@ -332,10 +332,13 @@ def _introspection_baseline(
|
|
|
332
332
|
function_map = {}
|
|
333
333
|
event_metadatas: dict[str, EventMetadata] = {}
|
|
334
334
|
|
|
335
|
-
|
|
335
|
+
cap_name = capability.capability_name
|
|
336
|
+
status_func, response_funcs, event_funcs = _get_functions(type(capability))
|
|
336
337
|
|
|
337
338
|
# parse functions
|
|
338
|
-
for
|
|
339
|
+
for class_name, name, method, min_params in response_funcs:
|
|
340
|
+
public_name = f'{cap_name}.{name}'
|
|
341
|
+
|
|
339
342
|
# TODO - I'm placing this here for now because we'll eventually want to capture data plane and broker configs in the schema.
|
|
340
343
|
# (It's possible we may want to separate the backing service schema from the application logic, but it's unlikely.)
|
|
341
344
|
# At the moment, we're just validating that users can support their response_data_handler property.
|
|
@@ -343,7 +346,7 @@ def _introspection_baseline(
|
|
|
343
346
|
data_handler = getattr(method, RESPONSE_DATA)
|
|
344
347
|
if data_handler in excluded_data_handlers:
|
|
345
348
|
die(
|
|
346
|
-
f"On capability '{
|
|
349
|
+
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
350
|
)
|
|
348
351
|
|
|
349
352
|
docstring = inspect.cleandoc(method.__doc__) if method.__doc__ else None
|
|
@@ -358,7 +361,7 @@ def _introspection_baseline(
|
|
|
358
361
|
)
|
|
359
362
|
):
|
|
360
363
|
die(
|
|
361
|
-
f"On capability '{
|
|
364
|
+
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
365
|
)
|
|
363
366
|
|
|
364
367
|
# The schema format should be hard-coded and determined based on how Pydantic parses the schema.
|
|
@@ -390,7 +393,7 @@ def _introspection_baseline(
|
|
|
390
393
|
# Pydantic BaseModels require annotations even if using a default value, so we'll remain consistent.
|
|
391
394
|
if annotation is inspect.Parameter.empty:
|
|
392
395
|
die(
|
|
393
|
-
f"On capability '{
|
|
396
|
+
f"On capability '{class_name}', parameter '{parameter.name}' type annotation on function '{name}' missing. {SCHEMA_HELP_MSG}"
|
|
394
397
|
)
|
|
395
398
|
# rationale for disallowing function default values:
|
|
396
399
|
# https://docs.pydantic.dev/latest/concepts/validation_decorator/#using-field-to-describe-function-arguments
|
|
@@ -398,7 +401,7 @@ def _introspection_baseline(
|
|
|
398
401
|
# also makes TypeAdapters considerably easier to construct
|
|
399
402
|
if parameter.default is not inspect.Parameter.empty:
|
|
400
403
|
die(
|
|
401
|
-
f"On capability '{
|
|
404
|
+
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
405
|
)
|
|
403
406
|
try:
|
|
404
407
|
function_cache_request_adapter = TypeAdapter(annotation)
|
|
@@ -410,7 +413,7 @@ def _introspection_baseline(
|
|
|
410
413
|
)
|
|
411
414
|
except PydanticUserError as e:
|
|
412
415
|
die(
|
|
413
|
-
f"On capability '{
|
|
416
|
+
f"On capability '{class_name}', parameter '{parameter.name}' type annotation '{annotation}' on function '{name}' is invalid\n{e}"
|
|
414
417
|
)
|
|
415
418
|
|
|
416
419
|
else:
|
|
@@ -419,7 +422,7 @@ def _introspection_baseline(
|
|
|
419
422
|
# this block handles response parameters
|
|
420
423
|
if return_annotation is inspect.Signature.empty:
|
|
421
424
|
die(
|
|
422
|
-
f"On capability '{
|
|
425
|
+
f"On capability '{class_name}', return type annotation on function '{name}' missing. {SCHEMA_HELP_MSG}"
|
|
423
426
|
)
|
|
424
427
|
try:
|
|
425
428
|
function_cache_response_adapter = TypeAdapter(return_annotation)
|
|
@@ -431,11 +434,12 @@ def _introspection_baseline(
|
|
|
431
434
|
)
|
|
432
435
|
except PydanticUserError as e:
|
|
433
436
|
die(
|
|
434
|
-
f"On capability '{
|
|
437
|
+
f"On capability '{class_name}', return annotation '{return_annotation}' on function '{name}' is invalid.\n{e}"
|
|
435
438
|
)
|
|
436
439
|
|
|
437
440
|
# final function mapping
|
|
438
|
-
function_map[
|
|
441
|
+
function_map[public_name] = FunctionMetadata(
|
|
442
|
+
type(capability),
|
|
439
443
|
method,
|
|
440
444
|
function_cache_request_adapter,
|
|
441
445
|
function_cache_response_adapter,
|
|
@@ -444,7 +448,7 @@ def _introspection_baseline(
|
|
|
444
448
|
# this block handles events associated with intersect_messages (implies command pattern)
|
|
445
449
|
function_events: dict[str, IntersectEventDefinition] = getattr(method, EVENT_ATTR_KEY)
|
|
446
450
|
_add_events(
|
|
447
|
-
|
|
451
|
+
class_name,
|
|
448
452
|
name,
|
|
449
453
|
schemas,
|
|
450
454
|
event_schemas,
|
|
@@ -455,9 +459,9 @@ def _introspection_baseline(
|
|
|
455
459
|
channels[name]['events'] = list(function_events.keys())
|
|
456
460
|
|
|
457
461
|
# parse global schemas
|
|
458
|
-
for
|
|
462
|
+
for class_name, name, method, _ in event_funcs:
|
|
459
463
|
_add_events(
|
|
460
|
-
|
|
464
|
+
class_name,
|
|
461
465
|
name,
|
|
462
466
|
schemas,
|
|
463
467
|
event_schemas,
|
|
@@ -471,7 +475,9 @@ def _introspection_baseline(
|
|
|
471
475
|
)
|
|
472
476
|
# this conditional allows for the status function to also be called like a message
|
|
473
477
|
if status_fn_type_adapter and status_fn and status_fn_name:
|
|
474
|
-
|
|
478
|
+
public_status_name = f'{cap_name}.{status_fn_name}'
|
|
479
|
+
function_map[public_status_name] = FunctionMetadata(
|
|
480
|
+
type(capability),
|
|
475
481
|
status_fn,
|
|
476
482
|
None,
|
|
477
483
|
status_fn_type_adapter,
|
|
@@ -487,14 +493,15 @@ def _introspection_baseline(
|
|
|
487
493
|
)
|
|
488
494
|
|
|
489
495
|
|
|
490
|
-
def
|
|
491
|
-
|
|
492
|
-
|
|
496
|
+
def get_schema_and_functions_from_capability_implementations(
|
|
497
|
+
capabilities: list[IntersectBaseCapabilityImplementation],
|
|
498
|
+
service_name: HierarchyConfig,
|
|
493
499
|
excluded_data_handlers: set[IntersectDataHandler],
|
|
494
500
|
) -> tuple[
|
|
495
501
|
dict[str, Any],
|
|
496
502
|
dict[str, FunctionMetadata],
|
|
497
503
|
dict[str, EventMetadata],
|
|
504
|
+
IntersectBaseCapabilityImplementation | None,
|
|
498
505
|
str | None,
|
|
499
506
|
TypeAdapter[Any] | None,
|
|
500
507
|
]:
|
|
@@ -502,20 +509,46 @@ def get_schema_and_functions_from_capability_implementation(
|
|
|
502
509
|
|
|
503
510
|
In-depth introspection is handled later on.
|
|
504
511
|
"""
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
512
|
+
capability_type_docs: str = ''
|
|
513
|
+
status_function_cap: IntersectBaseCapabilityImplementation | None = None
|
|
514
|
+
status_function_name: str | None = None
|
|
515
|
+
status_function_schema: dict[str, Any] | None = None
|
|
516
|
+
status_function_adapter: TypeAdapter[Any] | None = None
|
|
517
|
+
schemas: dict[Any, Any] = {}
|
|
518
|
+
channels: dict[str, dict[str, dict[str, Any]]] = {} # endpoint schemas
|
|
519
|
+
function_map: dict[str, FunctionMetadata] = {} # endpoint functionality
|
|
520
|
+
events: dict[str, Any] = {} # event schemas
|
|
521
|
+
event_map: dict[str, EventMetadata] = {} # event functionality
|
|
522
|
+
for capability in capabilities:
|
|
523
|
+
capability_type: type[IntersectBaseCapabilityImplementation] = type(capability)
|
|
524
|
+
if capability_type.__doc__:
|
|
525
|
+
capability_type_docs += inspect.cleandoc(capability_type.__doc__) + '\n'
|
|
526
|
+
(
|
|
527
|
+
cap_schemas,
|
|
528
|
+
(cap_status_fn_name, cap_status_schema, cap_status_type_adapter),
|
|
529
|
+
cap_channels,
|
|
530
|
+
cap_function_map,
|
|
531
|
+
cap_events,
|
|
532
|
+
cap_event_map,
|
|
533
|
+
) = _introspection_baseline(capability, excluded_data_handlers)
|
|
534
|
+
|
|
535
|
+
if cap_status_fn_name and cap_status_schema and cap_status_type_adapter:
|
|
536
|
+
status_function_cap = capability
|
|
537
|
+
status_function_name = cap_status_fn_name
|
|
538
|
+
status_function_schema = cap_status_schema
|
|
539
|
+
status_function_adapter = cap_status_type_adapter
|
|
540
|
+
|
|
541
|
+
schemas.update(cap_schemas)
|
|
542
|
+
channels.update(cap_channels)
|
|
543
|
+
function_map.update(cap_function_map)
|
|
544
|
+
events.update(cap_events)
|
|
545
|
+
event_map.update(cap_event_map)
|
|
513
546
|
|
|
514
547
|
asyncapi_spec = {
|
|
515
548
|
'asyncapi': ASYNCAPI_VERSION,
|
|
516
549
|
'x-intersect-version': version_string,
|
|
517
550
|
'info': {
|
|
518
|
-
'title':
|
|
551
|
+
'title': service_name.hierarchy_string('.'),
|
|
519
552
|
'version': '0.0.0', # NOTE: this will be modified by INTERSECT CORE, users do not manage their schema versions
|
|
520
553
|
},
|
|
521
554
|
# applies to how an incoming message payload will be parsed.
|
|
@@ -540,11 +573,12 @@ def get_schema_and_functions_from_capability_implementation(
|
|
|
540
573
|
},
|
|
541
574
|
},
|
|
542
575
|
}
|
|
543
|
-
if capability_type.__doc__:
|
|
544
|
-
asyncapi_spec['info']['description'] = inspect.cleandoc(capability_type.__doc__) # type: ignore[index]
|
|
545
576
|
|
|
546
|
-
if
|
|
547
|
-
asyncapi_spec['
|
|
577
|
+
if capability_type_docs != '':
|
|
578
|
+
asyncapi_spec['info']['description'] = capability_type_docs # type: ignore[index]
|
|
579
|
+
|
|
580
|
+
if status_function_schema:
|
|
581
|
+
asyncapi_spec['status'] = status_function_schema
|
|
548
582
|
|
|
549
583
|
"""
|
|
550
584
|
TODO - might want to include these fields
|
|
@@ -555,4 +589,11 @@ def get_schema_and_functions_from_capability_implementation(
|
|
|
555
589
|
},
|
|
556
590
|
"""
|
|
557
591
|
|
|
558
|
-
return
|
|
592
|
+
return (
|
|
593
|
+
asyncapi_spec,
|
|
594
|
+
function_map,
|
|
595
|
+
event_map,
|
|
596
|
+
status_function_cap,
|
|
597
|
+
status_function_name,
|
|
598
|
+
status_function_adapter,
|
|
599
|
+
)
|
|
@@ -11,21 +11,34 @@ from .._internal.constants import BASE_EVENT_ATTR, BASE_RESPONSE_ATTR, BASE_STAT
|
|
|
11
11
|
from .._internal.logger import logger
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
|
+
from uuid import UUID
|
|
15
|
+
|
|
14
16
|
from .._internal.interfaces import IntersectEventObserver
|
|
17
|
+
from ..service_callback_definitions import (
|
|
18
|
+
INTERSECT_SERVICE_RESPONSE_CALLBACK_TYPE,
|
|
19
|
+
)
|
|
20
|
+
from ..shared_callback_definitions import (
|
|
21
|
+
IntersectDirectMessageParams,
|
|
22
|
+
)
|
|
15
23
|
|
|
16
24
|
|
|
17
25
|
class IntersectBaseCapabilityImplementation:
|
|
18
26
|
"""Base class for all capabilities.
|
|
19
27
|
|
|
20
28
|
EVERY capability implementation will need to extend this class. Additionally, if you redefine the constructor,
|
|
21
|
-
you MUST call super.__init__() .
|
|
29
|
+
you MUST call `super.__init__()` .
|
|
22
30
|
"""
|
|
23
31
|
|
|
24
32
|
def __init__(self) -> None:
|
|
25
33
|
"""This constructor just sets up observers.
|
|
26
34
|
|
|
27
|
-
NOTE: If you write your own constructor, you MUST call super.__init__() inside of it. The Service will throw an error if you don't.
|
|
35
|
+
NOTE: If you write your own constructor, you MUST call `super.__init__()` inside of it. The Service will throw an error if you don't.
|
|
36
|
+
"""
|
|
37
|
+
self._capability_name: str = 'InvalidCapability'
|
|
38
|
+
"""
|
|
39
|
+
The advertised name for the capability, as opposed to the implementation class name
|
|
28
40
|
"""
|
|
41
|
+
|
|
29
42
|
self.__intersect_sdk_observers__: list[IntersectEventObserver] = []
|
|
30
43
|
"""
|
|
31
44
|
INTERNAL USE ONLY.
|
|
@@ -34,16 +47,31 @@ class IntersectBaseCapabilityImplementation:
|
|
|
34
47
|
"""
|
|
35
48
|
|
|
36
49
|
def __init_subclass__(cls) -> None:
|
|
37
|
-
"""This prevents users from overriding a few key functions.
|
|
50
|
+
"""This prevents users from overriding a few key functions.
|
|
51
|
+
|
|
52
|
+
General rule of thumb is that any function which starts with `intersect_sdk_` is a protected namespace for defining
|
|
53
|
+
the INTERSECT-SDK public API between a capability and its observers.
|
|
54
|
+
"""
|
|
38
55
|
if (
|
|
39
56
|
cls._intersect_sdk_register_observer
|
|
40
57
|
is not IntersectBaseCapabilityImplementation._intersect_sdk_register_observer
|
|
41
58
|
or cls.intersect_sdk_emit_event
|
|
42
59
|
is not IntersectBaseCapabilityImplementation.intersect_sdk_emit_event
|
|
60
|
+
or cls.intersect_sdk_call_service
|
|
61
|
+
is not IntersectBaseCapabilityImplementation.intersect_sdk_call_service
|
|
43
62
|
):
|
|
44
|
-
msg = f"{cls.__name__}:
|
|
63
|
+
msg = f"{cls.__name__}: Attempted to override a reserved INTERSECT-SDK function (don't start your function names with '_intersect_sdk_' or 'intersect_sdk_')"
|
|
45
64
|
raise RuntimeError(msg)
|
|
46
65
|
|
|
66
|
+
@property
|
|
67
|
+
def capability_name(self) -> str:
|
|
68
|
+
"""The advertised name for the capability provided by this implementation."""
|
|
69
|
+
return self._capability_name
|
|
70
|
+
|
|
71
|
+
@capability_name.setter
|
|
72
|
+
def capability_name(self, cname: str) -> None:
|
|
73
|
+
self._capability_name = cname
|
|
74
|
+
|
|
47
75
|
@final
|
|
48
76
|
def _intersect_sdk_register_observer(self, observer: IntersectEventObserver) -> None:
|
|
49
77
|
"""INTERNAL USE ONLY."""
|
|
@@ -99,3 +127,27 @@ class IntersectBaseCapabilityImplementation:
|
|
|
99
127
|
return
|
|
100
128
|
for observer in self.__intersect_sdk_observers__:
|
|
101
129
|
observer._on_observe_event(event_name, event_value, annotated_operation) # noqa: SLF001 (private for application devs, NOT for base implementation)
|
|
130
|
+
|
|
131
|
+
@final
|
|
132
|
+
def intersect_sdk_call_service(
|
|
133
|
+
self,
|
|
134
|
+
request: IntersectDirectMessageParams,
|
|
135
|
+
response_handler: INTERSECT_SERVICE_RESPONSE_CALLBACK_TYPE | None = None,
|
|
136
|
+
) -> list[UUID]:
|
|
137
|
+
"""Create an external request that we'll send to a different Service.
|
|
138
|
+
|
|
139
|
+
Params:
|
|
140
|
+
- request: the request we want to send out, encapsulated as an IntersectClientMessageParams object
|
|
141
|
+
- response_handler: optional callback for how we want to handle the response from this request.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
- list of generated RequestIDs associated with your request. Note that for almost all use cases,
|
|
145
|
+
this list will have only one associated RequestID.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
- pydantic.ValidationError - if the request parameter isn't valid
|
|
149
|
+
"""
|
|
150
|
+
return [
|
|
151
|
+
observer.create_external_request(request, response_handler)
|
|
152
|
+
for observer in self.__intersect_sdk_observers__
|
|
153
|
+
]
|