intersect-sdk 0.6.4__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.
Files changed (75) hide show
  1. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/PKG-INFO +1 -1
  2. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/pyproject.toml +3 -1
  3. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/__init__.py +9 -3
  4. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/function_metadata.py +4 -0
  5. intersect_sdk-0.7.0/src/intersect_sdk/_internal/interfaces.py +49 -0
  6. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/messages/userspace.py +19 -5
  7. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/schema.py +82 -41
  8. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/capability/base.py +56 -4
  9. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/client.py +12 -12
  10. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/client_callback_definitions.py +7 -43
  11. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/schema.py +7 -6
  12. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/service.py +355 -39
  13. intersect_sdk-0.7.0/src/intersect_sdk/service_callback_definitions.py +16 -0
  14. intersect_sdk-0.7.0/src/intersect_sdk/shared_callback_definitions.py +67 -0
  15. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/version.py +1 -1
  16. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/e2e/test_examples.py +14 -7
  17. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/fixtures/example_schema.json +2 -2
  18. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/fixtures/example_schema.py +1 -0
  19. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/integration/test_return_type_mismatch.py +6 -4
  20. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/integration/test_service.py +32 -30
  21. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/unit/test_base_capability_implementation.py +68 -2
  22. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/unit/test_invalid_schema_runtime.py +1 -1
  23. intersect_sdk-0.7.0/tests/unit/test_schema_valid.py +93 -0
  24. intersect_sdk-0.6.4/src/intersect_sdk/_internal/interfaces.py +0 -20
  25. intersect_sdk-0.6.4/tests/unit/test_schema_valid.py +0 -79
  26. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/LICENSE +0 -0
  27. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/README.md +0 -0
  28. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/__init__.py +0 -0
  29. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/constants.py +0 -0
  30. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/__init__.py +0 -0
  31. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/brokers/__init__.py +0 -0
  32. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/brokers/amqp_client.py +0 -0
  33. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/brokers/broker_client.py +0 -0
  34. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/brokers/mqtt_client.py +0 -0
  35. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/control_plane_manager.py +0 -0
  36. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/discovery_service.py +0 -0
  37. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/control_plane/topic_handler.py +0 -0
  38. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/data_plane/__init__.py +0 -0
  39. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/data_plane/data_plane_manager.py +0 -0
  40. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/data_plane/minio_utils.py +0 -0
  41. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/event_metadata.py +0 -0
  42. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/exceptions.py +0 -0
  43. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/logger.py +0 -0
  44. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/messages/__init__.py +0 -0
  45. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/messages/event.py +0 -0
  46. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/messages/lifecycle.py +0 -0
  47. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/multi_flag_thread_event.py +0 -0
  48. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/pydantic_schema_generator.py +0 -0
  49. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/stoppable_thread.py +0 -0
  50. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/utils.py +0 -0
  51. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/version.py +0 -0
  52. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/_internal/version_resolver.py +0 -0
  53. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/app_lifecycle.py +0 -0
  54. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/capability/__init__.py +0 -0
  55. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/config/__init__.py +0 -0
  56. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/config/client.py +0 -0
  57. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/config/service.py +0 -0
  58. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/config/shared.py +0 -0
  59. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/constants.py +0 -0
  60. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/core_definitions.py +0 -0
  61. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/py.typed +0 -0
  62. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/src/intersect_sdk/service_definitions.py +0 -0
  63. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/__init__.py +0 -0
  64. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/conftest.py +0 -0
  65. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/e2e/__init__.py +0 -0
  66. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/fixtures/__init__.py +0 -0
  67. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/fixtures/return_type_mismatch.py +0 -0
  68. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/integration/__init__.py +0 -0
  69. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/unit/__init__.py +0 -0
  70. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/unit/test_annotations.py +0 -0
  71. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/unit/test_config.py +0 -0
  72. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/unit/test_lifecycle_message.py +0 -0
  73. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/unit/test_schema_invalids.py +0 -0
  74. {intersect_sdk-0.6.4 → intersect_sdk-0.7.0}/tests/unit/test_userspace_message.py +0 -0
  75. {intersect_sdk-0.6.4 → 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.6.4
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>
@@ -35,7 +35,7 @@ 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.7.0"
39
39
 
40
40
  [project.license]
41
41
  text = "BSD-3-Clause"
@@ -66,6 +66,7 @@ test = [
66
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"
67
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"
68
68
  test-unit = "pytest tests/unit --cov=src/intersect_sdk/"
69
+ test-e2e = "pytest tests/e2e --cov=src/intersect_sdk/"
69
70
  lint-format = "ruff format"
70
71
  lint-ruff = "ruff check --fix"
71
72
  lint-mypy = "mypy src/intersect_sdk/"
@@ -162,6 +163,7 @@ max-complexity = 20
162
163
  max-args = 10
163
164
  max-branches = 20
164
165
  max-returns = 10
166
+ max-statements = 75
165
167
 
166
168
  [tool.ruff.lint.extend-per-file-ignores]
167
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
- 'IntersectClientMessageParams',
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',
@@ -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
+ ...
@@ -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,
@@ -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
- capability_name: str
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
- capability_name, status_fn_name, status_fn, min_params = status_info
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 '{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)."
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 '{capability_name}', capability status function '{status_fn_name}' should have a valid return annotation."
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 '{capability_name}', return annotation '{status_signature.return_annotation}' on function '{status_fn_name}' is invalid.\n{e}"
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
- capability_name: str,
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 '{capability_name}', event key '{event_key}' on function '{function_name}' was previously defined differently. \n{diff_str}\n"
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 '{capability_name}', function '{function_name}' should not set data_handler as {event_definition.data_handler} unless an instance is configured in IntersectConfig.data_stores ."
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 '{capability_name}', event key '{event_key}' on function '{function_name}' has an invalid value in the events mapping.\n{e}"
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: type[IntersectBaseCapabilityImplementation],
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
- status_func, response_funcs, event_funcs = _get_functions(capability)
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 capability_name, name, method, min_params in response_funcs:
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 '{capability_name}', function '{name}' should not set response_data_type as {data_handler} unless an instance is configured in IntersectConfig.data_stores ."
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 '{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)."
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 '{capability_name}', parameter '{parameter.name}' type annotation on function '{name}' missing. {SCHEMA_HELP_MSG}"
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 '{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')."
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 '{capability_name}', parameter '{parameter.name}' type annotation '{annotation}' on function '{name}' is invalid\n{e}"
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 '{capability_name}', return type annotation on function '{name}' missing. {SCHEMA_HELP_MSG}"
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 '{capability_name}', return annotation '{return_annotation}' on function '{name}' is invalid.\n{e}"
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[name] = FunctionMetadata(
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
- capability_name,
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 capability_name, name, method, _ in event_funcs:
462
+ for class_name, name, method, _ in event_funcs:
459
463
  _add_events(
460
- capability_name,
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
- function_map[status_fn_name] = FunctionMetadata(
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 get_schema_and_functions_from_capability_implementation(
491
- capability_type: type[IntersectBaseCapabilityImplementation],
492
- capability_name: HierarchyConfig,
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
- 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)
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': capability_name.hierarchy_string('.'),
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 status_schema:
547
- asyncapi_spec['status'] = status_schema
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 asyncapi_spec, function_map, event_map, status_fn_name, status_type_adapter
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__}: Cannot override functions '_intersect_sdk_register_observer' or 'intersect_sdk_emit_event'"
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
+ ]