switch-api 0.6.14__tar.gz → 0.6.16__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 (70) hide show
  1. {switch_api-0.6.14 → switch_api-0.6.16}/HISTORY.md +20 -0
  2. {switch_api-0.6.14 → switch_api-0.6.16}/PKG-INFO +21 -1
  3. {switch_api-0.6.14 → switch_api-0.6.16}/setup.py +1 -1
  4. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/__init__.py +1 -1
  5. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_utils/_constants.py +8 -6
  6. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_utils/_utils.py +10 -0
  7. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/controls/controls.py +114 -44
  8. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/pipeline/__init__.py +2 -2
  9. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/pipeline/automation.py +338 -4
  10. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/pipeline/pipeline.py +10 -0
  11. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api.egg-info/PKG-INFO +21 -1
  12. {switch_api-0.6.14 → switch_api-0.6.16}/AUTHORS.rst +0 -0
  13. {switch_api-0.6.14 → switch_api-0.6.16}/LICENCE +0 -0
  14. {switch_api-0.6.14 → switch_api-0.6.16}/MANIFEST.in +0 -0
  15. {switch_api-0.6.14 → switch_api-0.6.16}/README.md +0 -0
  16. {switch_api-0.6.14 → switch_api-0.6.16}/pyproject.toml +0 -0
  17. {switch_api-0.6.14 → switch_api-0.6.16}/setup.cfg +0 -0
  18. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/__init__.py +0 -0
  19. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/_authentication.py +0 -0
  20. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/_credentials_store/__init__.py +0 -0
  21. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/_credentials_store/_credentials_store.py +0 -0
  22. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/_msal/__init__.py +0 -0
  23. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/_msal/_custom_application.py +0 -0
  24. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_compute/__init__.py +0 -0
  25. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_compute/_commands.py +0 -0
  26. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_compute/_compute.py +0 -0
  27. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_compute/_iotservice.py +0 -0
  28. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_compute/_models.py +0 -0
  29. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_compute/_utils.py +0 -0
  30. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_guide/__init__.py +0 -0
  31. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_guide/main.py +0 -0
  32. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_guide/processor.py +0 -0
  33. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_utils/__init__.py +0 -0
  34. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_utils/_marketplace.py +0 -0
  35. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_utils/_mqtt.py +0 -0
  36. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_utils/_platform.py +0 -0
  37. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/analytics/__init__.py +0 -0
  38. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/analytics/analytics.py +0 -0
  39. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/cache/__init__.py +0 -0
  40. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/cache/cache.py +0 -0
  41. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/controls/__init__.py +0 -0
  42. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/controls/_constants.py +0 -0
  43. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/controls/_enums.py +0 -0
  44. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/controls/_mqtt.py +0 -0
  45. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/dataset/__init__.py +0 -0
  46. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/dataset/dataset.py +0 -0
  47. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/email/__init__.py +0 -0
  48. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/email/email_sender.py +0 -0
  49. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/error_handlers/__init__.py +0 -0
  50. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/error_handlers/error_handlers.py +0 -0
  51. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/extensions/__init__.py +0 -0
  52. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/extensions/extensions.py +0 -0
  53. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/extensions/field_meta.py +0 -0
  54. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/extensions/helpers.py +0 -0
  55. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/extensions/pipeline.py +0 -0
  56. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/initialize.py +0 -0
  57. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/integration/__init__.py +0 -0
  58. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/integration/_utils.py +0 -0
  59. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/integration/helpers.py +0 -0
  60. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/integration/integration.py +0 -0
  61. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/pipeline/definitions.py +0 -0
  62. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/platform_insights/__init__.py +0 -0
  63. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/platform_insights/platform_insights.py +0 -0
  64. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/realtime/__init__.py +0 -0
  65. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/realtime/_mqtt.py +0 -0
  66. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/realtime/realtime.py +0 -0
  67. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api.egg-info/SOURCES.txt +0 -0
  68. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api.egg-info/dependency_links.txt +0 -0
  69. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api.egg-info/requires.txt +0 -0
  70. {switch_api-0.6.14 → switch_api-0.6.16}/switch_api.egg-info/top_level.txt +0 -0
@@ -1,5 +1,25 @@
1
1
  # History
2
2
 
3
+ #0.6.16
4
+ ### Changed
5
+ In the `controls` module:
6
+ - Updated `submit_control_continue()` to send log control requests to the IoT endpoint in chunks of 50 per request, improving reliability and preventing oversized payloads.
7
+ - Updated `submit_control_continue()` to also batch control requests sent via MQTT (50 per batch), ensuring more stable and efficient message delivery.
8
+
9
+ #0.6.15
10
+ ### Added
11
+ In the `automation` module:
12
+ - Added `send_cds_command()` to allow programmatic execution of CDS action commands (`Start`, `Stop`, `Restart`) for a specified data feed.
13
+ - Added `deploy_as_cds_data_feed()` to support deployment of custom drivers as CDS-triggered data feeds with settings.
14
+ - Added `deploy_as_webhooks_data_feed()` to enable deployment of custom drivers as webhook-triggered data feeds with support for client signing secrets, task priority, and framework configuration.
15
+
16
+ In the `utils` module:
17
+ - Added `ensure_uuid()` helper function to validate and normalize UUID inputs, supporting both `uuid.UUID` objects and UUID strings.
18
+
19
+ ### Modified
20
+ In the `constants` module:
21
+ - Extended `TASK_PRIORITY` to include new priority levels `control` and `lendlease-beot`.
22
+
3
23
  #0.6.14
4
24
  ### Modified
5
25
  In the `integration` module:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: switch_api
3
- Version: 0.6.14
3
+ Version: 0.6.16
4
4
  Summary: A complete package for data ingestion into the Switch Automation Platform.
5
5
  Home-page: UNKNOWN
6
6
  Author: Switch Automation Pty Ltd.
@@ -38,6 +38,26 @@ pip install switch_api
38
38
 
