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.
Files changed (77) hide show
  1. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/PKG-INFO +5 -1
  2. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/pyproject.toml +14 -2
  3. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/__init__.py +13 -5
  4. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/control_plane_manager.py +1 -1
  5. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/function_metadata.py +4 -0
  6. intersect_sdk-0.8.0/src/intersect_sdk/_internal/interfaces.py +51 -0
  7. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/messages/userspace.py +19 -5
  8. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/schema.py +110 -51
  9. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/capability/base.py +60 -6
  10. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/client.py +12 -57
  11. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/client_callback_definitions.py +8 -60
  12. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/config/shared.py +1 -0
  13. intersect_sdk-0.8.0/src/intersect_sdk/schema.py +70 -0
  14. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/service.py +406 -39
  15. intersect_sdk-0.8.0/src/intersect_sdk/service_callback_definitions.py +24 -0
  16. intersect_sdk-0.8.0/src/intersect_sdk/shared_callback_definitions.py +67 -0
  17. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/version.py +1 -1
  18. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/e2e/test_examples.py +18 -7
  19. intersect_sdk-0.8.0/tests/fixtures/example_schema.json +1067 -0
  20. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/fixtures/example_schema.py +2 -0
  21. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/integration/test_return_type_mismatch.py +7 -4
  22. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/integration/test_service.py +32 -30
  23. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_base_capability_implementation.py +76 -2
  24. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_invalid_schema_runtime.py +4 -2
  25. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_schema_invalids.py +235 -52
  26. intersect_sdk-0.8.0/tests/unit/test_schema_valid.py +91 -0
  27. intersect_sdk-0.6.4/src/intersect_sdk/_internal/interfaces.py +0 -20
  28. intersect_sdk-0.6.4/src/intersect_sdk/schema.py +0 -77
  29. intersect_sdk-0.6.4/tests/fixtures/example_schema.json +0 -1062
  30. intersect_sdk-0.6.4/tests/unit/test_schema_valid.py +0 -79
  31. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/LICENSE +0 -0
  32. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/README.md +0 -0
  33. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/__init__.py +0 -0
  34. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/constants.py +0 -0
  35. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/__init__.py +0 -0
  36. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/brokers/__init__.py +0 -0
  37. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/brokers/amqp_client.py +0 -0
  38. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/brokers/broker_client.py +0 -0
  39. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/brokers/mqtt_client.py +0 -0
  40. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/discovery_service.py +0 -0
  41. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/control_plane/topic_handler.py +0 -0
  42. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/data_plane/__init__.py +0 -0
  43. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/data_plane/data_plane_manager.py +0 -0
  44. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/data_plane/minio_utils.py +0 -0
  45. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/event_metadata.py +0 -0
  46. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/exceptions.py +0 -0
  47. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/logger.py +0 -0
  48. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/messages/__init__.py +0 -0
  49. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/messages/event.py +0 -0
  50. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/messages/lifecycle.py +0 -0
  51. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/multi_flag_thread_event.py +0 -0
  52. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/pydantic_schema_generator.py +0 -0
  53. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/stoppable_thread.py +0 -0
  54. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/utils.py +0 -0
  55. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/version.py +0 -0
  56. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/_internal/version_resolver.py +0 -0
  57. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/app_lifecycle.py +0 -0
  58. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/capability/__init__.py +0 -0
  59. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/config/__init__.py +0 -0
  60. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/config/client.py +0 -0
  61. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/config/service.py +0 -0
  62. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/constants.py +0 -0
  63. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/core_definitions.py +0 -0
  64. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/py.typed +0 -0
  65. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/src/intersect_sdk/service_definitions.py +0 -0
  66. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/__init__.py +0 -0
  67. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/conftest.py +0 -0
  68. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/e2e/__init__.py +0 -0
  69. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/fixtures/__init__.py +0 -0
  70. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/fixtures/return_type_mismatch.py +0 -0
  71. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/integration/__init__.py +0 -0
  72. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/__init__.py +0 -0
  73. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_annotations.py +0 -0
  74. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_config.py +0 -0
  75. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_lifecycle_message.py +0 -0
  76. {intersect_sdk-0.6.4 → intersect_sdk-0.8.0}/tests/unit/test_userspace_message.py +0 -0
  77. {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.6.4
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.6.4"
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>=0.4.2",
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 get_schema_from_capability_implementation
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
- 'get_schema_from_capability_implementation',
52
+ 'get_schema_from_capability_implementations',
47
53
  'IntersectService',
48
54
  'IntersectClient',
49
55
  'IntersectClientCallback',
50
- 'IntersectClientMessageParams',
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 ValueError(msg)
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
@@ -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
+ ...
@@ -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 IntersectDataHandler, IntersectMimeType
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: FA100 (Pydantic uses runtime annotations)
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=uuid.uuid4(),
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
- capability_name: str
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
- capability_name, status_fn_name, status_fn, min_params = status_info
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 '{capability_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)."
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 '{capability_name}', capability status function '{status_fn_name}' should have a valid return annotation."
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 '{capability_name}', return annotation '{status_signature.return_annotation}' on function '{status_fn_name}' is invalid.\n{e}"
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
- capability_name: str,
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 '{capability_name}', event key '{event_key}' on function '{function_name}' was previously defined differently. \n{diff_str}\n"
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 '{capability_name}', function '{function_name}' should not set data_handler as {event_definition.data_handler} unless an instance is configured in IntersectConfig.data_stores ."
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 '{capability_name}', event key '{event_key}' on function '{function_name}' has an invalid value in the events mapping.\n{e}"
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 capability_name, name, method, min_params in response_funcs:
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 '{capability_name}', function '{name}' should not set response_data_type as {data_handler} unless an instance is configured in IntersectConfig.data_stores ."
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 '{capability_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)."
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 '{capability_name}', parameter '{parameter.name}' type annotation on function '{name}' missing. {SCHEMA_HELP_MSG}"
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 '{capability_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')."
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 '{capability_name}', parameter '{parameter.name}' type annotation '{annotation}' on function '{name}' is invalid\n{e}"
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 '{capability_name}', return type annotation on function '{name}' missing. {SCHEMA_HELP_MSG}"
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 '{capability_name}', return annotation '{return_annotation}' on function '{name}' is invalid.\n{e}"
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[name] = FunctionMetadata(
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
- capability_name,
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 capability_name, name, method, _ in event_funcs:
467
+ for class_name, name, method, _ in event_funcs:
459
468
  _add_events(
460
- capability_name,
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
- function_map[status_fn_name] = FunctionMetadata(
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 get_schema_and_functions_from_capability_implementation(
491
- capability_type: type[IntersectBaseCapabilityImplementation],
492
- capability_name: HierarchyConfig,
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
- schemas,
507
- (status_fn_name, status_schema, status_type_adapter),
508
- channels,
509
- function_map,
510
- events,
511
- event_map,
512
- ) = _introspection_baseline(capability_type, excluded_data_handlers)
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': capability_name.hierarchy_string('.'),
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
- 'channels': channels,
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': 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
- if status_schema:
547
- asyncapi_spec['status'] = status_schema
548
-
549
- """
550
- TODO - might want to include these fields
551
- "securitySchemes": {},
552
- "operationTraits": {},
553
- "externalDocumentation": {
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
+ )