intersect-sdk-common 0.9.2__tar.gz → 0.9.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/PKG-INFO +2 -2
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/README.md +1 -1
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/pyproject.toml +5 -3
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/control_plane/brokers/amqp_client.py +39 -29
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/control_plane/brokers/broker_client.py +2 -1
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/control_plane/brokers/mqtt_client.py +18 -15
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/control_plane/control_plane_manager.py +31 -19
- intersect_sdk_common-0.9.4/src/intersect_sdk_common/control_plane/custom_binary_protocol.py +77 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/control_plane/topic_handler.py +2 -2
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/__init__.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/config.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/constants.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/control_plane/__init__.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/control_plane/brokers/__init__.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/control_plane/definitions.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/control_plane/messages/__init__.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/control_plane/messages/event.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/control_plane/messages/lifecycle.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/control_plane/messages/userspace.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/core_definitions.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/data_plane/__init__.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/data_plane/data_plane_manager.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/data_plane/minio_utils.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/exceptions.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/logger.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/py.typed +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/utils/__init__.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/utils/multi_flag_thread_event.py +0 -0
- {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: intersect-sdk-common
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: Python SDK to interact with INTERSECT
|
|
5
5
|
Keywords: intersect
|
|
6
6
|
Author: Lance Drane, Marshall McDonnell, Seth Hitefield, Andrew Ayres, Gregory Cage, Jesse McGaha, Robert Smith, Gavin Wiggins, Michael Brim, Rick Archibald, Addi Malviya Thakur
|
|
@@ -48,5 +48,5 @@ The INTERSECT-SDK MUST follow semantic versioning, and MUST update its semantic
|
|
|
48
48
|
```bash
|
|
49
49
|
uv venv .venv
|
|
50
50
|
source .venv/bin/activate
|
|
51
|
-
uv sync --
|
|
51
|
+
uv sync --all-extras --all-groups
|
|
52
52
|
```
|
|
@@ -18,7 +18,7 @@ readme = "README.md"
|
|
|
18
18
|
license = { text = "BSD-3-Clause" }
|
|
19
19
|
requires-python = ">=3.10,<4.0"
|
|
20
20
|
keywords = ["intersect"]
|
|
21
|
-
version = "0.9.
|
|
21
|
+
version = "0.9.4"
|
|
22
22
|
classifiers = [
|
|
23
23
|
"Programming Language :: Python :: 3",
|
|
24
24
|
"Programming Language :: Python :: 3.10",
|
|
@@ -34,13 +34,15 @@ dependencies = [
|
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
[dependency-groups]
|
|
37
|
-
|
|
37
|
+
dev = [
|
|
38
38
|
"pre-commit>=3.3.1",
|
|
39
39
|
"ruff==0.12.7",
|
|
40
40
|
"mypy>=1.10.0",
|
|
41
41
|
"codespell>=2.3.0",
|
|
42
|
+
"pytest>=7.3.2",
|
|
43
|
+
"pytest-cov>=4.1.0",
|
|
44
|
+
"httpretty>=1.1.4",
|
|
42
45
|
]
|
|
43
|
-
test = ["pytest>=7.3.2", "pytest-cov>=4.1.0", "httpretty>=1.1.4"]
|
|
44
46
|
|
|
45
47
|
[build-system]
|
|
46
48
|
requires = ["uv_build>=0.10.2,<0.11.0"]
|
|
@@ -23,14 +23,12 @@ from ...utils.multi_flag_thread_event import MultiFlagThreadEvent
|
|
|
23
23
|
from .broker_client import BrokerClient
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
-
from collections.abc import Callable
|
|
27
|
-
|
|
28
26
|
from pika.channel import Channel
|
|
29
27
|
from pika.frame import Frame
|
|
30
28
|
from pika.spec import Basic, BasicProperties
|
|
31
29
|
|
|
30
|
+
from ..control_plane_manager import ControlPlaneManager
|
|
32
31
|
from ..definitions import MessageCallback
|
|
33
|
-
from ..topic_handler import TopicHandler
|
|
34
32
|
|
|
35
33
|
|
|
36
34
|
_AMQP_MAX_RETRIES = 10
|
|
@@ -95,8 +93,7 @@ class AMQPClient(BrokerClient):
|
|
|
95
93
|
port: int,
|
|
96
94
|
username: str,
|
|
97
95
|
password: str,
|
|
98
|
-
|
|
99
|
-
find_wildcard_handler: Callable[[str], TopicHandler | None],
|
|
96
|
+
control_plane_manager: ControlPlaneManager,
|
|
100
97
|
is_root: bool,
|
|
101
98
|
) -> None:
|
|
102
99
|
"""The default constructor.
|
|
@@ -106,8 +103,7 @@ class AMQPClient(BrokerClient):
|
|
|
106
103
|
port: port number of AMQP broker
|
|
107
104
|
username: username credentials for AMQP broker
|
|
108
105
|
password: password credentials for AMQP broker
|
|
109
|
-
|
|
110
|
-
find_wildcard_handler: callback function which gets the wildcard topic handler from the channel manager
|
|
106
|
+
control_plane_manager: reference to the ControlPlaneManager instance, remember to ONLY use functions which do not mutate state
|
|
111
107
|
is_root: Whether or not the client can configure exchanges and queues themselves (core services), or if this must be delegated to a Core Service (SDK Clients/Services)
|
|
112
108
|
"""
|
|
113
109
|
self._connection_params = pika.ConnectionParameters(
|
|
@@ -132,11 +128,11 @@ class AMQPClient(BrokerClient):
|
|
|
132
128
|
self._thread: threading.Thread | None = None
|
|
133
129
|
|
|
134
130
|
# Callback to the topics_to_handler list inside of
|
|
135
|
-
self.
|
|
136
|
-
self._find_wildcard_handler = find_wildcard_handler
|
|
131
|
+
self._control_plane_manager = control_plane_manager
|
|
137
132
|
|
|
138
133
|
# mapping of topics to callables which can unsubscribe from the topic
|
|
139
134
|
self._topics_to_consumer_tags: dict[str, _ConsumerTagInfo] = {}
|
|
135
|
+
"""NOTE: separators for topics in this object is '.' , not '/' """
|
|
140
136
|
self._consumer_tags_to_threads: dict[str, threading.Thread] = {}
|
|
141
137
|
|
|
142
138
|
self._should_disconnect = False
|
|
@@ -227,17 +223,22 @@ class AMQPClient(BrokerClient):
|
|
|
227
223
|
else:
|
|
228
224
|
logger.error('Unable to publish message %s on topic %s', payload, topic)
|
|
229
225
|
|
|
230
|
-
def subscribe(self, topic: str, persist: bool) -> None:
|
|
226
|
+
def subscribe(self, topic: str, persist: bool, queue_name: str) -> None:
|
|
231
227
|
"""Subscribe to a topic.
|
|
232
228
|
|
|
233
229
|
topic: system-of-system hierarchy. In AMQP parlance this gets translated to the routing key.
|
|
234
230
|
persist: If True, we will create an idempotent queue name which should persist
|
|
235
231
|
even on broker or application shutdown. If False, we will allow the server to create a unique
|
|
236
232
|
queue name, and the queue will be destroyed once the associated channel is closed.
|
|
233
|
+
queue_name: The name of the queue to subscribe to.
|
|
237
234
|
"""
|
|
238
235
|
topic = _hierarchy_2_amqp(topic)
|
|
239
236
|
cb = functools.partial(
|
|
240
|
-
self._create_queue,
|
|
237
|
+
self._create_queue,
|
|
238
|
+
channel=self._channel_in,
|
|
239
|
+
topic=topic,
|
|
240
|
+
persist=persist,
|
|
241
|
+
queue_name=queue_name,
|
|
241
242
|
)
|
|
242
243
|
self._connection.ioloop.add_callback_threadsafe(cb)
|
|
243
244
|
|
|
@@ -390,7 +391,7 @@ class AMQPClient(BrokerClient):
|
|
|
390
391
|
def _on_input_channel_open(self, channel: Channel) -> None:
|
|
391
392
|
channel_num = 1
|
|
392
393
|
self._channel_in = channel
|
|
393
|
-
# consumer channel flag can be set immediately
|
|
394
|
+
# consumer channel flag can be set immediately (NOTE: this means that code which simultaneously creates Clients and Services will need to manually wait, to ensure that each application has made the basic_consume call to the broker before messages are published)
|
|
394
395
|
self._channel_flags.set_nth_flag(channel_num)
|
|
395
396
|
cb_1 = functools.partial(self._on_channel_closed, channel_num=channel_num)
|
|
396
397
|
self._channel_in.add_on_close_callback(cb_1)
|
|
@@ -403,17 +404,7 @@ class AMQPClient(BrokerClient):
|
|
|
403
404
|
callback=cb_2,
|
|
404
405
|
)
|
|
405
406
|
else:
|
|
406
|
-
|
|
407
|
-
amqp_topic = _hierarchy_2_amqp(topic)
|
|
408
|
-
cb = functools.partial(
|
|
409
|
-
self._setup_basic_qos,
|
|
410
|
-
None,
|
|
411
|
-
channel=channel,
|
|
412
|
-
topic=amqp_topic,
|
|
413
|
-
persist=topic_handler.topic_persist,
|
|
414
|
-
queue_name=topic_handler.queue_name,
|
|
415
|
-
)
|
|
416
|
-
self._connection.ioloop.add_callback_threadsafe(cb)
|
|
407
|
+
self._create_all_queues(channel)
|
|
417
408
|
|
|
418
409
|
def _on_exchange_declareok(self, _unused_frame: Frame, channel: Channel) -> None:
|
|
419
410
|
"""Create a queue on the broker (called from AMQP).
|
|
@@ -425,7 +416,17 @@ class AMQPClient(BrokerClient):
|
|
|
425
416
|
_unused_frame: response from declaring the exchange on the broker (irrelevant).
|
|
426
417
|
channel: The Channel being instantiated.
|
|
427
418
|
"""
|
|
428
|
-
|
|
419
|
+
self._create_all_queues(channel)
|
|
420
|
+
|
|
421
|
+
def _create_all_queues(self, channel: Channel) -> None:
|
|
422
|
+
"""Create queues for all subscription channels on the broker.
|
|
423
|
+
|
|
424
|
+
This should only be called once we've verified that the exchange exists.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
channel: The Channel being instantiated.
|
|
428
|
+
"""
|
|
429
|
+
for topic, topic_handler in self._control_plane_manager.get_all_subscription_channels():
|
|
429
430
|
amqp_topic = _hierarchy_2_amqp(topic)
|
|
430
431
|
cb = functools.partial(
|
|
431
432
|
self._create_queue,
|
|
@@ -456,6 +457,7 @@ class AMQPClient(BrokerClient):
|
|
|
456
457
|
queue=queue_name
|
|
457
458
|
if persist
|
|
458
459
|
else '', # if we're transient, let the broker generate a name for us
|
|
460
|
+
passive=not self._is_root,
|
|
459
461
|
durable=persist,
|
|
460
462
|
exclusive=not persist, # transient queues can be exclusive
|
|
461
463
|
callback=cb,
|
|
@@ -597,17 +599,25 @@ class AMQPClient(BrokerClient):
|
|
|
597
599
|
channel.basic_ack(basic_deliver.delivery_tag)
|
|
598
600
|
return
|
|
599
601
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
+
amqp_routing_key_rep = basic_deliver.routing_key
|
|
603
|
+
manager_routing_key_rep = _amqp_2_hierarchy(basic_deliver.routing_key)
|
|
604
|
+
topic_handler = self._control_plane_manager.get_non_wildcard_subscription_channels().get(
|
|
605
|
+
manager_routing_key_rep
|
|
606
|
+
)
|
|
602
607
|
if not topic_handler:
|
|
603
608
|
# we may have included the topic in one of our wildcard handlers
|
|
604
|
-
|
|
609
|
+
wildcard_result = self._control_plane_manager.get_wildcard_topic_and_topic_handler(
|
|
610
|
+
manager_routing_key_rep
|
|
611
|
+
)
|
|
612
|
+
if wildcard_result:
|
|
613
|
+
manager_routing_key_rep, topic_handler = wildcard_result
|
|
614
|
+
amqp_routing_key_rep = _hierarchy_2_amqp(manager_routing_key_rep)
|
|
605
615
|
if topic_handler:
|
|
606
|
-
consumer_tag_info = self._topics_to_consumer_tags.get(
|
|
616
|
+
consumer_tag_info = self._topics_to_consumer_tags.get(amqp_routing_key_rep)
|
|
607
617
|
if not consumer_tag_info:
|
|
608
618
|
logger.error(
|
|
609
619
|
'Could not fetch consumer tag for topic %s, please inform an INTERSECT-SDK developer that you saw this message',
|
|
610
|
-
|
|
620
|
+
amqp_routing_key_rep,
|
|
611
621
|
)
|
|
612
622
|
return
|
|
613
623
|
while not consumer_tag_info.consumer_tag_obtained():
|
|
@@ -55,7 +55,7 @@ class BrokerClient(Protocol):
|
|
|
55
55
|
"""
|
|
56
56
|
...
|
|
57
57
|
|
|
58
|
-
def subscribe(self, topic: str, persist: bool) -> None:
|
|
58
|
+
def subscribe(self, topic: str, persist: bool, queue_name: str) -> None:
|
|
59
59
|
"""Subscribe to a topic over the pre-existing connection (via connect()).
|
|
60
60
|
|
|
61
61
|
This function should ALSO be called by reconnect handlers, and not just directly.
|
|
@@ -64,6 +64,7 @@ class BrokerClient(Protocol):
|
|
|
64
64
|
Args:
|
|
65
65
|
topic: Topic to subscribe to.
|
|
66
66
|
persist: Whether or not the queue subscribed to is intended to be long-lived.
|
|
67
|
+
queue_name: The name of the queue to subscribe to. Used or ignored depending on the protocol.
|
|
67
68
|
"""
|
|
68
69
|
...
|
|
69
70
|
|
|
@@ -22,12 +22,10 @@ from ...logger import logger
|
|
|
22
22
|
from .broker_client import BrokerClient
|
|
23
23
|
|
|
24
24
|
if TYPE_CHECKING:
|
|
25
|
-
from collections.abc import Callable
|
|
26
|
-
|
|
27
25
|
from paho.mqtt.client import DisconnectFlags
|
|
28
26
|
from paho.mqtt.reasoncodes import ReasonCode
|
|
29
27
|
|
|
30
|
-
from ..
|
|
28
|
+
from ..control_plane_manager import ControlPlaneManager
|
|
31
29
|
|
|
32
30
|
|
|
33
31
|
_MQTT_MAX_RETRIES = 10
|
|
@@ -37,7 +35,7 @@ _MQTT_MAX_RETRIES = 10
|
|
|
37
35
|
# this will be a breaking change, so only add it when ready to break
|
|
38
36
|
def _hierarchy_2_mqtt(hierarchy: str) -> str:
|
|
39
37
|
"""Take the hierarchy string format saved in the Service and map it to the MQTT topic format. Currently just covers wildcards."""
|
|
40
|
-
return hierarchy.replace('
|
|
38
|
+
return hierarchy.replace('*', '+')
|
|
41
39
|
|
|
42
40
|
|
|
43
41
|
class MQTTClient(BrokerClient):
|
|
@@ -61,8 +59,7 @@ class MQTTClient(BrokerClient):
|
|
|
61
59
|
port: int,
|
|
62
60
|
username: str,
|
|
63
61
|
password: str,
|
|
64
|
-
|
|
65
|
-
find_wildcard_handler: Callable[[str], TopicHandler | None],
|
|
62
|
+
control_plane_manager: ControlPlaneManager,
|
|
66
63
|
uid: str | None = None,
|
|
67
64
|
) -> None:
|
|
68
65
|
"""The default constructor.
|
|
@@ -72,8 +69,7 @@ class MQTTClient(BrokerClient):
|
|
|
72
69
|
port: port number of MQTT broker
|
|
73
70
|
username: username credentials for MQTT broker
|
|
74
71
|
password: password credentials for MQTT broker
|
|
75
|
-
|
|
76
|
-
find_wildcard_handler: callback function which gets the wildcard topic handler from the channel manager
|
|
72
|
+
control_plane_manager: reference to the ControlPlaneManager instance, remember to ONLY use functions which do not mutate state
|
|
77
73
|
uid: A string representing the unique id to identify the client.
|
|
78
74
|
"""
|
|
79
75
|
# Unique id for the MQTT broker to associate this client with
|
|
@@ -97,8 +93,7 @@ class MQTTClient(BrokerClient):
|
|
|
97
93
|
self._connected_flag = threading.Event()
|
|
98
94
|
|
|
99
95
|
# ConnectionManager callable state
|
|
100
|
-
self.
|
|
101
|
-
self._find_wildcard_handler = find_wildcard_handler
|
|
96
|
+
self._control_plane_manager = control_plane_manager
|
|
102
97
|
|
|
103
98
|
# MQTT v3.1.1 automatically downgrades a QOS which is too high (good), but MQTT v5 will terminate the connection (bad)
|
|
104
99
|
# see https://github.com/rabbitmq/rabbitmq-server/discussions/11842
|
|
@@ -170,12 +165,13 @@ class MQTTClient(BrokerClient):
|
|
|
170
165
|
resolved_topic, payload, qos=self._max_supported_qos if persist else 0, properties=props
|
|
171
166
|
)
|
|
172
167
|
|
|
173
|
-
def subscribe(self, topic: str, persist: bool) -> None:
|
|
168
|
+
def subscribe(self, topic: str, persist: bool, queue_name: str) -> None: # noqa: ARG002
|
|
174
169
|
"""Subscribe to a topic over the pre-existing connection (via connect()).
|
|
175
170
|
|
|
176
171
|
Args:
|
|
177
172
|
topic: Topic to subscribe to.
|
|
178
173
|
persist: Determine if the associated message queue of the topic is long-lived (True) or not (False)
|
|
174
|
+
queue_name: The name of the queue to subscribe to. Ignored with current MQTT implementation.
|
|
179
175
|
"""
|
|
180
176
|
resolved_topic = _hierarchy_2_mqtt(topic)
|
|
181
177
|
# NOTE: RabbitMQ only works with QOS of 1 and 0, and seems to convert QOS2 to QOS1
|
|
@@ -204,10 +200,17 @@ class MQTTClient(BrokerClient):
|
|
|
204
200
|
message: MQTT message
|
|
205
201
|
"""
|
|
206
202
|
# NOTE: should not need to convert the MQTT topic to the protocol agnostic representation, as we only change the wildcard format (which won't show up as the message topic)
|
|
207
|
-
|
|
203
|
+
inmem_topic = message.topic
|
|
204
|
+
topic_handler = self._control_plane_manager.get_non_wildcard_subscription_channels().get(
|
|
205
|
+
inmem_topic
|
|
206
|
+
)
|
|
208
207
|
if not topic_handler:
|
|
209
208
|
# we may have included the topic in one of our wildcard handlers
|
|
210
|
-
|
|
209
|
+
wildcard_result = self._control_plane_manager.get_wildcard_topic_and_topic_handler(
|
|
210
|
+
inmem_topic
|
|
211
|
+
)
|
|
212
|
+
if wildcard_result:
|
|
213
|
+
_, topic_handler = wildcard_result
|
|
211
214
|
# Note that if we return prior to the callback, there will be no reply message
|
|
212
215
|
if not topic_handler:
|
|
213
216
|
logger.warning('Incompatible message topic %s, rejecting message', message.topic)
|
|
@@ -298,8 +301,8 @@ class MQTTClient(BrokerClient):
|
|
|
298
301
|
self._max_supported_qos = properties.MaximumQoS
|
|
299
302
|
|
|
300
303
|
self._connected_flag.set()
|
|
301
|
-
for topic, topic_handler in self.
|
|
302
|
-
self.subscribe(topic, topic_handler.topic_persist)
|
|
304
|
+
for topic, topic_handler in self._control_plane_manager.get_all_subscription_channels():
|
|
305
|
+
self.subscribe(topic, topic_handler.topic_persist, topic_handler.queue_name)
|
|
303
306
|
else:
|
|
304
307
|
# This will generally suggest a misconfiguration
|
|
305
308
|
self._connected = False
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Wrapper around interacting with the control plane, which may be multiple brokers."""
|
|
2
2
|
|
|
3
|
+
import itertools
|
|
3
4
|
import re
|
|
4
|
-
from collections.abc import Callable
|
|
5
5
|
|
|
6
6
|
from ..config import ControlPlaneConfig
|
|
7
7
|
from ..exceptions import IntersectSetupError
|
|
@@ -23,8 +23,7 @@ _CHANNEL_REGEX = re.compile(r'^[a-zA-Z0-9*/-]+[a-zA-Z0-9*#/-]$')
|
|
|
23
23
|
|
|
24
24
|
def _create_control_provider(
|
|
25
25
|
config: ControlPlaneConfig,
|
|
26
|
-
|
|
27
|
-
find_wildcard_handler: Callable[[str], TopicHandler | None],
|
|
26
|
+
control_plane_manager: 'ControlPlaneManager',
|
|
28
27
|
) -> BrokerClient:
|
|
29
28
|
if config.protocol == 'amqp0.9.1':
|
|
30
29
|
from .brokers.amqp_client import ( # noqa: PLC0415 (lazy load all AMQP modules)
|
|
@@ -36,8 +35,7 @@ def _create_control_provider(
|
|
|
36
35
|
port=config.port or 5672,
|
|
37
36
|
username=config.username,
|
|
38
37
|
password=config.password,
|
|
39
|
-
|
|
40
|
-
find_wildcard_handler=find_wildcard_handler,
|
|
38
|
+
control_plane_manager=control_plane_manager,
|
|
41
39
|
is_root=config.is_root,
|
|
42
40
|
)
|
|
43
41
|
|
|
@@ -49,8 +47,7 @@ def _create_control_provider(
|
|
|
49
47
|
port=config.port or 1883,
|
|
50
48
|
username=config.username,
|
|
51
49
|
password=config.password,
|
|
52
|
-
|
|
53
|
-
find_wildcard_handler=find_wildcard_handler,
|
|
50
|
+
control_plane_manager=control_plane_manager,
|
|
54
51
|
)
|
|
55
52
|
|
|
56
53
|
|
|
@@ -68,10 +65,7 @@ class ControlPlaneManager:
|
|
|
68
65
|
queue_name_generator should be a hardcoded value for Core Services, the SDK should provide its own function to generate queue names.
|
|
69
66
|
"""
|
|
70
67
|
self._control_providers = [
|
|
71
|
-
_create_control_provider(
|
|
72
|
-
config, self.get_subscription_channels, self.get_wildcard_topic_handler
|
|
73
|
-
)
|
|
74
|
-
for config in control_configs
|
|
68
|
+
_create_control_provider(config, self) for config in control_configs
|
|
75
69
|
]
|
|
76
70
|
|
|
77
71
|
# flag which indicates if we SHOULD be connected.
|
|
@@ -121,7 +115,7 @@ class ControlPlaneManager:
|
|
|
121
115
|
topic_handler.queue_name = queue_name
|
|
122
116
|
if self.is_connected():
|
|
123
117
|
for provider in self._control_providers:
|
|
124
|
-
provider.subscribe(channel, persist)
|
|
118
|
+
provider.subscribe(channel, persist, queue_name)
|
|
125
119
|
|
|
126
120
|
def remove_subscription_channel(self, channel: str) -> bool:
|
|
127
121
|
"""Stop subscribing to a channel on all configured brokers.
|
|
@@ -142,29 +136,47 @@ class ControlPlaneManager:
|
|
|
142
136
|
provider.unsubscribe(channel)
|
|
143
137
|
return True
|
|
144
138
|
|
|
145
|
-
def
|
|
146
|
-
"""Get
|
|
139
|
+
def get_non_wildcard_subscription_channels(self) -> dict[str, TopicHandler]:
|
|
140
|
+
"""Get primary subscription channels.
|
|
147
141
|
|
|
148
|
-
These channels cannot be wildcards, and incoming topics must match the channel topics exactly.
|
|
142
|
+
These channels cannot be wildcards, and incoming topics must match the channel topics exactly.
|
|
143
|
+
|
|
144
|
+
This function is safe to call from the broker clients, as it does not mutate state.
|
|
149
145
|
|
|
150
146
|
Returns:
|
|
151
147
|
the dictionary of topics to topic information
|
|
152
148
|
"""
|
|
153
149
|
return self._topics_to_handlers
|
|
154
150
|
|
|
155
|
-
def
|
|
151
|
+
def get_wildcard_topic_and_topic_handler(self, topic: str) -> tuple[str, TopicHandler] | None:
|
|
156
152
|
"""Get the wildcard topic handler for a given topic, if it exists.
|
|
157
153
|
|
|
158
154
|
This is an inefficient lookup, so should only be used if we fail to find a non-wildcard match for an incoming topic. Note that this function gets accessed as a callback from the direct broker implementations.
|
|
159
155
|
|
|
156
|
+
This function is safe to call from the broker clients, as it does not mutate state.
|
|
157
|
+
|
|
158
|
+
Params:
|
|
159
|
+
topic: the topic for an incoming message (this will not have any wildcards in it)
|
|
160
|
+
|
|
160
161
|
Returns:
|
|
161
|
-
the
|
|
162
|
+
if a match found: the wildcard string configured application-side to match the topic parameter, and the topic handler associated with the topic
|
|
163
|
+
if no match found: None
|
|
162
164
|
"""
|
|
163
|
-
for topic_handler in self._wildcards.
|
|
165
|
+
for wildcard, topic_handler in self._wildcards.items():
|
|
164
166
|
if topic_handler.does_topic_match(topic):
|
|
165
|
-
return topic_handler
|
|
167
|
+
return wildcard, topic_handler
|
|
166
168
|
return None
|
|
167
169
|
|
|
170
|
+
def get_all_subscription_channels(self) -> itertools.chain[tuple[str, TopicHandler]]:
|
|
171
|
+
"""Get all subscription channels, including wildcard channels.
|
|
172
|
+
|
|
173
|
+
This function is safe to call from the broker clients, as it does not mutate state.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
an iterator which yields tuples of (channel, topic handler) for all channels, including wildcards. Note that wildcard channels will be returned in no particular order, and may be interspersed with non-wildcard channels.
|
|
177
|
+
"""
|
|
178
|
+
return itertools.chain(self._topics_to_handlers.items(), self._wildcards.items())
|
|
179
|
+
|
|
168
180
|
def connect(self) -> None:
|
|
169
181
|
"""Connect to all configured brokers.
|
|
170
182
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""This module contains the binary specification for an INTERSECT message IF the protocol does not provide any built-in header support. If it does, use the built-in functionality of the protocol.
|
|
2
|
+
|
|
3
|
+
Some protocols do not have any built-in header support, leaving it up to us to define the binary structure of the message. We want to avoid "chunking" messages into multiple parts, so a message should be guaranteed to include all metadata.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from intersect_sdk_common import IntersectApplicationError
|
|
7
|
+
|
|
8
|
+
_HEADER_KV_SEPARATOR = b'\x02'
|
|
9
|
+
"""Indicates end of a header key and start of a header value.
|
|
10
|
+
|
|
11
|
+
Used because applications should generally assign no special meaning to this byte, and this byte has no reason to appear in header keys or values.
|
|
12
|
+
"""
|
|
13
|
+
_HEADER_VALUE_SEPARATOR = b'\x03'
|
|
14
|
+
"""Indicates end of a header value and start of the next header key.
|
|
15
|
+
|
|
16
|
+
Used because applications should generally assign no special meaning to this byte, and this byte has no reason to appear in header keys or values.
|
|
17
|
+
"""
|
|
18
|
+
_PAYLOAD_SEPARATOR = b'\x01'
|
|
19
|
+
"""Indicates end of headers and start of the payload.
|
|
20
|
+
|
|
21
|
+
Used because applications should generally assign no special meaning to this byte, and this byte has no reason to appear in header keys or values.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_TOTAL_REASONABLE_HEADER_BYTES = 131072
|
|
25
|
+
"""The total number of bytes that should be reasonably expected to be used for header keys, values, and header separators combined. This provides applications with some level of DOS protection.
|
|
26
|
+
|
|
27
|
+
It is rare in practice for ANY application to use this number of bytes for the total amount of headers, for example http2_max_header_size in NGINX is rarely set above 128KB
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
CONTENT_TYPE_HEADER_KEY = 'content_type'
|
|
31
|
+
CONTENT_TYPE_HEADER_KEY_BYTES = CONTENT_TYPE_HEADER_KEY.encode()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_binary_message(body: bytes, content_type: str, headers: dict[str, str]) -> bytes:
|
|
35
|
+
"""Create a binary message from the body, headers, and content type."""
|
|
36
|
+
return b''.join(
|
|
37
|
+
[
|
|
38
|
+
# content-type 'header' first (this is generally handled separately from other headers in many protocols)
|
|
39
|
+
CONTENT_TYPE_HEADER_KEY_BYTES,
|
|
40
|
+
_HEADER_KV_SEPARATOR,
|
|
41
|
+
content_type.encode(),
|
|
42
|
+
_HEADER_VALUE_SEPARATOR if len(headers) else b'',
|
|
43
|
+
# headers
|
|
44
|
+
_HEADER_VALUE_SEPARATOR.join(
|
|
45
|
+
_HEADER_KV_SEPARATOR.join([key.encode(), value.encode()])
|
|
46
|
+
for key, value in headers.items()
|
|
47
|
+
),
|
|
48
|
+
# end of headers
|
|
49
|
+
_PAYLOAD_SEPARATOR,
|
|
50
|
+
# body
|
|
51
|
+
body,
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_binary_message(message: bytes) -> tuple[bytes, str, dict[str, str]]:
|
|
57
|
+
"""Parse a binary message into its body, content type, and headers."""
|
|
58
|
+
# IMPORTANT!!! ----- Total length of header keys and values combined should be limited to first several bytes, terminate header search early if headers aren't a reasonable length.
|
|
59
|
+
payload_sep_location = message.find(_PAYLOAD_SEPARATOR, 0, _TOTAL_REASONABLE_HEADER_BYTES)
|
|
60
|
+
if payload_sep_location == -1:
|
|
61
|
+
msg = 'Probable malformed message: no payload separator found in first expected bytes, .'
|
|
62
|
+
raise IntersectApplicationError(msg)
|
|
63
|
+
header_string = message[:payload_sep_location]
|
|
64
|
+
headers = {
|
|
65
|
+
key.decode(): value.decode()
|
|
66
|
+
for key, value in (
|
|
67
|
+
header.split(_HEADER_KV_SEPARATOR)
|
|
68
|
+
for header in header_string.split(_HEADER_VALUE_SEPARATOR)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
try:
|
|
72
|
+
content_type = headers.pop(CONTENT_TYPE_HEADER_KEY)
|
|
73
|
+
except KeyError as e:
|
|
74
|
+
msg = 'Probable malformed message: no content_type header found in message, discarding it.'
|
|
75
|
+
raise IntersectApplicationError(msg) from e
|
|
76
|
+
|
|
77
|
+
return message[payload_sep_location + 1 :], content_type, headers
|
|
@@ -46,8 +46,8 @@ class TopicHandler:
|
|
|
46
46
|
# match any sequence of 0 or more words (make sure to allow for the separator)
|
|
47
47
|
regex_builder.append('[a-zA-Z0-9/-]*')
|
|
48
48
|
elif char == '/':
|
|
49
|
-
# in-memory topics use '/' as the separator,
|
|
50
|
-
regex_builder.append('
|
|
49
|
+
# in-memory topics use '/' as the separator, it is the protocol handler's responsibility to convert topics to use '/' as the separator
|
|
50
|
+
regex_builder.append('/')
|
|
51
51
|
else:
|
|
52
52
|
# ordinary character
|
|
53
53
|
regex_builder.append(char)
|
{intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/__init__.py
RENAMED
|
File without changes
|
{intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/config.py
RENAMED
|
File without changes
|
{intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/exceptions.py
RENAMED
|
File without changes
|
{intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/logger.py
RENAMED
|
File without changes
|
|
File without changes
|
{intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.4}/src/intersect_sdk_common/version.py
RENAMED
|
File without changes
|