39
39
  # History
40
40
 
41
+ #0.6.16
42
+ ### Changed
43
+ In the `controls` module:
44
+ - Updated `submit_control_continue()` to send log control requests to the IoT endpoint in chunks of 50 per request, improving reliability and preventing oversized payloads.
45
+ - Updated `submit_control_continue()` to also batch control requests sent via MQTT (50 per batch), ensuring more stable and efficient message delivery.
46
+
47
+ #0.6.15
48
+ ### Added
49
+ In the `automation` module:
50
+ - Added `send_cds_command()` to allow programmatic execution of CDS action commands (`Start`, `Stop`, `Restart`) for a specified data feed.
51
+ - Added `deploy_as_cds_data_feed()` to support deployment of custom drivers as CDS-triggered data feeds with settings.
52
+ - Added `deploy_as_webhooks_data_feed()` to enable deployment of custom drivers as webhook-triggered data feeds with support for client signing secrets, task priority, and framework configuration.
53
+
54
+ In the `utils` module:
55
+ - Added `ensure_uuid()` helper function to validate and normalize UUID inputs, supporting both `uuid.UUID` objects and UUID strings.
56
+
57
+ ### Modified
58
+ In the `constants` module:
59
+ - Extended `TASK_PRIORITY` to include new priority levels `control` and `lendlease-beot`.
60
+
41
61
  #0.6.14
42
62
  ### Modified
43
63
  In the `integration` module:
@@ -16,7 +16,7 @@ setup(
16
16
  long_description_content_type='text/markdown',
17
17
  license='MIT License',
18
18
  name="switch_api",
19
- version="0.6.14",
19
+ version="0.6.16",
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',
@@ -42,4 +42,4 @@ from . import _guide
42
42
  # log.addHandler(_ch)
43
43
  # https://dev.to/joaomcteixeira/setting-up-python-logging-for-a-library-app-6ml
44
44
 
45
- __version__ = "0.6.14"
45
+ __version__ = "0.6.16"
@@ -31,10 +31,10 @@ DATETIME_COL_FMT = Literal['DateTime', 'Date', 'Time']
31
31
  # 'Split year and month in 2 columns',
32
32
  # 'Start and End Date in 2 columns']
33
33
 
34
- EXPECTED_DELIVERY = Literal['5min', '15min', 'Hourly', 'Daily', 'Weekly', 'Monthly', 'Quarterly']
34
+ EXPECTED_DELIVERY = Literal[ 'AdHoc', '1min', '5min', '15min', 'Hourly', 'Daily', 'Weekly', 'Monthly', 'Quarterly']
35
35
 
36
36
  # deploy_type 'File' for (FTP. Email, Upload)
37
- DEPLOY_TYPE = Literal['Email', 'Ftp', 'Upload', 'Timer']
37
+ DEPLOY_TYPE = Literal['Email', 'Ftp', 'Upload', 'Timer', 'CDS']
38
38
 
39
39
  ACCOUNT = Literal['SwitchStorage', 'SwitchContainer', 'Argus', 'DataIngestion', 'GatewayMqttStorage']
40
40
 
@@ -45,11 +45,11 @@ QUERY_LANGUAGE = Literal['sql', 'kql']
45
45
  RESPONSE_TYPE = Literal['dataframe', 'json', 'string']
46
46
 
47
47
  ERROR_TYPE = Literal['DateTime', 'MissingDevices', 'NonNumeric', 'MissingRequiredField(s)', 'MissingSite(s)',
48
- 'DuplicateRecords', 'MissingSensors', 'UnableToReadFile', 'InvalidFileType', 'InvalidInputSettings', 'ReadingSummariesTimeout']
48
+ 'DuplicateRecords', 'MissingSensors', 'UnableToReadFile', 'InvalidFileType', 'InvalidInputSettings', 'ReadingSummariesTimeout', 'InvalidDataType', 'NullValues']
49
49
 
50
50
  PROCESS_STATUS = Literal['ActionRequired', 'Failed']
51
51
 
52
- MAPPING_ENTITIES = Literal['Installations', 'Devices/Sensors', 'Readings', 'Work Orders', 'IQ']
52
+ MAPPING_ENTITIES = Literal['Installations', 'Devices/Sensors', 'Readings', 'Work Orders', 'IQ', 'CustomData', 'Benchmarks']
53
53
 
54
54
  WORK_ORDER_CATEGORY = Literal['Preventative Maintenance', 'Tenant Request']
55
55
 
