switch-api 0.6.17__tar.gz → 0.6.19__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 (78) hide show
  1. {switch_api-0.6.17 → switch_api-0.6.19}/HISTORY.md +14 -0
  2. {switch_api-0.6.17 → switch_api-0.6.19}/PKG-INFO +38 -6
  3. switch_api-0.6.19/pyproject.toml +6 -0
  4. {switch_api-0.6.17 → switch_api-0.6.19}/setup.py +2 -2
  5. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/__init__.py +4 -2
  6. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/realtime/__init__.py +2 -1
  7. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/realtime/_mqtt.py +93 -13
  8. switch_api-0.6.19/switch_api/realtime/_redis.py +429 -0
  9. switch_api-0.6.19/switch_api/realtime/realtime.py +397 -0
  10. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api.egg-info/PKG-INFO +39 -7
  11. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api.egg-info/SOURCES.txt +7 -1
  12. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api.egg-info/requires.txt +1 -0
  13. switch_api-0.6.19/tests/test_dataset.py +7 -0
  14. switch_api-0.6.19/tests/test_error_handlers.py +196 -0
  15. switch_api-0.6.19/tests/test_initialize.py +5 -0
  16. switch_api-0.6.19/tests/test_integration.py +37 -0
  17. switch_api-0.6.19/tests/test_pipeline.py +6 -0
  18. switch_api-0.6.17/pyproject.toml +0 -6
  19. switch_api-0.6.17/switch_api/realtime/realtime.py +0 -235
  20. {switch_api-0.6.17 → switch_api-0.6.19}/AUTHORS.rst +0 -0
  21. {switch_api-0.6.17 → switch_api-0.6.19}/LICENCE +0 -0
  22. {switch_api-0.6.17 → switch_api-0.6.19}/MANIFEST.in +0 -0
  23. {switch_api-0.6.17 → switch_api-0.6.19}/README.md +0 -0
  24. {switch_api-0.6.17 → switch_api-0.6.19}/setup.cfg +0 -0
  25. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_authentication/__init__.py +0 -0
  26. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_authentication/_authentication.py +0 -0
  27. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_authentication/_credentials_store/__init__.py +0 -0
  28. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_authentication/_credentials_store/_credentials_store.py +0 -0
  29. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_authentication/_msal/__init__.py +0 -0
  30. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_authentication/_msal/_custom_application.py +0 -0
  31. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_compute/__init__.py +0 -0
  32. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_compute/_commands.py +0 -0
  33. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_compute/_compute.py +0 -0
  34. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_compute/_iotservice.py +0 -0
  35. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_compute/_models.py +0 -0
  36. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_compute/_utils.py +0 -0
  37. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_guide/__init__.py +0 -0
  38. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_guide/main.py +0 -0
  39. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_guide/processor.py +0 -0
  40. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_utils/__init__.py +0 -0
  41. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_utils/_constants.py +0 -0
  42. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_utils/_marketplace.py +0 -0
  43. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_utils/_mqtt.py +0 -0
  44. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_utils/_platform.py +0 -0
  45. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/_utils/_utils.py +0 -0
  46. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/analytics/__init__.py +0 -0
  47. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/analytics/analytics.py +0 -0
  48. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/cache/__init__.py +0 -0
  49. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/cache/cache.py +0 -0
  50. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/controls/__init__.py +0 -0
  51. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/controls/_constants.py +0 -0
  52. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/controls/_enums.py +0 -0
  53. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/controls/_mqtt.py +0 -0
  54. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/controls/controls.py +0 -0
  55. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/dataset/__init__.py +0 -0
  56. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/dataset/dataset.py +0 -0
  57. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/email/__init__.py +0 -0
  58. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/email/email_sender.py +0 -0
  59. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/error_handlers/__init__.py +0 -0
  60. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/error_handlers/error_handlers.py +0 -0
  61. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/extensions/__init__.py +0 -0
  62. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/extensions/extensions.py +0 -0
  63. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/extensions/field_meta.py +0 -0
  64. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/extensions/helpers.py +0 -0
  65. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/extensions/pipeline.py +0 -0
  66. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/initialize.py +0 -0
  67. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/integration/__init__.py +0 -0
  68. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/integration/_utils.py +0 -0
  69. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/integration/helpers.py +0 -0
  70. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/integration/integration.py +0 -0
  71. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/pipeline/__init__.py +0 -0
  72. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/pipeline/automation.py +0 -0
  73. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/pipeline/definitions.py +0 -0
  74. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/pipeline/pipeline.py +0 -0
  75. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/platform_insights/__init__.py +0 -0
  76. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api/platform_insights/platform_insights.py +0 -0
  77. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api.egg-info/dependency_links.txt +0 -0
  78. {switch_api-0.6.17 → switch_api-0.6.19}/switch_api.egg-info/top_level.txt +0 -0
@@ -1,5 +1,19 @@
1
1
  # History
2
2
 
3
+ #0.6.18
4
+ ### Changed
5
+ In the `realtime` module:
6
+ - Updated `submit_realtime_updates()` to use the passed `timeout` value when waiting for a retained MQTT message.
7
+ - Added Redis-backed optimistic concurrency support for realtime dataset updates using WATCH/MULTI/EXEC.
8
+ - Added `set_realtime_redis_config()` to configure Redis as the authoritative state store before publishing updates to MQTT.
9
+ - Added `redis` package dependency for Redis-backed realtime state updates.
10
+ - Added a maximum of 3 retained-message subscription attempts for connection or subscribe failures in the MQTT fallback path.
11
+ - Preserved new-topic behavior: when no retained message exists and Redis is not configured, the provided dataframe is used and published as the initial retained dataset.
12
+ - Updated `subscribe_with_retained_sync()` to apply the passed timeout to the MQTT connection, check subscribe failures, and log when no retained topics are received within the wait period.
13
+ - Added Redis timeout retry handling with a default maximum of 3 attempts for Redis realtime update operations.
14
+ - Added MQTT publish reconnect/retry handling for transient `MQTT_ERR_NO_CONN` publish failures.
15
+
16
+
3
17
  #0.6.17
4
18
  ### Changed
5
19
  In the `integration` module:
@@ -1,11 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: switch_api
3
- Version: 0.6.17
3
+ Version: 0.6.19
4
4
  Summary: A complete package for data ingestion into the Switch Automation Platform.
5
- Home-page: UNKNOWN
6
5
  Author: Switch Automation Pty Ltd.
7
6
  License: MIT License
8
- Platform: UNKNOWN
9
7
  Classifier: Development Status :: 2 - Pre-Alpha
10
8
  Classifier: License :: OSI Approved :: MIT License
11
9
  Classifier: Intended Audience :: Other Audience
@@ -17,6 +15,28 @@ Requires-Python: >=3.8.0
17
15
  Description-Content-Type: text/markdown
18
16
  License-File: LICENCE
19
17
  License-File: AUTHORS.rst
18
+ Requires-Dist: pandas==1.5.3
19
+ Requires-Dist: requests
20
+ Requires-Dist: azure-storage-blob
21
+ Requires-Dist: pandera[io]==0.7.1
22
+ Requires-Dist: azure-servicebus
23
+ Requires-Dist: msal>=1.11.0
24
+ Requires-Dist: paho-mqtt==1.6.1
25
+ Requires-Dist: uvicorn==0.22.0
26
+ Requires-Dist: fastapi==0.98.0
27
+ Requires-Dist: pyodbc==4.0.39
28
+ Requires-Dist: cachetools==5.5.0
29
+ Requires-Dist: numexpr==2.10.2
30
+ Requires-Dist: redis<6,>=4.5.5
31
+ Dynamic: author
32
+ Dynamic: classifier
33
+ Dynamic: description
34
+ Dynamic: description-content-type
35
+ Dynamic: license
36
+ Dynamic: license-file
37
+ Dynamic: requires-dist
38
+ Dynamic: requires-python
39
+ Dynamic: summary
20
40
 
21
41
  # Switch Automation library for Python
22
42
  This is a package for data ingestion into the Switch Automation software platform.
@@ -38,6 +58,20 @@ pip install switch_api
38
58
 
39
59
  # History
40
60
 
61
+ #0.6.18
62
+ ### Changed
63
+ In the `realtime` module:
64
+ - Updated `submit_realtime_updates()` to use the passed `timeout` value when waiting for a retained MQTT message.
65
+ - Added Redis-backed optimistic concurrency support for realtime dataset updates using WATCH/MULTI/EXEC.
66
+ - Added `set_realtime_redis_config()` to configure Redis as the authoritative state store before publishing updates to MQTT.
67
+ - Added `redis` package dependency for Redis-backed realtime state updates.
68
+ - Added a maximum of 3 retained-message subscription attempts for connection or subscribe failures in the MQTT fallback path.
69
+ - Preserved new-topic behavior: when no retained message exists and Redis is not configured, the provided dataframe is used and published as the initial retained dataset.
70
+ - Updated `subscribe_with_retained_sync()` to apply the passed timeout to the MQTT connection, check subscribe failures, and log when no retained topics are received within the wait period.
71
+ - Added Redis timeout retry handling with a default maximum of 3 attempts for Redis realtime update operations.
72
+ - Added MQTT publish reconnect/retry handling for transient `MQTT_ERR_NO_CONN` publish failures.
73
+
74
+
41
75
  #0.6.17
42
76
  ### Changed
43
77
  In the `integration` module:
@@ -1196,5 +1230,3 @@ Removed the `name_as_filename` and `treat_as_timeseries` parameter from the foll
1196
1230
  - `switch.integration.replace_data()`
1197
1231
  - `switch.integration.append_data()`
1198
1232
  - `switch.integration.upload_data()`
1199
-
1200
-
@@ -0,0 +1,6 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=64",
4
+ "wheel"
5
+ ]
6
+ build-backend = "setuptools.build_meta"
@@ -16,11 +16,11 @@ setup(
16
16
  long_description_content_type='text/markdown',
17
17
  license='MIT License',
18
18
  name="switch_api",
19
- version="0.6.17",
19
+ version="0.6.19",
20
20
  packages=find_packages(include=["switch_api", "switch_api.*"]),
21
21
  install_requires=['pandas==1.5.3', 'requests', 'azure-storage-blob', 'pandera[io]==0.7.1', 'azure-servicebus',
22
22
  'msal>=1.11.0', 'paho-mqtt==1.6.1', 'uvicorn==0.22.0', 'fastapi==0.98.0', 'pyodbc==4.0.39', 'cachetools==5.5.0',
23
- 'numexpr==2.10.2'],
23
+ 'numexpr==2.10.2', 'redis>=4.5.5,<6'],
24
24
  python_requires=">=3.8.0",
25
25
  classifiers=[
26
26
  'Development Status :: 2 - Pre-Alpha',
@@ -10,7 +10,8 @@ Data Ingestion into the Switch Automation
10
10
  Complete package for ingestion data into the Switch Automation Platform.
11
11
  """
12
12
  __all__ = ['dataset', 'analytics', 'pipeline', 'error_handlers', 'integration',
13
- 'initialize', 'cache', 'extensions', 'email', 'controls', 'realtime', '_compute', 'generate_filepath', 'set_mqtt_broker_config']
13
+ 'initialize', 'cache', 'extensions', 'email', 'controls', 'realtime', '_compute', 'generate_filepath',
14
+ 'set_mqtt_broker_config', 'set_realtime_redis_config']
14
15
 
15
16
  from .initialize import initialize
16
17
  from . import dataset
@@ -27,6 +28,7 @@ from . import realtime
27
28
  from . import _compute
28
29
  from ._utils._utils import generate_filepath
29
30
  from ._utils._mqtt import set_mqtt_broker_config
31
+ from .realtime import set_realtime_redis_config
30
32
  from . import _guide
31
33
 
32
34
 
@@ -42,4 +44,4 @@ from . import _guide
42
44
  # log.addHandler(_ch)
43
45
  # https://dev.to/joaomcteixeira/setting-up-python-logging-for-a-library-app-6ml
44
46
 
45
- __version__ = "0.6.17"
47
+ __version__ = "0.6.19"
@@ -8,5 +8,6 @@ A module for sending control request of sensors.
8
8
  """
9
9
 
10
10
  from .realtime import submit_realtime_updates
11
+ from ._redis import set_realtime_redis_config
11
12
 
12
- __all__ = ['submit_realtime_updates']
13
+ __all__ = ['submit_realtime_updates', 'set_realtime_redis_config']
@@ -7,10 +7,10 @@
7
7
  A module containing the SwithRealtime class and associated methods referenced by the controls module in the package. This
8
8
  module is not directly referenced by end users.
9
9
  """
10
- import json
11
10
  import logging
12
11
  import sys
13
12
  import threading
13
+ import time
14
14
  from typing import Iterable
15
15
  from urllib.parse import urlparse
16
16
  import paho.mqtt.client as mqtt
@@ -68,6 +68,7 @@ class SwitchRealtime:
68
68
 
69
69
  self._connected = False
70
70
  self._connect_event = threading.Event()
71
+ self._loop_started = False
71
72
 
72
73
  # Store all topic states here
73
74
  self.topic_state = {}
@@ -94,8 +95,13 @@ class SwitchRealtime:
94
95
  logger.info(f'Attempting connection to MQTT broker with client_id={self.client_id} on host={self.host} port={self.port}')
95
96
  logger.info(f'hostname={url.hostname}')
96
97
 
97
- self.client.connect(host=url.hostname, port=self.port)
98
- self.client.loop_start() # Run network loop in background thread
98
+ if self._loop_started:
99
+ logger.info("Reconnecting existing MQTT client network loop.")
100
+ self.client.reconnect()
101
+ else:
102
+ self.client.connect(host=url.hostname, port=self.port)
103
+ self.client.loop_start() # Run network loop in background thread
104
+ self._loop_started = True
99
105
 
100
106
  self._connected = self._connect_event.wait(timeout=timeout)
101
107
 
@@ -109,8 +115,6 @@ class SwitchRealtime:
109
115
  if topics is None:
110
116
  raise ValueError("topics cannot be None")
111
117
 
112
- self.connect()
113
-
114
118
  topics = list(topics)
115
119
 
116
120
  if not topics:
@@ -122,6 +126,11 @@ class SwitchRealtime:
122
126
  if not isinstance(timeout, int) or timeout <= 0:
123
127
  raise ValueError("timeout must be a positive integer")
124
128
 
129
+ if not self.connect(timeout=timeout):
130
+ raise TimeoutError(
131
+ f"Could not connect to MQTT Broker within {timeout} seconds."
132
+ )
133
+
125
134
  # --- Initialize tracking ---
126
135
  self._expected_topics = set(topics)
127
136
  self._received_retained = set()
@@ -129,29 +138,100 @@ class SwitchRealtime:
129
138
 
130
139
  # --- Subscribe ---
131
140
  for topic in topics:
132
- self.client.subscribe(topic)
141
+ result, _ = self.client.subscribe(topic)
142
+ if result != mqtt.MQTT_ERR_SUCCESS:
143
+ raise RuntimeError(
144
+ f"Failed to subscribe to {topic}. MQTT result code: {result}"
145
+ )
133
146
  logger.info(f"Subscribed to = {topic}")
134
147
 
135
148
  # --- Wait until all retained received OR timeout ---
136
- self._retained_event.wait(timeout=timeout)
149
+ received_all = self._retained_event.wait(timeout=timeout)
137
150
 
138
151
  # --- Return only retained dataset ---
139
- return {
152
+ retained_dataset = {
140
153
  topic: self.topic_state[topic]["payload"]
141
154
  for topic in self._received_retained
142
155
  if topic in self.topic_state
143
156
  }
144
157
 
158
+ if not received_all:
159
+ missing_topics = self._expected_topics - self._received_retained
160
+ logger.info(
161
+ f"No retained messages received within {timeout} seconds "
162
+ f"for topics: {sorted(missing_topics)}"
163
+ )
164
+
165
+ return retained_dataset
145
166
 
146
- def publish(self, topic: str, payload: str, qos: int = 1, retain: bool = True):
167
+
168
+ def publish(
169
+ self,
170
+ topic: str,
171
+ payload: str,
172
+ qos: int = 1,
173
+ retain: bool = True,
174
+ timeout: int = WS_MQTT_CONNECTION_TIMEOUT,
175
+ max_retries: int = 3,
176
+ ):
147
177
  """
148
178
  Publish message to broker.
149
179
  Set retain=True to store as last known state.
150
180
  """
151
- result = self.client.publish(topic, payload, qos=qos, retain=retain)
181
+ if not isinstance(max_retries, int) or max_retries < 1:
182
+ raise ValueError("max_retries must be a positive integer.")
183
+
184
+ last_error = None
185
+
186
+ for attempt in range(1, max_retries + 1):
187
+ if not self._connected:
188
+ logger.warning(
189
+ f"MQTT client is not connected before publish to {topic}. "
190
+ f"Reconnect attempt {attempt}/{max_retries}."
191
+ )
192
+ reconnect_error = None
193
+ try:
194
+ connected = self.connect(timeout=timeout)
195
+ except Exception as e:
196
+ connected = False
197
+ reconnect_error = f"Could not reconnect to MQTT Broker: {e}"
198
+ logger.error(reconnect_error)
199
+
200
+ if not connected:
201
+ last_error = reconnect_error or "Could not reconnect to MQTT Broker."
202
+ logger.error(last_error)
203
+ time.sleep(1)
204
+ continue
205
+
206
+ result = self.client.publish(topic, payload, qos=qos, retain=retain)
207
+
208
+ if result.rc == mqtt.MQTT_ERR_SUCCESS:
209
+ logger.info(
210
+ f"MQTT publish accepted for topic={topic}, "
211
+ f"mid={result.mid}, attempt={attempt}/{max_retries}"
212
+ )
213
+ return result
214
+
215
+ last_error = (
216
+ f"Failed to publish to {topic}. MQTT result code: {result.rc}"
217
+ )
218
+ logger.error(last_error)
219
+
220
+ if result.rc != mqtt.MQTT_ERR_NO_CONN or attempt >= max_retries:
221
+ break
222
+
223
+ self._connected = False
224
+ logger.warning(
225
+ f"MQTT publish failed because client is not connected. "
226
+ f"Retrying publish to {topic} after reconnect "
227
+ f"(attempt {attempt + 1}/{max_retries})."
228
+ )
229
+ time.sleep(1)
230
+
231
+ message = last_error or f"Failed to publish to {topic}."
232
+ logger.error(message)
233
+ raise RuntimeError(message)
152
234
 
153
- if result.rc != mqtt.MQTT_ERR_SUCCESS:
154
- logger.error(f"Failed to publish to {topic} : {json.loads(payload)}")
155
235
 
156
236
 
157
237
 
@@ -202,4 +282,4 @@ class SwitchRealtime:
202
282
  self._retained_event.set()
203
283
 
204
284
  else:
205
- logger.info(f'MQTT Message - {topic} : {payload}')
285
+ logger.info(f'MQTT Message - {topic} : {payload}')