intersect-sdk-common 0.9.2__tar.gz → 0.9.3__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 (28) hide show
  1. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/PKG-INFO +1 -1
  2. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/pyproject.toml +1 -1
  3. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/control_plane/brokers/amqp_client.py +32 -27
  4. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/control_plane/brokers/mqtt_client.py +16 -13
  5. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/control_plane/control_plane_manager.py +30 -18
  6. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/control_plane/topic_handler.py +2 -2
  7. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/README.md +0 -0
  8. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/__init__.py +0 -0
  9. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/config.py +0 -0
  10. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/constants.py +0 -0
  11. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/control_plane/__init__.py +0 -0
  12. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/control_plane/brokers/__init__.py +0 -0
  13. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/control_plane/brokers/broker_client.py +0 -0
  14. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/control_plane/definitions.py +0 -0
  15. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/control_plane/messages/__init__.py +0 -0
  16. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/control_plane/messages/event.py +0 -0
  17. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/control_plane/messages/lifecycle.py +0 -0
  18. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/control_plane/messages/userspace.py +0 -0
  19. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/core_definitions.py +0 -0
  20. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/data_plane/__init__.py +0 -0
  21. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/data_plane/data_plane_manager.py +0 -0
  22. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/data_plane/minio_utils.py +0 -0
  23. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/exceptions.py +0 -0
  24. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/logger.py +0 -0
  25. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/py.typed +0 -0
  26. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/utils/__init__.py +0 -0
  27. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/src/intersect_sdk_common/utils/multi_flag_thread_event.py +0 -0
  28. {intersect_sdk_common-0.9.2 → intersect_sdk_common-0.9.3}/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.2
3
+ Version: 0.9.3
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
@@ -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.2"
21
+ version = "0.9.3"
22
22
  classifiers = [
23
23
  "Programming Language :: Python :: 3",
24
24
  "Programming Language :: Python :: 3.10",
@@ -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
- topics_to_handlers: Callable[[], dict[str, TopicHandler]],
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
- topics_to_handlers: callback function which gets the topic to handler map from the channel manager
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._topics_to_handlers = topics_to_handlers
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
@@ -390,7 +386,7 @@ class AMQPClient(BrokerClient):
390
386
  def _on_input_channel_open(self, channel: Channel) -> None:
391
387
  channel_num = 1
392
388
  self._channel_in = channel
393
- # consumer channel flag can be set immediately
389
+ # 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
390
  self._channel_flags.set_nth_flag(channel_num)
395
391
  cb_1 = functools.partial(self._on_channel_closed, channel_num=channel_num)
396
392
  self._channel_in.add_on_close_callback(cb_1)
@@ -403,17 +399,7 @@ class AMQPClient(BrokerClient):
403
399
  callback=cb_2,
404
400
  )
405
401
  else:
406
- for topic, topic_handler in self._topics_to_handlers().items():
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)
402
+ self._create_all_queues(channel)
417
403
 
418
404
  def _on_exchange_declareok(self, _unused_frame: Frame, channel: Channel) -> None:
419
405
  """Create a queue on the broker (called from AMQP).
@@ -425,7 +411,17 @@ class AMQPClient(BrokerClient):
425
411
  _unused_frame: response from declaring the exchange on the broker (irrelevant).
426
412
  channel: The Channel being instantiated.
427
413
  """
428
- for topic, topic_handler in self._topics_to_handlers().items():
414
+ self._create_all_queues(channel)
415
+
416
+ def _create_all_queues(self, channel: Channel) -> None:
417
+ """Create queues for all subscription channels on the broker.
418
+
419
+ This should only be called once we've verified that the exchange exists.
420
+
421
+ Args:
422
+ channel: The Channel being instantiated.
423
+ """
424
+ for topic, topic_handler in self._control_plane_manager.get_all_subscription_channels():
429
425
  amqp_topic = _hierarchy_2_amqp(topic)