@@ -81,9 +81,9 @@ DATA_SET_QUERY_PARAMETER_TYPES = Literal['String', 'Number', 'DateTime', 'Keywor
81
81
 
82
82
  ADX_TABLE_DEF_TYPES = Literal['object', 'dynamic', 'int32', 'int64', 'float32', 'float64', 'datetime', 'string']
83
83
 
84
- TASK_PRIORITY = Literal['default', 'standard', 'advanced']
84
+ TASK_PRIORITY = Literal['default', 'standard', 'advanced', 'control', 'lendlease-beot']
85
85
 
86
- TASK_FRAMEWORK = Literal['PythonScriptFramework', 'TaskInsightsEngine']
86
+ TASK_FRAMEWORK = Literal['PythonScriptFramework', 'TaskInsightsEngine', 'CDSEngine']
87
87
 
88
88
  AMORTISATION_METHOD = Literal['Exclusive', 'Inclusive']
89
89
 
@@ -95,6 +95,8 @@ GUIDES_SCOPE = Literal['Portfolio-wide']
95
95
 
96
96
  IMPORT_STATUS = Literal['New', 'ToImport', 'OnHold', 'Exclude', 'Edited', 'ToRemove']
97
97
 
98
+ CDS_ACTION_COMMAND = Literal["Start", "Stop", "Restart"]
99
+
98
100
  WS_DEFAULT_PORT = 443
99
101
  WS_MQTT_CONNECTION_TIMEOUT = 180
100
102
  WS_MQTT_DEFAULT_MAX_TIMEOUT = 180
@@ -521,3 +521,13 @@ def requests_retry_session2(retries: int = 5, status_forcelist: List[int] = [408
521
521
  session.mount('https://', adapter=adapter)
522
522
  session.retries = retries
523
523
  return session
524
+
525
+ def ensure_uuid(value, field_name):
526
+ if isinstance(value, uuid.UUID):
527
+ return value
528
+ if isinstance(value, str):
529
+ try:
530
+ return uuid.UUID(value)
531
+ except ValueError:
532
+ pass
533
+ raise ValueError(f"{field_name} must be a valid UUID or UUID string.")
@@ -55,6 +55,43 @@ global _session
55
55
  _control_components = {}
56
56
  _switch_mqtt = None
57
57
  _session = None
58
+ MAX_CONTROL_REQUEST_SENSORS = 50
59
+
60
+ def _chunk_dataframe(df: pandas.DataFrame, chunk_size: int = MAX_CONTROL_REQUEST_SENSORS) -> List[pandas.DataFrame]:
61
+ """Split a dataframe into bounded chunks while preserving row order."""
62
+ if df.empty:
63
+ return []
64
+
65
+ return [
66
+ df.iloc[start_idx:start_idx + chunk_size].copy()
67
+ for start_idx in range(0, len(df), chunk_size)
68
+ ]
69
+
70
+ def _build_chunk_session_ids(chunk_count: int, session_id: Union[uuid.UUID, str, None] = None) -> List[uuid.UUID]:
71
+ """Return one unique session id per chunk, reusing the provided id for the first chunk when valid."""
72
+ if chunk_count <= 0:
73
+ return []
74
+
75
+ chunk_session_ids: List[uuid.UUID] = []
76
+ seen_session_ids = set()
77
+
78
+ if is_valid_uuid(session_id):
79
+ first_session_id = uuid.UUID(str(session_id))
80
+ else:
81
+ first_session_id = uuid.uuid4()
82
+
83
+ chunk_session_ids.append(first_session_id)
84
+ seen_session_ids.add(str(first_session_id))
85
+
86
+ while len(chunk_session_ids) < chunk_count:
87
+ next_session_id = uuid.uuid4()
88
+ if str(next_session_id) in seen_session_ids:
89
+ continue
90
+
91
+ chunk_session_ids.append(next_session_id)
92
+ seen_session_ids.add(str(next_session_id))
93
+
94
+ return chunk_session_ids
58
95
 
59
96
  def submit_control_continue(api_inputs: ApiInputs, data: pandas.DataFrame, information_columns: list = [],
60
97
  is_send_control: bool = True, ws_timeout_in_seconds: int = WS_MQTT_CONNECTION_TIMEOUT):
@@ -409,39 +446,70 @@ def __send_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
409
446
  acknowledgement_df = format_acknowledgement_payload_df(
410
447
  df=acknowledgement_df, required_columns=required_columns, has_priority=has_priority)
411
448
 
449
+ control_request_df = control_request_df.copy()
450
+ control_request_df = format_control_payload_df(
451
+ required_columns=required_columns, has_priority=has_priority, df=control_request_df)
452
+
453
+ acknowledgement_chunks = _chunk_dataframe(acknowledgement_df)
454
+ control_request_chunks = _chunk_dataframe(control_request_df)
455
+ chunk_session_ids = _build_chunk_session_ids(
456
+ chunk_count=len(control_request_chunks),
457
+ session_id=session_id)
458
+
412
459
  if is_send_control:
413
- _, result_df = log_control_request(
414
- session=_session,
415
- api_inputs=api_inputs,
416
- df=acknowledgement_df,
417
- session_id=session_id,
418
- installation_id=installation_id)
419
- if result_df.empty:
460
+ if len(acknowledgement_chunks) != len(control_request_chunks):
461
+ logger.error("Chunk count mismatch between control acknowledgement and MQTT payloads.")
420
462
  df_matrix[COL_CONTROL_STATUS] = ControlStatus.NotSentToService.value
421
463
  return df_matrix
422
464
 
423
- control_request_df = control_request_df.copy()
424
- control_request_df = format_control_payload_df(
425
- required_columns=required_columns, has_priority=has_priority, df=control_request_df)
465
+ successful_ack_chunks = []
466
+ successful_control_chunks = []
467
+ successful_session_ids = []
468
+
469
+ for acknowledgement_chunk, control_request_chunk, chunk_session_id in zip(
470
+ acknowledgement_chunks,
471
+ control_request_chunks,
472
+ chunk_session_ids,
473
+ ):
474
+ _, result_df = log_control_request(
475
+ session=_session,
476
+ api_inputs=api_inputs,
477
+ df=acknowledgement_chunk,
478
+ session_id=chunk_session_id,
479
+ installation_id=installation_id,
480
+ )
426
481
 
427
- def process_paged_request(df, page_size: int = 20):
428
- paged_results = pandas.DataFrame()
429
- total_rows = len(df)
430
- num_pages = (total_rows + page_size - 1) // page_size
482
+ if result_df.empty:
483
+ logger.warning(
484
+ "Skipping failed chunk for session_id=%s",
485
+ chunk_session_id,
486
+ )
487
+ continue
431
488
 
432
- for page_num in range(num_pages):
433
- start_idx = page_num * page_size
434
- end_idx = min((page_num + 1) * page_size, total_rows)
489
+ successful_ack_chunks.append(acknowledgement_chunk)
490
+ successful_control_chunks.append(control_request_chunk)
491
+ successful_session_ids.append(chunk_session_id)
435
492
 
436
- # Get the data for the current page
437
- page_data = df.iloc[start_idx:end_idx]
493
+ #only successfuly chunks are used for the control request next
494
+ acknowledgement_chunks = successful_ack_chunks
495
+ control_request_chunks = successful_control_chunks
496
+ chunk_session_ids = successful_session_ids
438
497
 
439
- result = send_control(page_data)
498
+ if not acknowledgement_chunks:
499
+ logger.error("All chunks failed during log control request.")
500
+ df_matrix[COL_CONTROL_STATUS] = ControlStatus.NotSentToService.value
501
+ return df_matrix
502
+
503
+ def process_paged_request(chunks: List[pandas.DataFrame], chunk_session_ids: List[uuid.UUID]):
504
+ paged_results = pandas.DataFrame()
505
+
506
+ for page_data, chunk_session_id in zip(chunks, chunk_session_ids):
507
+ result = send_control(page_data, chunk_session_id)
440
508
  paged_results = pandas.concat([paged_results, result])
441
509
 
442
510
  return paged_results
443
511
 
444
- def send_control(page_data):
512
+ def send_control(page_data: pandas.DataFrame, chunk_session_id: uuid.UUID):
445
513
  retry_count = 0
446
514
  max_retries = 3
447
515
  success_results = pandas.DataFrame()
@@ -451,7 +519,7 @@ def __send_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
451
519
 
452
520
  while retry_count < max_retries:
453
521
  success_response, missing_response = _switch_mqtt.send_control_request(
454
- session_id=str(session_id),
522
+ session_id=str(chunk_session_id),
455
523
  installation_id=installation_id,
456
524
  sensors=dataframe_to_control.to_dict(orient='records'))
457
525
 
@@ -522,7 +590,7 @@ def __send_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
522
590
  return control_result
523
591
 
524
592
  if is_send_control:
525
- control_results = process_paged_request(control_request_df)
593
+ control_results = process_paged_request(control_request_chunks, chunk_session_ids)
526
594
  control_results = control_results.rename(
527
595
  columns={'sensorId': COL_OBJECT_PROPERTY_ID})
528
596
  control_results = pandas.merge(control_results, df_matrix[[
@@ -673,12 +741,18 @@ def submit_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
673
741
  if _session == None:
674
742
  _session = requests_retry_session2(retries=5, backoff_factor=2)
675
743
 
676
- _, result_df = log_control_request(session=_session,
677
- api_inputs=api_inputs,
678
- df=data_frame,
679
- session_id=session_id,
680
- installation_id=installation_id)
681
- if result_df.empty:
744
+ acknowledgement_chunks = _chunk_dataframe(data_frame)
745
+ chunk_session_ids = _build_chunk_session_ids(
746
+ chunk_count=len(acknowledgement_chunks),
747
+ session_id=session_id)
748
+
749
+ for acknowledgement_chunk, chunk_session_id in zip(acknowledgement_chunks, chunk_session_ids):
750
+ _, result_df = log_control_request(session=_session,
751
+ api_inputs=api_inputs,
752
+ df=acknowledgement_chunk,
753
+ session_id=chunk_session_id,
754
+ installation_id=installation_id)
755
+ if result_df.empty:
682
756
  return pandas.DataFrame()
683
757
 
684
758
  # Proceeds when the control request is successful
@@ -722,24 +796,18 @@ def submit_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
722
796
  logger.info("Could not connect to MQTT Broker.")
723
797
  return 'Could not connect to MQTT Broker.', pandas.DataFrame()
724
798
 
725
- def process_paged_request(df, page_size: int = 20):
726
- paged_results = pandas.DataFrame()
727
- total_rows = len(df)
728
- num_pages = (total_rows + page_size - 1) // page_size
729
-
730
- for page_num in range(num_pages):
731
- start_idx = page_num * page_size
732
- end_idx = min((page_num + 1) * page_size, total_rows)
799
+ control_request_chunks = _chunk_dataframe(data_frame)
733
800
 
734
- # Get the data for the current page
735
- page_data = df.iloc[start_idx:end_idx]
801
+ def process_paged_request(chunks: List[pandas.DataFrame], chunk_session_ids: List[uuid.UUID]):
802
+ paged_results = pandas.DataFrame()
736
803
 
737
- result = send_control(page_data)
804
+ for page_data, chunk_session_id in zip(chunks, chunk_session_ids):
805
+ result = send_control(page_data, chunk_session_id)
738
806
  paged_results = pandas.concat([paged_results, result])
739
807
 
740
808
  return paged_results
741
809
 
742
- def send_control(page_data):
810
+ def send_control(page_data: pandas.DataFrame, chunk_session_id: uuid.UUID):
743
811
  retry_count = 0
744
812
  max_retries = 3
745
813
  success_results = pandas.DataFrame()
@@ -749,7 +817,7 @@ def submit_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
749
817
 
750
818
  while retry_count < max_retries:
751
819
  success_response, missing_response = _switch_mqtt.send_control_request(
752
- session_id=str(session_id),
820
+ session_id=str(chunk_session_id),
753
821
  installation_id=installation_id,
754
822
  sensors=dataframe_to_control.to_dict(orient='records'))
755
823
 
@@ -821,7 +889,7 @@ def submit_control(api_inputs: ApiInputs, installation_id: Union[uuid.UUID, str]
821
889
 
822
890
  # [Subscribe] to ClientId + Site State Topics
823
891
  _switch_mqtt.subscribe_installation(installation_id=installation_id)
824
- control_results = process_paged_request(data_frame)
892
+ control_results = process_paged_request(control_request_chunks, chunk_session_ids)
825
893
 
826
894
  return control_results
827
895
 
@@ -983,6 +1051,8 @@ def log_control_request(session: requests.Session, api_inputs: ApiInputs, df: pa
983
1051
  logger.info("Control Request Session Id: %s", session_id)
984
1052
  logger.info("Control Request for User: %s=%s",
985
1053
  api_inputs.email_address, api_inputs.user_id)
1054
+ logger.info("Log Control Request Payload:")
1055
+ logger.info(json_payload)
986
1056
 
987
1057
  try:
988
1058
  response = session_post(session, url, json_payload, headers)
@@ -1177,4 +1247,4 @@ def session_post(session, url, json_payload, headers):
1177
1247
  if "Connection refused" in str(ex):
1178
1248
  logger.error("Hard failure: Connection Refused. Not retrying.")
1179
1249
  raise
1180
- raise
1250
+ raise
@@ -75,13 +75,13 @@ functions for retrieving details of existing tasks on the Switch Automation Plat
75
75
  """
76
76
 
77
77
  from .pipeline import (IntegrationTask, QueueTask, LogicModuleTask, AnalyticsTask, DiscoverableIntegrationTask,
78
- EventWorkOrderTask, BlobTask, Guide, IQTask, logger)
78
+ EventWorkOrderTask, BlobTask, Guide, IQTask, CDSTask, logger)
79
79
  from .automation import Automation
80
80
  from .definitions import (IntegrationDeviceConfigPropertyDefinition, IntegrationDeviceDefinition,
81
81
  AnalyticsSettings, EventWorkOrderFieldDefinition, IntegrationSettings)
82
82
 
83
83
 
84
84
  __all__ = ['IntegrationTask', 'QueueTask', 'LogicModuleTask', 'AnalyticsTask', 'DiscoverableIntegrationTask',
85
- 'EventWorkOrderTask', 'BlobTask', 'Guide', 'IQTask', 'Automation', 'logger', 'IntegrationDeviceConfigPropertyDefinition',
85
+ 'EventWorkOrderTask', 'BlobTask', 'Guide', 'IQTask', 'CDSTask', 'Automation', 'logger', 'IntegrationDeviceConfigPropertyDefinition',
86
86
  'IntegrationDeviceDefinition', 'EventWorkOrderFieldDefinition',
87
87
  'AnalyticsSettings', 'IntegrationSettings']
@@ -23,12 +23,12 @@ import sys
23
23
  import uuid
24
24
  from io import StringIO
25
25
  from azure.servicebus import ServiceBusClient, ServiceBusReceiveMode
26
- from .._utils._utils import _is_valid_regex, ApiInputs, _column_name_cap, DataFeedFileProcessOutput
26
+ from .._utils._utils import _is_valid_regex, ApiInputs, _column_name_cap, DataFeedFileProcessOutput, ensure_uuid
27
27
  from .._utils._marketplace import add_marketplace_item
28
- from .._utils._constants import (argus_prefix, EXPECTED_DELIVERY, MAPPING_ENTITIES,
28
+ from .._utils._constants import (CDS_ACTION_COMMAND, argus_prefix, EXPECTED_DELIVERY, MAPPING_ENTITIES,
29
29
  QUEUE_NAME, ERROR_TYPE, SCHEDULE_TIMEZONE, DEPLOY_TYPE, TASK_PRIORITY, TASK_FRAMEWORK, GUIDES_EXTERNAL_TYPES, GUIDES_SCOPE)
30
30
  from .._utils._platform import _get_ingestion_service_bus_connection_string, Blob
31
- from .pipeline import (Task, QueueTask, IntegrationTask, LogicModuleTask, AnalyticsTask, EventWorkOrderTask,
31
+ from .pipeline import (CDSTask, Task, QueueTask, IntegrationTask, LogicModuleTask, AnalyticsTask, EventWorkOrderTask,
32
32
  DiscoverableIntegrationTask, BlobTask, Guide, IQTask)
33
33
  from ..extensions import ExtensionTask, replace_extensions_imports, has_extensions_support
34
34
  from .definitions import (IntegrationDeviceDefinition, EventWorkOrderFieldDefinition, AnalyticsSettings,
@@ -1040,6 +1040,227 @@ class Automation:
1040
1040
  df.columns = _column_name_cap(df.columns)
1041
1041
 
1042
1042
  return df
1043
+
1044
+ @staticmethod
1045
+ def deploy_as_cds_data_feed(task: Task,
1046
+ api_inputs: ApiInputs,
1047
+ data_feed_id: uuid.UUID,
1048
+ expected_delivery: EXPECTED_DELIVERY,
1049
+ settings: dict = {},
1050
+ data_feed_name: str = None):
1051
+ """
1052
+
1053
+ Parameters
1054
+ ----------
1055
+ task : Task
1056
+ The custom driver class created from the Abstract Base Class 'Task'
1057
+ api_inputs : ApiInputs
1058
+ Object returned by the initialize() function.
1059
+ data_feed_id : uuid.UUID
1060
+ The DataFeedId to update if existing, else will create a new record with the given value.
1061
+ expected_delivery : EXPECTED_DELIVERY
1062
+ The expected delivery frequency of the data.
1063
+ data_feed_name : str, Optional
1064
+ The name of the data feed (to be displayed in Task Insights UI). If not provided, will default to the
1065
+ task name.
1066
+ settings : dict
1067
+ List of settings used to deploy the driver. May containing information needed to access 3rd party api
1068
+ - e.g. URI, Username, Pwd, AccessToken, etc
1069
+
1070
+ Returns
1071
+ -------
1072
+ df : pandas.DataFrame
1073
+ Dataframe containing the details of the deployed https endpoint data feed.
1074
+
1075
+ """
1076
+ if not isinstance(task, CDSTask):
1077
+ logger.error(f"deploy_as_cds_data_feed only accepts tasks that implement CDSTask, got {type(task).__name__}")
1078
+ return pandas.DataFrame()
1079
+
1080
+ if api_inputs.api_base_url == '' or api_inputs.bearer_token == '':
1081
+ logger.error("You must call initialize() before using API.")
1082
+ return pandas.DataFrame()
1083
+
1084
+ if data_feed_id is None and data_feed_name is None:
1085
+ data_feed_id = uuid.UUID('00000000-0000-0000-0000-000000000000')
1086
+ data_feed_name = task.__class__.__qualname__
1087
+ elif data_feed_id is not None and data_feed_name is None:
1088
+ data_feed_id = data_feed_id
1089
+ data_feed_name = data_feed_name
1090
+
1091
+ if not set([expected_delivery]).issubset(set(EXPECTED_DELIVERY.__args__)):
1092
+ logger.error('expected_delivery parameter must be set to one of the allowed values defined by the '
1093
+ 'EXPECTED_DELIVERY literal: %s', EXPECTED_DELIVERY.__args__)
1094
+ return pandas.DataFrame()
1095
+
1096
+ if settings and not isinstance(settings, dict):
1097
+ logger.error("setting parameter should be of type dict.")
1098
+ return pandas.DataFrame()
1099
+
1100
+ headers = api_inputs.api_headers.default
1101
+
1102
+ logger.info('Deploy %s (%s) as a data feed for ApiProjectID: %s', type(task).__name__, str(task.id), api_inputs.api_project_id)
1103
+
1104
+ task_framework: TASK_FRAMEWORK = 'CDSEngine'
1105
+ task_priority: TASK_PRIORITY = 'standard'
1106
+
1107
+ logger.info('Task Framework is "%s"', str(task_framework))
1108
+ logger.info('Task Priotiy is "%s"', str(task_priority))
1109
+
1110
+ payload = {
1111
+ "driverId": str(task.id),
1112
+ "dataFeedId": str(data_feed_id),
1113
+ "name": data_feed_name,
1114
+ "feedType": "CDS",
1115
+ "sourceType": "CDS",
1116
+ "expectedDelivery": expected_delivery,
1117
+ "taskFramework": task_framework,
1118
+ "taskPriority": task_priority,
1119
+ "settings": settings
1120
+ }
1121
+
1122
+ url = f"{api_inputs.api_projects_endpoint}/{api_inputs.api_project_id}/data-ingestion/tasks/deployment"
1123
+
1124
+ logger.info("Sending request: POST %s", url)
1125
+
1126
+ response = requests.post(url, json=payload, headers=headers)
1127
+ response_status = '{} {}'.format(response.status_code, response.reason)
1128
+ if response.status_code != 200:
1129
+ logger.error("API Call was not successful. Response Status: %s. Reason: %s.", response.status_code,
1130
+ response.reason)
1131
+ return response_status, pandas.DataFrame()
1132
+ elif len(response.text) == 0:
1133
+ logger.error('No data returned for this API call. %s', response.request.url)
1134
+ return response_status, pandas.DataFrame()
1135
+
1136
+ df = pandas.read_json(StringIO(response.text), typ='Series').to_frame().T
1137
+ df.columns = _column_name_cap(df.columns)
1138
+
1139
+ return df
1140
+
1141
+ @staticmethod
1142
+ def deploy_as_webhooks_data_feed(task: Task,
1143
+ api_inputs: ApiInputs,
1144
+ data_feed_id: uuid.UUID,
1145
+ client_signing_secret: str,
1146
+ expected_delivery: EXPECTED_DELIVERY,
1147
+ settings: dict = {},
1148
+ data_feed_name: str = None,
1149
+ task_priority: TASK_PRIORITY = 'standard',
1150
+ task_framework: TASK_FRAMEWORK = 'TaskInsightsEngine'):
1151
+
1152
+ """Deploy the custom driver as a Webhook triggered Data Feed.
1153
+
1154
+ Parameters
1155
+ ----------
1156
+ task : Task
1157
+ The custom driver class created from the Abstract Base Class 'Task'
1158
+ api_inputs : ApiInputs
1159
+ Object returned by the initialize() function.
1160
+ data_feed_id : uuid.UUID
1161
+ The DataFeedId to update if existing, else will create a new record with the given value.
1162
+ client_signing_secret : uuid.UUID
1163
+ Client Signing Secret
1164
+ expected_delivery : EXPECTED_DELIVERY
1165
+ The expected delivery frequency of the data.
1166
+ queue_name : QUEUE_NAME, optional
1167
+ The queue name (Default value = 'task').
1168
+ data_feed_name : str, Optional
1169
+ The name of the data feed (to be displayed in Task Insights UI). If not provided, will default to the
1170
+ task name.
1171
+ task_priority: TASK_PRIORITY
1172
+ Determines the priority of the datafeed tasks when processing. This equates to how much resources would be alloted
1173
+ to run the task - 'default`, 'standard', or 'advanced'. Defaults to 'default'.
1174
+ task_framework: TASK_FRAMEWORK
1175
+ Determines the framework of the datafeed tasks when processing.
1176
+ 'PythonScriptFramework' for the old task runner engine.
1177
+ 'TaskInsightsEngine' for the new task running in container apps.
1178
+ settings : dict
1179
+ List of settings used to deploy the driver. May containing information needed to access 3rd party api
1180
+ - e.g. URI, Username, Pwd, AccessToken, etc
1181
+
1182
+ Returns
1183
+ -------
1184
+ df : pandas.DataFrame
1185
+ Dataframe containing the details of the deployed webhook endpoint data feed.
1186
+
1187
+ """
1188
+
1189
+ if api_inputs.api_base_url == '' or api_inputs.bearer_token == '':
1190
+ logger.error("You must call initialize() before using API.")
1191
+ return pandas.DataFrame()
1192
+
1193
+ if data_feed_id is None and data_feed_name is None:
1194
+ data_feed_id = uuid.UUID('00000000-0000-0000-0000-000000000000')
1195
+ data_feed_name = task.__class__.__qualname__
1196
+ elif data_feed_id is not None and data_feed_name is None:
1197
+ data_feed_id = data_feed_id
1198
+ data_feed_name = data_feed_name
1199
+
1200
+ if client_signing_secret == '' or client_signing_secret == None:
1201
+ logger.error("Client Signing Secret cannot be null.")
1202
+ return pandas.DataFrame()
1203
+
1204
+ headers = api_inputs.api_headers.default
1205
+
1206
+ if not set([expected_delivery]).issubset(set(EXPECTED_DELIVERY.__args__)):
1207
+ logger.error('expected_delivery parameter must be set to one of the allowed values defined by the '
1208
+ 'EXPECTED_DELIVERY literal: %s', EXPECTED_DELIVERY.__args__)
1209
+ return pandas.DataFrame()
1210
+
1211
+ if not set([task_framework]).issubset(set(TASK_FRAMEWORK.__args__)):
1212
+ logger.error('task_framework parameter must be set to one of the allowed values defined by the '
1213
+ 'TASK_FRAMEWORK literal: %s', TASK_FRAMEWORK.__args__)
1214
+ return pandas.DataFrame()
1215
+
1216
+ if not set([task_priority]).issubset(set(TASK_PRIORITY.__args__)):
1217
+ logger.error('task_priority parameter must be set to one of the allowed values defined by the '
1218
+ 'TASK_PRIORITY literal: %s', TASK_PRIORITY.__args__)
1219
+ return pandas.DataFrame()
1220
+
1221
+ if settings and not isinstance(settings, dict):
1222
+ logger.error("setting parameter should be of type dict.")
1223
+ return pandas.DataFrame()
1224
+
1225
+ logger.info('Deploy %s (%s) as a data feed for ApiProjectID: %s', type(task).__name__, str(task.id),
1226
+ api_inputs.api_project_id)
1227
+ logger.info('Task Framework is "%s"', str(task_framework))
1228
+ logger.info('Task Priotiy is "%s"', str(task_priority))
1229
+
1230
+ payload = {
1231
+ "dataFeedId": str(data_feed_id),
1232
+ "driverId": str(task.id),
1233
+ "name": data_feed_name,
1234
+ "feedType": "Webhooks",
1235
+ "expectedDelivery": expected_delivery,
1236
+ "sourceType": "Webhooks",
1237
+ "taskFramework": task_framework,
1238
+ "taskPriority": task_priority,
1239
+ "webhook": {
1240
+ "clientSigningSecret": client_signing_secret
1241
+ },
1242
+ "settings": settings
1243
+ }
1244
+
1245
+ url = f"{api_inputs.api_projects_endpoint}/{api_inputs.api_project_id}/data-ingestion/tasks/deployment"
1246
+
1247
+ logger.info("Sending request: POST %s", url)
1248
+
1249
+ response = requests.post(url, json=payload, headers=headers)
1250
+ response_status = '{} {}'.format(response.status_code, response.reason)
1251
+
1252
+ if response.status_code != 200:
1253
+ logger.error("API Call was not successful. Response Status: %s. Reason: %s.", response.status_code,
1254
+ response.reason)
1255
+ return response_status, pandas.DataFrame()
1256
+ elif len(response.text) == 0:
1257
+ logger.error('No data returned for this API call. %s', response.request.url)
1258
+ return response_status, pandas.DataFrame()
1259
+
1260
+ df = pandas.read_json(StringIO(response.text), typ='Series').to_frame().T
1261
+ df.columns = _column_name_cap(df.columns)
1262
+
1263
+ return df
1043
1264
 
1044
1265
  @staticmethod
1045
1266
  def deploy_as_blob_data_feed(task: BlobTask,
@@ -1377,6 +1598,10 @@ class Automation:
1377
1598
  # Check if there's any timer deploy_type being cancelled, delete it as well by calling delete method
1378
1599
  if any("Timer" in deploy_type for deploy_type in deployment_type):
1379
1600
  return Automation.delete_deployed_data_feed(api_inputs=api_inputs, data_feed_id=data_feed_id)
1601
+
1602
+ # Check if there's any CDS deploy_type being cancelled, send `STOP` action command by calling send_cds_command()
1603
+ if any("CDS" in deploy_type for deploy_type in deployment_type):
1604
+ return Automation.send_cds_command(api_inputs=api_inputs, data_feed_id=data_feed_id, action_command='Stop')
1380
1605
 
1381
1606
  df = pandas.read_json(response.text, typ='Series').to_frame().T
1382
1607
  df.columns = _column_name_cap(df.columns)
@@ -1385,7 +1610,8 @@ class Automation:
1385
1610
 
1386
1611
  @staticmethod
1387
1612
  def delete_deployed_data_feed(api_inputs: ApiInputs, data_feed_id: uuid.UUID):
1388
- """Delete deployment for a given `data_feed_id`.
1613
+ """
1614
+ Delete timer schedule for a given `data_feed_id`.
1389
1615
 
1390
1616
  Parameters
1391
1617
  ----------
@@ -1813,3 +2039,111 @@ class Automation:
1813
2039
  task_code += 'task = ' + task_type.__name__ + '()'
1814
2040
 
1815
2041
  return task_code
2042
+
2043
+ @staticmethod
2044
+ def send_cds_command(
2045
+ api_inputs: ApiInputs,
2046
+ data_feed_id: uuid.UUID,
2047
+ action_command: CDS_ACTION_COMMAND):
2048
+ """Send a CDS action command for a given data feed.
2049
+
2050
+ This method sends a POST request to the CDS action-command endpoint for the
2051
+ specified data feed. It validates the required API inputs and command values
2052
+ before building the request payload and submitting it to the API.
2053
+
2054
+ Parameters
2055
+ ----------
2056
+ api_inputs : ApiInputs
2057
+ Object returned by the initialize.initialize() function. This must contain
2058
+ a valid ``api_base_url``, ``bearer_token``, ``api_project_id``,
2059
+ ``api_projects_endpoint``, and default request headers.
2060
+ data_feed_id : uuid.UUID
2061
+ The unique identifier of the data feed that the CDS command will be sent to.
2062
+ action_command : CDS_ACTION_COMMAND
2063
+ The action command to execute for the target data feed. This must be set
2064
+ to one of the allowed literal values defined by ``CDS_ACTION_COMMAND``.
2065
+
2066
+ Returns
2067
+ -------
2068
+ pandas.DataFrame
2069
+ A dataframe containing the API response when the request is successful.
2070
+
2071
+ tuple[str, pandas.DataFrame]
2072
+ Returns a tuple of ``(response_status, empty_dataframe)`` when the API call
2073
+ is not successful.
2074
+
2075
+ pandas.DataFrame
2076
+ Returns an empty dataframe when API inputs are not initialized or when no
2077
+ data is returned from the API.
2078
+
2079
+ Raises
2080
+ ------
2081
+ ValueError
2082
+ Raised when:
2083
+ - ``api_inputs`` is None
2084
+ - ``data_feed_id`` is not a valid ``uuid.UUID``
2085
+ - ``action_command`` is not one of the allowed ``CDS_ACTION_COMMAND`` values
2086
+
2087
+ Notes
2088
+ -----
2089
+ This method performs the following steps:
2090
+
2091
+ 1. Validates the required inputs.
2092
+ 2. Builds the request payload using the API project ID, data feed ID,
2093
+ and action command.
2094
+ 3. Sends a POST request to the CDS action-command API endpoint.
2095
+ 4. Converts the JSON response into a single-row pandas dataframe.
2096
+ 5. Normalizes the dataframe column names using ``_column_name_cap``.
2097
+
2098
+ Examples
2099
+ --------
2100
+ >>> df = sw.pipeline.Automation._send_cds_command(
2101
+ ... api_inputs=api_inputs,
2102
+ ... data_feed_id=uuid.UUID("12345678-1234-1234-1234-123456789abc"),
2103
+ ... action_command="Start"
2104
+ ... )
2105
+
2106
+ """
2107
+
2108
+ # ensure data_feed_id is UUID
2109
+ data_feed_id = ensure_uuid(data_feed_id, "data_feed_id")
2110
+
2111
+ # Validate api_inputs
2112
+ if api_inputs is None:
2113
+ raise ValueError("api_inputs cannot be None.")
2114
+
2115
+ if not api_inputs.api_base_url or not api_inputs.bearer_token:
2116
+ logger.error("You must call initialize() before using API.")
2117
+ return pandas.DataFrame()
2118
+
2119
+ # Validate enums/literals
2120
+ if action_command not in CDS_ACTION_COMMAND.__args__:
2121
+ raise ValueError('action_command parameter must be set to one of the allowed values defined by the '
2122
+ 'CDS_ACTION_COMMAND literal: %s', CDS_ACTION_COMMAND.__args__)
2123
+
2124
+ payload = {
2125
+ "apiProjectId": api_inputs.api_project_id,
2126
+ "dataFeedId": str(data_feed_id),
2127
+ "actionCommand": action_command
2128
+ }
2129
+
2130
+ url = f"{api_inputs.api_projects_endpoint}/{api_inputs.api_project_id}/data-ingestion/data-feed/{data_feed_id}/cds/action-command"
2131
+
2132
+ logger.info("Sending request: POST %s", url)
2133
+
2134
+ headers = api_inputs.api_headers.default
2135
+ response = requests.post(url, json=payload, headers=headers)
2136
+ response_status = '{} {}'.format(response.status_code, response.reason)
2137
+
2138
+ if response.status_code != 200:
2139
+ logger.error("API Call was not successful. Response Status: %s. Reason: %s.", response.status_code,
2140
+ response.reason)
2141
+ return response_status, pandas.DataFrame()
2142
+ elif len(response.text) == 0:
2143
+ logger.error('No data returned for this API call. %s', response.request.url)
2144
+ return response_status, pandas.DataFrame()
2145
+
2146
+ df = pandas.read_json(StringIO(response.text), typ='Series').to_frame().T
2147
+ df.columns = _column_name_cap(df.columns)
2148
+
2149
+ return df
@@ -621,6 +621,16 @@ class LogicModuleTask(Task):
621
621
 
622
622
  # Needs to be implemented into the deploy_on_timer
623
623
 
624
+ class CDSTask(Task):
625
+ """
626
+ Base class for long-running CDS driver tasks.
627
+
628
+ Implement process_loop() for continuous execution / polling / worker-style behavior.
629
+ """
630
+
631
+ @abstractmethod
632
+ def process_loop(self, api_inputs, settings):
633
+ pass
624
634
 
625
635
  class RunAt(enum.Enum):
626
636
  """ """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: switch-api
3
- Version: 0.6.14
3
+ Version: 0.6.16
4
4
  Summary: A complete package for data ingestion into the Switch Automation Platform.
5
5
  Home-page: UNKNOWN
6
6
  Author: Switch Automation Pty Ltd.
@@ -38,6 +38,26 @@ pip install switch_api
38
38
 
39
39
  # History
40
40
 
41
+ #0.6.16
42
+ ### Changed
43
+ In the `controls` module:
44
+ - Updated `submit_control_continue()` to send log control requests to the IoT endpoint in chunks of 50 per request, improving reliability and preventing oversized payloads.
45
+ - Updated `submit_control_continue()` to also batch control requests sent via MQTT (50 per batch), ensuring more stable and efficient message delivery.
46
+
47
+ #0.6.15
48
+ ### Added
49
+ In the `automation` module:
50
+ - Added `send_cds_command()` to allow programmatic execution of CDS action commands (`Start`, `Stop`, `Restart`) for a specified data feed.
51
+ - Added `deploy_as_cds_data_feed()` to support deployment of custom drivers as CDS-triggered data feeds with settings.
52
+ - Added `deploy_as_webhooks_data_feed()` to enable deployment of custom drivers as webhook-triggered data feeds with support for client signing secrets, task priority, and framework configuration.
53
+
54
+ In the `utils` module:
55
+ - Added `ensure_uuid()` helper function to validate and normalize UUID inputs, supporting both `uuid.UUID` objects and UUID strings.
56
+
57
+ ### Modified
58
+ In the `constants` module:
59
+ - Extended `TASK_PRIORITY` to include new priority levels `control` and `lendlease-beot`.
60
+
41
61
  #0.6.14
42
62
  ### Modified
43
63
  In the `integration` module:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes