intersect-sdk 0.6.1a1__py3-none-any.whl

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 (47) hide show
  1. intersect_sdk/__init__.py +64 -0
  2. intersect_sdk/_internal/__init__.py +4 -0
  3. intersect_sdk/_internal/compression.py.tmp +38 -0
  4. intersect_sdk/_internal/constants.py +10 -0
  5. intersect_sdk/_internal/control_plane/__init__.py +0 -0
  6. intersect_sdk/_internal/control_plane/brokers/__init__.py +0 -0
  7. intersect_sdk/_internal/control_plane/brokers/amqp_client.py +280 -0
  8. intersect_sdk/_internal/control_plane/brokers/broker_client.py +58 -0
  9. intersect_sdk/_internal/control_plane/brokers/mqtt_client.py +154 -0
  10. intersect_sdk/_internal/control_plane/control_plane_manager.py +147 -0
  11. intersect_sdk/_internal/control_plane/discovery_service.py +40 -0
  12. intersect_sdk/_internal/data_plane/__init__.py +0 -0
  13. intersect_sdk/_internal/data_plane/data_plane_manager.py +102 -0
  14. intersect_sdk/_internal/data_plane/minio_utils.py +149 -0
  15. intersect_sdk/_internal/event_metadata.py +60 -0
  16. intersect_sdk/_internal/exceptions.py +17 -0
  17. intersect_sdk/_internal/function_metadata.py +27 -0
  18. intersect_sdk/_internal/interfaces.py +20 -0
  19. intersect_sdk/_internal/logger.py +3 -0
  20. intersect_sdk/_internal/messages/__init__.py +0 -0
  21. intersect_sdk/_internal/messages/event.py +157 -0
  22. intersect_sdk/_internal/messages/lifecycle.py +174 -0
  23. intersect_sdk/_internal/messages/userspace.py +171 -0
  24. intersect_sdk/_internal/pydantic_schema_generator.py +504 -0
  25. intersect_sdk/_internal/schema.py +558 -0
  26. intersect_sdk/_internal/stoppable_thread.py +19 -0
  27. intersect_sdk/_internal/utils.py +32 -0
  28. intersect_sdk/_internal/version_resolver.py +58 -0
  29. intersect_sdk/app_lifecycle.py +147 -0
  30. intersect_sdk/capability/__init__.py +14 -0
  31. intersect_sdk/capability/base.py +101 -0
  32. intersect_sdk/client.py +443 -0
  33. intersect_sdk/client_callback_definitions.py +154 -0
  34. intersect_sdk/config/__init__.py +20 -0
  35. intersect_sdk/config/client.py +52 -0
  36. intersect_sdk/config/service.py +42 -0
  37. intersect_sdk/config/shared.py +168 -0
  38. intersect_sdk/constants.py +9 -0
  39. intersect_sdk/core_definitions.py +37 -0
  40. intersect_sdk/py.typed +0 -0
  41. intersect_sdk/schema.py +77 -0
  42. intersect_sdk/service.py +607 -0
  43. intersect_sdk/service_definitions.py +262 -0
  44. intersect_sdk/version.py +15 -0
  45. intersect_sdk-0.6.1a1.dist-info/METADATA +39 -0
  46. intersect_sdk-0.6.1a1.dist-info/RECORD +47 -0
  47. intersect_sdk-0.6.1a1.dist-info/WHEEL +4 -0
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from typing import TYPE_CHECKING, Any, Callable, Literal
5
+
6
+ from pydantic import TypeAdapter
7
+
8
+ from ..exceptions import IntersectInvalidBrokerError
9
+ from ..logger import logger
10
+ from .brokers.mqtt_client import MQTTClient
11
+
12
+ if TYPE_CHECKING:
13
+ from ...config.shared import ControlPlaneConfig
14
+ from .brokers.broker_client import BrokerClient
15
+
16
+ GENERIC_MESSAGE_SERIALIZER = TypeAdapter(Any)
17
+
18
+
19
+ def serialize_message(message: Any) -> bytes:
20
+ """Serialize a message to bytes, in preparation for publishing it on a message broker.
21
+
22
+ Works as a generic serializer/deserializer
23
+ """
24
+ return GENERIC_MESSAGE_SERIALIZER.dump_json(message, warnings=False)
25
+
26
+
27
+ def create_control_provider(
28
+ config: ControlPlaneConfig,
29
+ topic_handler_callback: Callable[[], defaultdict[str, set[Callable[[bytes], None]]]],
30
+ ) -> BrokerClient:
31
+ if config.protocol == 'amqp0.9.1':
32
+ # only try to import the AMQP client if the user is using an AMQP broker
33
+ try:
34
+ from .brokers.amqp_client import AMQPClient
35
+
36
+ return AMQPClient(
37
+ host=config.host,
38
+ port=config.port or 5672,
39
+ username=config.username,
40
+ password=config.password,
41
+ topics_to_handlers=topic_handler_callback,
42
+ )
43
+ except ImportError as e:
44
+ msg = "Configuration includes AMQP broker, but AMQP dependencies were not installed. Install intersect with the 'amqp' optional dependency to use this backend. (i.e. `pip install intersect_sdk[amqp]`)"
45
+ raise IntersectInvalidBrokerError(msg) from e
46
+ # MQTT
47
+ return MQTTClient(
48
+ host=config.host,
49
+ port=config.port or 1883,
50
+ username=config.username,
51
+ password=config.password,
52
+ topics_to_handlers=topic_handler_callback,
53
+ )
54
+
55
+
56
+ class ControlPlaneManager:
57
+ """The ControlPlaneManager class allows for working with multiple brokers from a single function call."""
58
+
59
+ def __init__(
60
+ self,
61
+ control_configs: list[ControlPlaneConfig] | Literal['discovery'],
62
+ ) -> None:
63
+ if control_configs == 'discovery':
64
+ msg = 'Discovery service not implemented yet'
65
+ raise ValueError(msg)
66
+ self._control_providers = [
67
+ create_control_provider(config, self.get_subscription_channels)
68
+ for config in control_configs
69
+ ]
70
+
71
+ self._ready = False
72
+ # topics_to_handlers are managed here and transcend connections/disconnections to the broker
73
+ self._topics_to_handlers: defaultdict[str, set[Callable[[bytes], None]]] = defaultdict(set)
74
+
75
+ def add_subscription_channel(
76
+ self, channel: str, callbacks: set[Callable[[bytes], None]]
77
+ ) -> None:
78
+ """Start listening for userspace messages on a channel on all configured brokers.
79
+
80
+ Note that ALL channels listened to will always handle userspace messages.
81
+
82
+ Params:
83
+ channel: string of the channel which we should start listening to
84
+ """
85
+ self._topics_to_handlers[channel] |= callbacks
86
+ if self._ready:
87
+ for provider in self._control_providers:
88
+ provider.subscribe(channel)
89
+
90
+ def remove_subscription_channel(self, channel: str) -> bool:
91
+ """Stop subscribing to a channel on all configured brokers.
92
+
93
+ Params:
94
+ channel: string of the channel which should no longer be listened to
95
+ Returns: True if channel is no longer being listened to, False if the channel wasn't being listened to in the first place
96
+ """
97
+ try:
98
+ del self._topics_to_handlers[channel]
99
+ except KeyError:
100
+ return False
101
+ else:
102
+ if self._ready:
103
+ for provider in self._control_providers:
104
+ provider.unsubscribe(channel)
105
+ return True
106
+
107
+ def get_subscription_channels(self) -> defaultdict[str, set[Callable[[bytes], None]]]:
108
+ """Get the subscription channels.
109
+
110
+ Note that this function gets accessed as a callback from the direct broker implementations.
111
+
112
+ Returns:
113
+ the dictionary of topics to callback functions
114
+ """
115
+ return self._topics_to_handlers
116
+
117
+ def connect(self) -> None:
118
+ """Connect to all configured brokers and subscribe to any channels configured."""
119
+ # TODO - when implementing discovery service, discovery and connection logic should be applied here
120
+ for provider in self._control_providers:
121
+ provider.connect()
122
+ for channel in self._topics_to_handlers:
123
+ provider.subscribe(channel)
124
+ self._ready = True
125
+
126
+ def disconnect(self) -> None:
127
+ """Disconnect from all configured brokers."""
128
+ self._ready = False
129
+ for provider in self._control_providers:
130
+ provider.disconnect()
131
+
132
+ def publish_message(self, channel: str, msg: Any) -> None:
133
+ """Publish message on channel for all brokers."""
134
+ if self._ready:
135
+ serialized_message = serialize_message(msg)
136
+ for provider in self._control_providers:
137
+ provider.publish(channel, serialized_message)
138
+ else:
139
+ logger.error('Cannot send message, providers are not connected')
140
+
141
+ def is_connected(self) -> bool:
142
+ """Check that we are connected to ALL configured brokers.
143
+
144
+ Returns:
145
+ - True if we are currently connected to all brokers we've configured, False if not
146
+ """
147
+ return all(control_provider.is_connected() for control_provider in self._control_providers)
@@ -0,0 +1,40 @@
1
+ """TODO: This is all old code we currently aren't using."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from urllib.parse import urlparse
7
+ from urllib.request import Request, urlopen
8
+
9
+
10
+ def discover_broker(address: str, broker_endpoint: str) -> tuple[str, str, int]:
11
+ """Get the metadata for a broker from the discovery service.
12
+
13
+ Args:
14
+ address: A string containing the address for the discovery service.
15
+ broker_endpoint: specific API for broker
16
+ Returns:
17
+ Three strings. The first is the name of the broker type (as used in
18
+ _create_broker_client()), the second is the broker's address, and
19
+ the third is the broker's port number.
20
+ """
21
+ url = f'{address}/v0.1/{broker_endpoint}'
22
+
23
+ # Get scheme associated with the `url` string
24
+ scheme = urlparse(url).scheme
25
+
26
+ # Only accept `http` and `https` schemes, otherwise raise error
27
+ if scheme not in ('http', 'https'):
28
+ msg = f'URL scheme is {scheme}, only http or https schemes are accepted'
29
+ raise ValueError(msg)
30
+
31
+ request = Request(url) # noqa: S310 (scheme checked earlier)
32
+ with urlopen(request) as response: # noqa: S310 (scheme checked earlier)
33
+ body = response.read()
34
+
35
+ broker_info = json.loads(body.decode('utf-8'))
36
+ endpoint = broker_info['endpoint']
37
+ backend_name = broker_info['backendName']
38
+ address, port = endpoint.split(':', 1)
39
+
40
+ return backend_name, address, port
File without changes
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ from typing import TYPE_CHECKING
5
+
6
+ from ...core_definitions import IntersectDataHandler, IntersectMimeType
7
+ from ..exceptions import IntersectError
8
+ from ..logger import logger
9
+ from .minio_utils import MinioPayload, create_minio_store, get_minio_object, send_minio_object
10
+
11
+ if TYPE_CHECKING:
12
+ from ...config.shared import DataStoreConfigMap, HierarchyConfig
13
+ from ..messages.event import EventMessage
14
+ from ..messages.userspace import UserspaceMessage
15
+
16
+
17
+ class DataPlaneManager:
18
+ """The DataPlaneManager serves as a common interface to the data plane.
19
+
20
+ The API supports extensive plug-and-play for different data providers.
21
+ """
22
+
23
+ def __init__(self, hierarchy: HierarchyConfig, data_configs: DataStoreConfigMap) -> None:
24
+ """Inside the constructor, we verify that all data configuration credentials are correct.
25
+
26
+ Params:
27
+ hierarchy: Hierarchy configuration
28
+ data_configs: data configuration
29
+ """
30
+ self._hierarchy = hierarchy
31
+ self._minio_providers = list(map(create_minio_store, data_configs.minio))
32
+
33
+ # warn users about missing data plane
34
+ if not self._minio_providers:
35
+ logger.warn('WARNING: This service cannot support any MINIO instances')
36
+
37
+ def incoming_message_data_handler(self, message: UserspaceMessage | EventMessage) -> bytes:
38
+ """Get data from the request data provider.
39
+
40
+ Params:
41
+ message: the message sent externally to this location
42
+ Returns:
43
+ the actual data we want to submit to the user function
44
+ Raise:
45
+ IntersectException - if we couldn't get the data
46
+ """
47
+ request_data_handler = message['headers']['data_handler']
48
+ if request_data_handler == IntersectDataHandler.MESSAGE:
49
+ return message['payload'] # type: ignore[return-value]
50
+ if request_data_handler == IntersectDataHandler.MINIO:
51
+ # TODO - we may want to send additional provider information in the payload
52
+ payload: MinioPayload = message['payload'] # type: ignore[assignment]
53
+ provider = None
54
+ for store in self._minio_providers:
55
+ if store._base_url._url.geturl() == payload['minio_url']: # noqa: SLF001 (only way to get URL from MINIO API)
56
+ provider = store
57
+ break
58
+ if not provider:
59
+ logger.error(
60
+ f"You did not configure listening to MINIO instance '{payload['minio_url']}'. You must fix this to handle this data."
61
+ )
62
+ raise IntersectError
63
+ return get_minio_object(provider, payload)
64
+ logger.warning(f'Cannot parse data handler {request_data_handler}')
65
+ raise IntersectError
66
+
67
+ def outgoing_message_data_handler(
68
+ self,
69
+ function_response: bytes,
70
+ content_type: IntersectMimeType,
71
+ data_handler: IntersectDataHandler,
72
+ ) -> bytes | MinioPayload:
73
+ """Send the user's response to the appropriate data provider.
74
+
75
+ Params:
76
+ - function_response - the return value from the user's function
77
+ - content_type - content type of function_response
78
+ - data_handler - where we're going to send the data off to (i.e. the message, MINIO...)
79
+
80
+ Returns:
81
+ the payload of the message
82
+ Raise:
83
+ IntersectException - if there was any error in submitting the response
84
+ """
85
+ # TODO - instead of requiring users to specify the data handler themselves, another idea could be to use
86
+ # sys.getsizeof(function_response) and determine the data handler dynamically
87
+ # users could perhaps specify T1/T2/T3 data types but not the specific implementation
88
+ if data_handler == IntersectDataHandler.MESSAGE:
89
+ return function_response
90
+ if data_handler == IntersectDataHandler.MINIO:
91
+ if not self._minio_providers:
92
+ logger.error(
93
+ 'No MINIO provider configured, so you cannot set response_data_handler on @intersect_message to equal IntersectDataHandler.MINIO .'
94
+ )
95
+ raise IntersectError
96
+ provider = random.choice(self._minio_providers) # noqa: S311 (TODO choose a MINIO provider better than at random - this may be determined from external message params)
97
+ return send_minio_object(function_response, provider, content_type, self._hierarchy)
98
+
99
+ logger.error(
100
+ f'No support implemented for code {data_handler}, please upgrade your intersect-sdk version.'
101
+ )
102
+ raise IntersectError
@@ -0,0 +1,149 @@
1
+ import mimetypes
2
+ from hashlib import sha224
3
+ from io import BytesIO
4
+ from uuid import uuid4
5
+
6
+ from minio import Minio
7
+ from minio.error import MinioException
8
+ from typing_extensions import TypedDict
9
+ from urllib3.exceptions import MaxRetryError
10
+ from urllib3.util import parse_url
11
+
12
+ from ...config.shared import DataStoreConfig, HierarchyConfig
13
+ from ...core_definitions import IntersectMimeType
14
+ from ..exceptions import IntersectError
15
+ from ..logger import logger
16
+ from ..utils import die
17
+
18
+
19
+ class MinioPayload(TypedDict):
20
+ """This is a payload which gets sent in the actual userspace message if the data handler is "MINIO"."""
21
+
22
+ minio_url: str
23
+ """
24
+ The complete URL of the MINIO instance. This is used for finding the correct MINIO instance when retrieving an object.
25
+ """
26
+ minio_bucket: str
27
+ """
28
+ The name of the bucket where the object is located. Each service should only create objects in one bucket.
29
+ """
30
+ minio_object_id: str
31
+ """
32
+ The name of the object. This is a random UUID.
33
+ """
34
+
35
+
36
+ def _condense_minio_bucket_name(hierarchy: HierarchyConfig) -> str:
37
+ """Condense a hierarchy string into a string less than 64 characters.
38
+
39
+ This function is needed to handle MINIO bucket names. Collisions should be extremely rare,
40
+ and it should be fairly straightforward to identify a specific bucket for MINIO admins.
41
+
42
+ Bucket name = first characters (up to 6) of service name + hyphen + sha224 of full hierarchy string
43
+
44
+ TODO in the future, MINIO calls should be system-level only, so only the system + facility + organization
45
+ should need to be hashed.
46
+ Also, potentially come up with a better hashing algorithm (though sha224 is compressed enough, and gives us 56 characters).
47
+ """
48
+ return f'{hierarchy.service[:6]}-{sha224(hierarchy.hierarchy_string().encode()).hexdigest()}'
49
+
50
+
51
+ def create_minio_store(config: DataStoreConfig) -> Minio:
52
+ config_uri = parse_url(config.host)
53
+ if not config_uri.host:
54
+ die(f'Minio configuration host {config.host} cannot be parsed as a valid hostname.')
55
+
56
+ client = Minio(
57
+ secure=config_uri.scheme == 'https',
58
+ access_key=config.username,
59
+ secret_key=config.password,
60
+ endpoint=config_uri.host if not config.port else f'{config_uri.host}:{config.port}',
61
+ )
62
+ try:
63
+ # it doesn't matter if the bucket exists, we're just checking the exception
64
+ # this is the fastest way in the public API to perform a HEAD request.
65
+ client.bucket_exists('qwop')
66
+ except MinioException:
67
+ die(f'Invalid credentials for Minio instance: {config}')
68
+ return client
69
+
70
+
71
+ def send_minio_object(
72
+ data: bytes, provider: Minio, content_type: IntersectMimeType, hierarchy: HierarchyConfig
73
+ ) -> MinioPayload:
74
+ """Core function to save data in MINIO.
75
+
76
+ Params:
77
+ data: user response data, as bytes
78
+ provider: the Minio client
79
+ content_type: the content type of the body
80
+ hierarchy: the hierarchy configuration
81
+ Returns:
82
+ The MINIO payload which gets sent in the actual message.
83
+
84
+ Raises:
85
+ IntersectException - if any non-fatal MinIO error is caught
86
+ """
87
+ bucket_name = _condense_minio_bucket_name(hierarchy)
88
+ # mimetypes.guess_extension() is a nice-to-have for MINIO preview, but isn't essential.
89
+ object_id = str(uuid4()) + (mimetypes.guess_extension(content_type.value) or '')
90
+ try:
91
+ if not provider.bucket_exists(bucket_name):
92
+ provider.make_bucket(bucket_name)
93
+ buff_data = BytesIO(data)
94
+ provider.put_object(
95
+ bucket_name=bucket_name,
96
+ object_name=object_id,
97
+ data=buff_data,
98
+ length=buff_data.getbuffer().nbytes,
99
+ content_type=content_type.value,
100
+ )
101
+ return MinioPayload(
102
+ minio_url=provider._base_url._url.geturl(), # noqa: SLF001 (only way to get URL from MINIO API)
103
+ minio_bucket=bucket_name,
104
+ minio_object_id=object_id,
105
+ )
106
+ except MaxRetryError as e:
107
+ logger.warning(
108
+ f'Non-fatal MinIO error when sending object, the server may be under stress but you should double-check your configuration. Details: \n{e}'
109
+ )
110
+ raise IntersectError from e
111
+ except MinioException as e:
112
+ logger.error(
113
+ f'Important MinIO error when sending object, this usually indicates a problem with your configuration. Details: \n{e}'
114
+ )
115
+ raise IntersectError from e
116
+
117
+
118
+ def get_minio_object(provider: Minio, payload: MinioPayload) -> bytes:
119
+ """Core function to retrieve data from MINIO.
120
+
121
+ Params:
122
+ provider: a pre-cached MinIO provider from the data provider store
123
+ payload: the payload from the message (at this point, the minio_url should exist)
124
+
125
+ Returns:
126
+ user response data, as raw bytes
127
+ Raises:
128
+ IntersectException - if any non-fatal MinIO error is caught
129
+ """
130
+ try:
131
+ response = provider.get_object(
132
+ bucket_name=payload['minio_bucket'], object_name=payload['minio_object_id']
133
+ )
134
+ # TODO - objects should ONLY be removed if they are T1
135
+ provider.remove_object(
136
+ bucket_name=payload['minio_bucket'], object_name=payload['minio_object_id']
137
+ )
138
+ except MaxRetryError as e:
139
+ logger.warning(
140
+ f'Non-fatal MinIO error when retrieving object, the server may be under stress but you should double-check your configuration. Details: \n{e}'
141
+ )
142
+ raise IntersectError from e
143
+ except MinioException as e:
144
+ logger.error(
145
+ f'Important MinIO error when retrieving object, this usually indicates a problem with your configuration. Details: \n{e}'
146
+ )
147
+ raise IntersectError from e
148
+ else:
149
+ return response.data
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, NamedTuple
4
+
5
+ if TYPE_CHECKING:
6
+ from pydantic import TypeAdapter
7
+
8
+ from ..core_definitions import IntersectDataHandler, IntersectMimeType
9
+ from ..service_definitions import IntersectEventDefinition
10
+
11
+
12
+ class EventMetadata(NamedTuple):
13
+ """Internal cache of metadata associated with an event.
14
+
15
+ NOTE: both this class and all properties in it should remain immutable after creation
16
+ """
17
+
18
+ operations: set[str]
19
+ """
20
+ A hash set of operations which advertise this event
21
+ """
22
+ type: type
23
+ """
24
+ The actual type of the event
25
+ """
26
+ type_adapter: TypeAdapter[Any]
27
+ """
28
+ The type adapter used for deserializing and validating events
29
+ """
30
+ data_transfer_handler: IntersectDataHandler
31
+ """
32
+ The data transfer type
33
+ """
34
+ content_type: IntersectMimeType
35
+ """
36
+ The content type
37
+ """
38
+
39
+
40
+ def definition_metadata_differences(
41
+ definition: IntersectEventDefinition, metadata: EventMetadata
42
+ ) -> list[tuple[str, str, str]]:
43
+ """Return a list of differences between 'definition' and 'metadata'.
44
+
45
+ First tuple value = defintion key
46
+ Second tuple value = second value
47
+ Third tuple value = already cached value
48
+ """
49
+ differences = []
50
+ if definition.event_type != metadata.type:
51
+ differences.append(('event_type', str(definition.event_type), str(metadata.type)))
52
+ if definition.content_type != metadata.content_type:
53
+ differences.append(
54
+ ('content_type', str(definition.content_type), str(metadata.content_type))
55
+ )
56
+ if definition.data_handler != metadata.data_transfer_handler:
57
+ differences.append(
58
+ ('data_handler', str(definition.data_handler), str(metadata.data_transfer_handler))
59
+ )
60
+ return differences
@@ -0,0 +1,17 @@
1
+ class IntersectError(Exception):
2
+ """Generic marker for INTERSECT-specific exceptions."""
3
+
4
+
5
+ class IntersectApplicationError(IntersectError):
6
+ """This is a special IntersectException, thrown if user application logic throws ANY kind of Exception.
7
+
8
+ In general, validation should be expressed through JSON schema as much as possible; however, JSON schema is NOT a complete prescription for input validation.
9
+ When this exception is thrown, however, we do not leak any exception information in the error message. On the other hand, if the input fails
10
+ JSON schema validation, Pydantic will throw a specific ValidationError, and that exception information will deliberately be exposed in the error message.
11
+
12
+ This exception should strictly be used for control flow - it should NEVER be a fatal exception
13
+ """
14
+
15
+
16
+ class IntersectInvalidBrokerError(IntersectError):
17
+ """Exception when invalid broker backend used."""
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Callable, NamedTuple
4
+
5
+ if TYPE_CHECKING:
6
+ from pydantic import TypeAdapter
7
+
8
+
9
+ class FunctionMetadata(NamedTuple):
10
+ """Internal cache of public function metadata.
11
+
12
+ NOTE: both this class and all properties in it should remain immutable after creation
13
+ """
14
+
15
+ method: Callable[[Any], Any]
16
+ """
17
+ The raw method of the function. The function itself is useless and should not be called,
18
+ but will store user-defined attributes needed for internal handling of data.
19
+ """
20
+ request_adapter: TypeAdapter[Any] | None
21
+ """
22
+ Type adapter for serializing and validating requests. Should only be null if user did not specify a request parameter.
23
+ """
24
+ response_adapter: TypeAdapter[Any]
25
+ """
26
+ Type adapter for serializing and validating responses.
27
+ """
@@ -0,0 +1,20 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+
5
+ class IntersectEventObserver(ABC):
6
+ """Abstract definition of an entity which observes an INTERSECT event (i.e. IntersectService).
7
+
8
+ Used as the common interface for event emitters (i.e. CapabilityImplementations).
9
+ """
10
+
11
+ @abstractmethod
12
+ def _on_observe_event(self, event_name: str, event_value: Any, operation: str) -> None:
13
+ """How to react to an event being fired.
14
+
15
+ Args:
16
+ event_name: The key of the event which is fired.
17
+ event_value: The value of the event which is fired.
18
+ operation: The source of the event (generally the function name, not directly invoked by application devs)
19
+ """
20
+ ...
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger('intersect-sdk')
File without changes