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.
- {switch_api-0.6.14 → switch_api-0.6.16}/HISTORY.md +20 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/PKG-INFO +21 -1
- {switch_api-0.6.14 → switch_api-0.6.16}/setup.py +1 -1
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/__init__.py +1 -1
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_utils/_constants.py +8 -6
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_utils/_utils.py +10 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/controls/controls.py +114 -44
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/pipeline/__init__.py +2 -2
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/pipeline/automation.py +338 -4
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/pipeline/pipeline.py +10 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api.egg-info/PKG-INFO +21 -1
- {switch_api-0.6.14 → switch_api-0.6.16}/AUTHORS.rst +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/LICENCE +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/MANIFEST.in +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/README.md +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/pyproject.toml +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/setup.cfg +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/_authentication.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/_credentials_store/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/_credentials_store/_credentials_store.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/_msal/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/_msal/_custom_application.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_compute/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_compute/_commands.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_compute/_compute.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_compute/_iotservice.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_compute/_models.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_compute/_utils.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_guide/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_guide/main.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_guide/processor.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_utils/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_utils/_marketplace.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_utils/_mqtt.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_utils/_platform.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/analytics/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/analytics/analytics.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/cache/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/cache/cache.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/controls/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/controls/_constants.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/controls/_enums.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/controls/_mqtt.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/dataset/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/dataset/dataset.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/email/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/email/email_sender.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/error_handlers/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/error_handlers/error_handlers.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/extensions/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/extensions/extensions.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/extensions/field_meta.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/extensions/helpers.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/extensions/pipeline.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/initialize.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/integration/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/integration/_utils.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/integration/helpers.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/integration/integration.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/pipeline/definitions.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/platform_insights/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/platform_insights/platform_insights.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/realtime/__init__.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/realtime/_mqtt.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api/realtime/realtime.py +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api.egg-info/SOURCES.txt +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api.egg-info/dependency_links.txt +0 -0
- {switch_api-0.6.14 → switch_api-0.6.16}/switch_api.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
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',
|
|
@@ -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
|
-
|
|
414
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
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
|
-
|
|
735
|
-
|
|
801
|
+
def process_paged_request(chunks: List[pandas.DataFrame], chunk_session_ids: List[uuid.UUID]):
|
|
802
|
+
paged_results = pandas.DataFrame()
|
|
736
803
|
|
|
737
|
-
|
|
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(
|
|
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(
|
|
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
|
-
"""
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
{switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/_credentials_store/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{switch_api-0.6.14 → switch_api-0.6.16}/switch_api/_authentication/_msal/_custom_application.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
|
|
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
|
|
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
|
|
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
|