430
426
  cb = functools.partial(
431
427
  self._create_queue,
@@ -456,6 +452,7 @@ class AMQPClient(BrokerClient):
456
452
  queue=queue_name
457
453
  if persist
458
454
  else '', # if we're transient, let the broker generate a name for us
455
+ passive=not self._is_root,
459
456
  durable=persist,
460
457
  exclusive=not persist, # transient queues can be exclusive
461
458
  callback=cb,
@@ -597,17 +594,25 @@ class AMQPClient(BrokerClient):
597
594
  channel.basic_ack(basic_deliver.delivery_tag)
598
595
  return
599
596
 
600
- tth_key = _amqp_2_hierarchy(basic_deliver.routing_key)
601
- topic_handler = self._topics_to_handlers().get(tth_key)
597
+ amqp_routing_key_rep = basic_deliver.routing_key
598
+ manager_routing_key_rep = _amqp_2_hierarchy(basic_deliver.routing_key)
599
+ topic_handler = self._control_plane_manager.get_non_wildcard_subscription_channels().get(
600
+ manager_routing_key_rep
601
+ )
602
602
  if not topic_handler:
603
603
  # we may have included the topic in one of our wildcard handlers
604
- topic_handler = self._find_wildcard_handler(tth_key)
604
+ wildcard_result = self._control_plane_manager.get_wildcard_topic_and_topic_handler(
605
+ manager_routing_key_rep
606
+ )
607
+ if wildcard_result:
608
+ manager_routing_key_rep, topic_handler = wildcard_result
609
+ amqp_routing_key_rep = _hierarchy_2_amqp(manager_routing_key_rep)
605
610
  if topic_handler:
606
- consumer_tag_info = self._topics_to_consumer_tags.get(basic_deliver.routing_key)
611
+ consumer_tag_info = self._topics_to_consumer_tags.get(amqp_routing_key_rep)
607
612
  if not consumer_tag_info:
608
613
  logger.error(
609
614
  'Could not fetch consumer tag for topic %s, please inform an INTERSECT-SDK developer that you saw this message',
610
- tth_key,
615
+ amqp_routing_key_rep,
611
616
  )
612
617
  return
613
618
  while not consumer_tag_info.consumer_tag_obtained():
@@ -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 ..topic_handler import TopicHandler
28
+ from ..control_plane_manager import ControlPlaneManager
31
29
 
32
30
 
33
31
  _MQTT_MAX_RETRIES = 10
@@ -37,7 +35,8 @@ _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('#', '+')
39
+ return hierarchy.replace('*', '+')
41
40
 
42
41
 
43
42
  class MQTTClient(BrokerClient):
@@ -61,8 +60,7 @@ class MQTTClient(BrokerClient):
61
60
  port: int,
62
61
  username: str,
63
62
  password: str,
64
- topics_to_handlers: Callable[[], dict[str, TopicHandler]],
65
- find_wildcard_handler: Callable[[str], TopicHandler | None],
63
+ control_plane_manager: ControlPlaneManager,
66
64
  uid: str | None = None,
67
65
  ) -> None:
68
66
  """The default constructor.
@@ -72,8 +70,7 @@ class MQTTClient(BrokerClient):
72
70
  port: port number of MQTT broker
73
71
  username: username credentials for MQTT broker
74
72
  password: password credentials for MQTT broker
75
- topics_to_handlers: callback function which gets the topic to handler map from the channel manager
76
- find_wildcard_handler: callback function which gets the wildcard topic handler from the channel manager
73
+ control_plane_manager: reference to the ControlPlaneManager instance, remember to ONLY use functions which do not mutate state
77
74
  uid: A string representing the unique id to identify the client.
78
75
  """
79
76
  # Unique id for the MQTT broker to associate this client with
@@ -97,8 +94,7 @@ class MQTTClient(BrokerClient):
97
94
  self._connected_flag = threading.Event()
98
95
 
99
96
  # ConnectionManager callable state
100
- self._topics_to_handlers = topics_to_handlers
101
- self._find_wildcard_handler = find_wildcard_handler
97
+ self._control_plane_manager = control_plane_manager
102
98
 
103
99
  # MQTT v3.1.1 automatically downgrades a QOS which is too high (good), but MQTT v5 will terminate the connection (bad)
104
100
  # see https://github.com/rabbitmq/rabbitmq-server/discussions/11842
@@ -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
- topic_handler = self._topics_to_handlers().get(message.topic)
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
- topic_handler = self._find_wildcard_handler(message.topic)
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,7 +301,7 @@ 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._topics_to_handlers().items():
304
+ for topic, topic_handler in self._control_plane_manager.get_all_subscription_channels():
302
305
  self.subscribe(topic, topic_handler.topic_persist)
303
306
  else:
304
307
  # This will generally suggest a misconfiguration
@@ -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
- topic_handler_callback: Callable[[], dict[str, TopicHandler]],
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
- topics_to_handlers=topic_handler_callback,
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
- topics_to_handlers=topic_handler_callback,
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.
@@ -142,29 +136,47 @@ class ControlPlaneManager:
142
136
  provider.unsubscribe(channel)
143
137
  return True
144
138
 
145
- def get_subscription_channels(self) -> dict[str, TopicHandler]:
146
- """Get the subscription channels.
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. Note that this function gets accessed as a callback from the direct broker implementations.
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 get_wildcard_topic_handler(self, topic: str) -> TopicHandler | None:
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 topic handler associated with the topic, if it exists; None otherwise
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.values():
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
 
@@ -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, but on the broker '.' is used
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)