switch-api 0.6.30__tar.gz → 0.6.33__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.30 → switch_api-0.6.33}/HISTORY.md +36 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/PKG-INFO +37 -1
- {switch_api-0.6.30 → switch_api-0.6.33}/setup.py +1 -1
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/__init__.py +1 -1
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_compute/_compute.py +8 -1
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/integration/helpers.py +2 -2
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/integration/integration.py +10 -1
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/pipeline/automation.py +12 -1
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api.egg-info/PKG-INFO +37 -1
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api.egg-info/SOURCES.txt +4 -1
- switch_api-0.6.33/tests/test_carbon_calculation.py +91 -0
- switch_api-0.6.33/tests/test_carbon_response_diagnostics.py +74 -0
- switch_api-0.6.33/tests/test_timer_deployment.py +62 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/AUTHORS.rst +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/LICENCE +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/MANIFEST.in +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/README.md +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/pyproject.toml +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/setup.cfg +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_authentication/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_authentication/_authentication.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_authentication/_credentials_store/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_authentication/_credentials_store/_credentials_store.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_authentication/_msal/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_authentication/_msal/_custom_application.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_compute/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_compute/_commands.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_compute/_iotservice.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_compute/_models.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_compute/_utils.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_guide/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_guide/main.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_guide/processor.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_utils/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_utils/_constants.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_utils/_marketplace.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_utils/_mqtt.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_utils/_platform.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_utils/_sql.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/_utils/_utils.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/analytics/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/analytics/analytics.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/cache/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/cache/cache.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/controls/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/controls/_constants.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/controls/_enums.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/controls/_mqtt.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/controls/controls.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/dataset/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/dataset/dataset.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/email/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/email/email_sender.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/error_handlers/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/error_handlers/error_handlers.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/extensions/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/extensions/extensions.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/extensions/field_meta.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/extensions/helpers.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/extensions/pipeline.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/initialize.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/integration/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/integration/_utils.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/pipeline/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/pipeline/definitions.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/pipeline/pipeline.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/platform_insights/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/platform_insights/platform_insights.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/realtime/__init__.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/realtime/_mqtt.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/realtime/_redis.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api/realtime/realtime.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api.egg-info/dependency_links.txt +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api.egg-info/requires.txt +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/switch_api.egg-info/top_level.txt +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/tests/test_authentication.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/tests/test_controls.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/tests/test_dataset.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/tests/test_error_handlers.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/tests/test_initialize.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/tests/test_integration.py +0 -0
- {switch_api-0.6.30 → switch_api-0.6.33}/tests/test_pipeline.py +0 -0
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# History
|
|
2
2
|
|
|
3
|
+
#0.6.33
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
In the `integration` module:
|
|
8
|
+
|
|
9
|
+
- Fixed `unknown type object` errors in carbon calculations by converting numeric object and pandas extension arrays into NumPy arrays supported by `numexpr`. Existing NumPy numeric dtypes and original input values are preserved.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
In the `_compute` module:
|
|
14
|
+
|
|
15
|
+
- Added HTTP status, response elapsed time, content type, and exception type to carbon-expression JSON parsing failure logs. Response bodies and exception messages are omitted. The existing empty-result fallback is retained.
|
|
16
|
+
|
|
17
|
+
#0.6.32
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
In the `integration` module:
|
|
22
|
+
|
|
23
|
+
- Changed the default `retries` for `get_device_sensors()` from `0` to `5`, enabling the existing retry policy for each page request by default. Callers can override the retry count, including `retries=0` to disable retries.
|
|
24
|
+
- Changed the default `retries` for `get_data()` from `0` to `5` to match its documented default. Callers can still override the retry count.
|
|
25
|
+
|
|
26
|
+
#0.6.31
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
In the `pipeline` module:
|
|
31
|
+
|
|
32
|
+
- Added optional `email`, `ftp`, `upload`, and `webhook` delivery settings to `Automation.deploy_on_timer()`, allowing scheduled data feeds to retain additional delivery methods.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Timer deployment requests include all four delivery settings while retaining `sourceType: Timer`. Omitted settings, `None`, or empty dictionaries clear the corresponding stored configuration; resupply each setting that should remain enabled.
|
|
37
|
+
- Existing positional arguments and driver `settings` retain their meaning.
|
|
38
|
+
|
|
3
39
|
#0.6.30
|
|
4
40
|
|
|
5
41
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: switch_api
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.33
|
|
4
4
|
Summary: A complete package for data ingestion into the Switch Automation Platform.
|
|
5
5
|
Author: Switch Automation Pty Ltd.
|
|
6
6
|
License: MIT License
|
|
@@ -58,6 +58,42 @@ pip install switch_api
|
|
|
58
58
|
|
|
59
59
|
# History
|
|
60
60
|
|
|
61
|
+
#0.6.33
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
In the `integration` module:
|
|
66
|
+
|
|
67
|
+
- Fixed `unknown type object` errors in carbon calculations by converting numeric object and pandas extension arrays into NumPy arrays supported by `numexpr`. Existing NumPy numeric dtypes and original input values are preserved.
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
|
|
71
|
+
In the `_compute` module:
|
|
72
|
+
|
|
73
|
+
- Added HTTP status, response elapsed time, content type, and exception type to carbon-expression JSON parsing failure logs. Response bodies and exception messages are omitted. The existing empty-result fallback is retained.
|
|
74
|
+
|
|
75
|
+
#0.6.32
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
In the `integration` module:
|
|
80
|
+
|
|
81
|
+
- Changed the default `retries` for `get_device_sensors()` from `0` to `5`, enabling the existing retry policy for each page request by default. Callers can override the retry count, including `retries=0` to disable retries.
|
|
82
|
+
- Changed the default `retries` for `get_data()` from `0` to `5` to match its documented default. Callers can still override the retry count.
|
|
83
|
+
|
|
84
|
+
#0.6.31
|
|
85
|
+
|
|
86
|
+
### Added
|
|
87
|
+
|
|
88
|
+
In the `pipeline` module:
|
|
89
|
+
|
|
90
|
+
- Added optional `email`, `ftp`, `upload`, and `webhook` delivery settings to `Automation.deploy_on_timer()`, allowing scheduled data feeds to retain additional delivery methods.
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
|
|
94
|
+
- Timer deployment requests include all four delivery settings while retaining `sourceType: Timer`. Omitted settings, `None`, or empty dictionaries clear the corresponding stored configuration; resupply each setting that should remain enabled.
|
|
95
|
+
- Existing positional arguments and driver `settings` retain their meaning.
|
|
96
|
+
|
|
61
97
|
#0.6.30
|
|
62
98
|
|
|
63
99
|
### Added
|
|
@@ -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.33",
|
|
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',
|
|
@@ -78,7 +78,14 @@ def get_carbon_calculation_expression(df: pd.DataFrame, api_inputs: ApiInputs) -
|
|
|
78
78
|
logger.info('Successfully retrieved %d carbon calculation expressions.', len(carbon_calculations_df))
|
|
79
79
|
return carbon_calculations_df
|
|
80
80
|
except ValueError as e:
|
|
81
|
-
logger.error(
|
|
81
|
+
logger.error(
|
|
82
|
+
"Failed to parse JSON for carbon calculations. exception_type=%s; "
|
|
83
|
+
"HTTP status=%s; elapsed_seconds=%.3f; content_type=%r",
|
|
84
|
+
type(e).__name__,
|
|
85
|
+
response_carbon.status_code,
|
|
86
|
+
response_carbon.elapsed.total_seconds(),
|
|
87
|
+
response_carbon.headers.get('Content-Type', ''),
|
|
88
|
+
)
|
|
82
89
|
return pd.DataFrame()
|
|
83
90
|
|
|
84
91
|
def _execute_iot_sql_query(api_inputs: ApiInputs, query: str) -> requests.Response:
|
|
@@ -268,7 +268,7 @@ def get_sites(api_inputs: ApiInputs, include_tag_groups: Union[list, bool] = Fal
|
|
|
268
268
|
|
|
269
269
|
def get_device_sensors(api_inputs: ApiInputs, include_tag_groups: Union[list, bool] = False,
|
|
270
270
|
include_metadata_keys: Union[list, bool] = False, sql_where_clause: str = None,
|
|
271
|
-
top_count: int = None, retries: int =
|
|
271
|
+
top_count: int = None, retries: int = 5, backoff_factor: Union[int, float] = 0.3):
|
|
272
272
|
"""Retrieve device and sensor information.
|
|
273
273
|
|
|
274
274
|
Optionally include all or a subset of tag groups and/or metadata keys depending on the configuration of the
|
|
@@ -436,7 +436,7 @@ def get_device_sensors(api_inputs: ApiInputs, include_tag_groups: Union[list, bo
|
|
|
436
436
|
|
|
437
437
|
|
|
438
438
|
def get_data(query_text, api_inputs: ApiInputs, query_language: QUERY_LANGUAGE = "kql",
|
|
439
|
-
response_type: RESPONSE_TYPE = 'dataframe', retries: int =
|
|
439
|
+
response_type: RESPONSE_TYPE = 'dataframe', retries: int = 5, backoff_factor: Union[int, float] = 2):
|
|
440
440
|
"""Retrieve data.
|
|
441
441
|
|
|
442
442
|
Parameters
|
|
@@ -1901,9 +1901,18 @@ def upsert_timeseries(df: pandas.DataFrame, api_inputs: ApiInputs, is_local_time
|
|
|
1901
1901
|
for expr in unique_expressions:
|
|
1902
1902
|
expr_mask = valid_carbon_calcs["CarbonExpression"] == expr
|
|
1903
1903
|
numexpr_expression = expr.replace("[Value]", "Value")
|
|
1904
|
+
value_series = valid_carbon_calcs.loc[expr_mask, "Value"]
|
|
1905
|
+
values = value_series.values
|
|
1906
|
+
# Preserve existing NumPy dtypes and their evaluation behavior.
|
|
1907
|
+
# Only normalize object or pandas extension arrays for numexpr.
|
|
1908
|
+
if not isinstance(values, np.ndarray) or values.dtype.kind == "O":
|
|
1909
|
+
numeric_values = pd.to_numeric(value_series, errors="raise")
|
|
1910
|
+
values = numeric_values.to_numpy()
|
|
1911
|
+
if values.dtype.kind == "O":
|
|
1912
|
+
values = numeric_values.to_numpy(dtype=np.float64, na_value=np.nan)
|
|
1904
1913
|
valid_carbon_calcs.loc[expr_mask, "Carbon"] = numexpr.evaluate(
|
|
1905
1914
|
numexpr_expression,
|
|
1906
|
-
local_dict={"Value":
|
|
1915
|
+
local_dict={"Value": values}
|
|
1907
1916
|
)
|
|
1908
1917
|
|
|
1909
1918
|
df = df.merge(
|
|
@@ -1366,7 +1366,9 @@ class Automation:
|
|
|
1366
1366
|
cron_schedule: str, queue_name: QUEUE_NAME = "task", settings: dict = None,
|
|
1367
1367
|
schedule_timezone: SCHEDULE_TIMEZONE = 'Local', timezone_offset_minutes: int = None,
|
|
1368
1368
|
data_feed_name: str = None, task_priority: TASK_PRIORITY = 'standard',
|
|
1369
|
-
task_framework: TASK_FRAMEWORK = 'TaskInsightsEngine'
|
|
1369
|
+
task_framework: TASK_FRAMEWORK = 'TaskInsightsEngine',
|
|
1370
|
+
email: dict = None, ftp: dict = None, upload: dict = None,
|
|
1371
|
+
webhook: dict = None):
|
|
1370
1372
|
"""Deploy driver to run on timer.
|
|
1371
1373
|
|
|
1372
1374
|
Parameters
|
|
@@ -1403,6 +1405,11 @@ class Automation:
|
|
|
1403
1405
|
'PythonScriptFramework' for the old task runner engine.
|
|
1404
1406
|
'TaskInsightsEngine' for the new task running in container apps.
|
|
1405
1407
|
|
|
1408
|
+
email, ftp, upload, webhook : dict, optional
|
|
1409
|
+
Independent delivery settings using API camelCase field names; each defaults to None.
|
|
1410
|
+
Timer updates replace all four: None or an empty object clears the corresponding setting.
|
|
1411
|
+
Upload requires a nonempty uploadUrl or ingestUrl; the server generates the actual URLs.
|
|
1412
|
+
|
|
1406
1413
|
Returns
|
|
1407
1414
|
-------
|
|
1408
1415
|
pandas.Dataframe
|
|
@@ -1505,6 +1512,10 @@ class Automation:
|
|
|
1505
1512
|
"timezoneOffsetMinutes": timezone_offset_minutes,
|
|
1506
1513
|
"scheduleTimezone": schedule_timezone
|
|
1507
1514
|
},
|
|
1515
|
+
"email": email,
|
|
1516
|
+
"ftp": ftp,
|
|
1517
|
+
"upload": upload,
|
|
1518
|
+
"webhook": webhook,
|
|
1508
1519
|
"settings": settings
|
|
1509
1520
|
}
|
|
1510
1521
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: switch_api
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.33
|
|
4
4
|
Summary: A complete package for data ingestion into the Switch Automation Platform.
|
|
5
5
|
Author: Switch Automation Pty Ltd.
|
|
6
6
|
License: MIT License
|
|
@@ -58,6 +58,42 @@ pip install switch_api
|
|
|
58
58
|
|
|
59
59
|
# History
|
|
60
60
|
|
|
61
|
+
#0.6.33
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
In the `integration` module:
|
|
66
|
+
|
|
67
|
+
- Fixed `unknown type object` errors in carbon calculations by converting numeric object and pandas extension arrays into NumPy arrays supported by `numexpr`. Existing NumPy numeric dtypes and original input values are preserved.
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
|
|
71
|
+
In the `_compute` module:
|
|
72
|
+
|
|
73
|
+
- Added HTTP status, response elapsed time, content type, and exception type to carbon-expression JSON parsing failure logs. Response bodies and exception messages are omitted. The existing empty-result fallback is retained.
|
|
74
|
+
|
|
75
|
+
#0.6.32
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
In the `integration` module:
|
|
80
|
+
|
|
81
|
+
- Changed the default `retries` for `get_device_sensors()` from `0` to `5`, enabling the existing retry policy for each page request by default. Callers can override the retry count, including `retries=0` to disable retries.
|
|
82
|
+
- Changed the default `retries` for `get_data()` from `0` to `5` to match its documented default. Callers can still override the retry count.
|
|
83
|
+
|
|
84
|
+
#0.6.31
|
|
85
|
+
|
|
86
|
+
### Added
|
|
87
|
+
|
|
88
|
+
In the `pipeline` module:
|
|
89
|
+
|
|
90
|
+
- Added optional `email`, `ftp`, `upload`, and `webhook` delivery settings to `Automation.deploy_on_timer()`, allowing scheduled data feeds to retain additional delivery methods.
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
|
|
94
|
+
- Timer deployment requests include all four delivery settings while retaining `sourceType: Timer`. Omitted settings, `None`, or empty dictionaries clear the corresponding stored configuration; resupply each setting that should remain enabled.
|
|
95
|
+
- Existing positional arguments and driver `settings` retain their meaning.
|
|
96
|
+
|
|
61
97
|
#0.6.30
|
|
62
98
|
|
|
63
99
|
### Added
|
|
@@ -70,9 +70,12 @@ switch_api/realtime/_mqtt.py
|
|
|
70
70
|
switch_api/realtime/_redis.py
|
|
71
71
|
switch_api/realtime/realtime.py
|
|
72
72
|
tests/test_authentication.py
|
|
73
|
+
tests/test_carbon_calculation.py
|
|
74
|
+
tests/test_carbon_response_diagnostics.py
|
|
73
75
|
tests/test_controls.py
|
|
74
76
|
tests/test_dataset.py
|
|
75
77
|
tests/test_error_handlers.py
|
|
76
78
|
tests/test_initialize.py
|
|
77
79
|
tests/test_integration.py
|
|
78
|
-
tests/test_pipeline.py
|
|
80
|
+
tests/test_pipeline.py
|
|
81
|
+
tests/test_timer_deployment.py
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Offline regression tests for the nested carbon calculation helper."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import unittest
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
import numexpr
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_carbon_helper():
|
|
16
|
+
# Isolate the nested helper without initializing API clients or ingesting data.
|
|
17
|
+
source = Path(__file__).parents[1] / "switch_api/integration/integration.py"
|
|
18
|
+
tree = ast.parse(source.read_text(encoding="utf-8"))
|
|
19
|
+
upsert = next(node for node in tree.body
|
|
20
|
+
if isinstance(node, ast.FunctionDef) and node.name == "upsert_timeseries")
|
|
21
|
+
helper = next(node for node in upsert.body
|
|
22
|
+
if isinstance(node, ast.FunctionDef) and node.name == "add_carbon_column")
|
|
23
|
+
namespace = {"pd": pd, "np": np, "numexpr": numexpr,
|
|
24
|
+
"logger": logging.getLogger(__name__)}
|
|
25
|
+
exec(compile(ast.Module(body=[helper], type_ignores=[]), str(source), "exec"), namespace)
|
|
26
|
+
return namespace["add_carbon_column"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CarbonCalculationTests(unittest.TestCase):
|
|
30
|
+
def calculate(self, values, expression="[Value] * 0.5"):
|
|
31
|
+
frame = pd.DataFrame({
|
|
32
|
+
"ObjectPropertyId": ["SENSOR"] * len(values),
|
|
33
|
+
"TimestampLocal": pd.to_datetime(["2026-06-01"] * len(values)),
|
|
34
|
+
"Value": values,
|
|
35
|
+
})
|
|
36
|
+
original = frame.copy(deep=True)
|
|
37
|
+
calculations = pd.DataFrame({
|
|
38
|
+
"ObjectPropertyID": ["sensor", "sensor"],
|
|
39
|
+
"CarbonExpression": ["[Value] * 9", expression],
|
|
40
|
+
"FromDate": ["2026-01-01"] * 2,
|
|
41
|
+
"ToDate": ["2026-12-31"] * 2,
|
|
42
|
+
"ObjectPropertyTemplateID": [None, "template"],
|
|
43
|
+
})
|
|
44
|
+
result = load_carbon_helper()(frame, calculations)
|
|
45
|
+
pd.testing.assert_frame_equal(frame, original)
|
|
46
|
+
pd.testing.assert_series_equal(result["Value"], original["Value"])
|
|
47
|
+
self.assertEqual(len(result), len(frame))
|
|
48
|
+
self.assertNotIn("__row_id", result)
|
|
49
|
+
return result["Carbon"].to_numpy()
|
|
50
|
+
|
|
51
|
+
def test_numeric_input_representations(self):
|
|
52
|
+
for values in ([2.0, 4.0], [2, 4], ["2", "4"],
|
|
53
|
+
[Decimal("2"), Decimal("4")],
|
|
54
|
+
pd.Series([2, 4], dtype=object),
|
|
55
|
+
pd.Series([2, 4], dtype="Float64")):
|
|
56
|
+
with self.subTest(values=repr(values)):
|
|
57
|
+
np.testing.assert_allclose(self.calculate(values), [1, 2])
|
|
58
|
+
|
|
59
|
+
def test_missing_values_preserve_existing_zero_carbon_fallback(self):
|
|
60
|
+
for values in (["2", None], pd.Series([2, pd.NA], dtype="Float64"),
|
|
61
|
+
pd.Series([2, pd.NA], dtype="Int64")):
|
|
62
|
+
with self.subTest(values=repr(values)):
|
|
63
|
+
np.testing.assert_allclose(self.calculate(values), [1, 0])
|
|
64
|
+
|
|
65
|
+
def test_invalid_text_is_rejected(self):
|
|
66
|
+
with self.assertRaises(ValueError):
|
|
67
|
+
self.calculate(["invalid", "2"])
|
|
68
|
+
|
|
69
|
+
def test_existing_numpy_numeric_dtypes_are_preserved(self):
|
|
70
|
+
for dtype in (np.int32, np.int64, np.float32, np.float64):
|
|
71
|
+
with self.subTest(dtype=dtype), patch.object(
|
|
72
|
+
numexpr, "evaluate", wraps=numexpr.evaluate
|
|
73
|
+
) as evaluate:
|
|
74
|
+
self.calculate(np.array([2, 4], dtype=dtype))
|
|
75
|
+
supplied = evaluate.call_args.kwargs["local_dict"]["Value"]
|
|
76
|
+
self.assertEqual(supplied.dtype, np.dtype(dtype))
|
|
77
|
+
|
|
78
|
+
def test_large_integer_precision_is_preserved_during_evaluation(self):
|
|
79
|
+
values = np.array([2**53 + 1, 2**53 + 3], dtype=np.int64)
|
|
80
|
+
np.testing.assert_array_equal(
|
|
81
|
+
self.calculate(values, "[Value] - 9007199254740992"), [1, 3]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def test_integer_only_expression_still_works(self):
|
|
85
|
+
np.testing.assert_array_equal(
|
|
86
|
+
self.calculate(np.array([2, 4], dtype=np.int64), "[Value] << 1"), [4, 8]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
unittest.main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Offline tests of carbon response diagnostics; no API initialization required."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import unittest
|
|
8
|
+
from unittest.mock import Mock
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CarbonResponseDiagnosticsTests(unittest.TestCase):
|
|
15
|
+
def setUp(self):
|
|
16
|
+
source = Path(__file__).parents[1] / 'switch_api/_compute/_compute.py'
|
|
17
|
+
tree = ast.parse(source.read_text(encoding='utf-8'))
|
|
18
|
+
functions = [node for node in tree.body if isinstance(node, ast.FunctionDef)
|
|
19
|
+
and node.name == 'get_carbon_calculation_expression']
|
|
20
|
+
self.logger = logging.getLogger('carbon_response_test')
|
|
21
|
+
self.response = requests.Response()
|
|
22
|
+
self.response.status_code = 200
|
|
23
|
+
self.response.elapsed = timedelta(seconds=10.125)
|
|
24
|
+
self.response.headers['Content-Type'] = 'application/json'
|
|
25
|
+
self.namespace = dict(pd=pd, ApiInputs=object,
|
|
26
|
+
logger=self.logger,
|
|
27
|
+
COMMAND_CARBON_CALCULATION_EXPRESSION=Mock(return_value='query'),
|
|
28
|
+
_execute_iot_sql_query=Mock(return_value=self.response))
|
|
29
|
+
exec(compile(ast.Module(body=functions, type_ignores=[]), str(source), 'exec'), self.namespace)
|
|
30
|
+
|
|
31
|
+
def retrieve(self, body):
|
|
32
|
+
self.response._content = body.encode('utf-8')
|
|
33
|
+
frame = pd.DataFrame({'ObjectPropertyId': ['sensor'],
|
|
34
|
+
'TimestampLocal': pd.to_datetime(['2026-06-01'])})
|
|
35
|
+
return self.namespace['get_carbon_calculation_expression'](frame, object())
|
|
36
|
+
|
|
37
|
+
def test_scalar_error_response_logs_diagnostics_and_keeps_fallback(self):
|
|
38
|
+
with self.assertLogs(self.logger, level='ERROR') as captured:
|
|
39
|
+
result = self.retrieve('{"error":"SQL timeout", "access_token":"private-token"}')
|
|
40
|
+
self.assertTrue(result.empty)
|
|
41
|
+
log = ' '.join(captured.output)
|
|
42
|
+
for expected in ('HTTP status=200', 'elapsed_seconds=10.125', 'application/json',
|
|
43
|
+
'exception_type=ValueError'):
|
|
44
|
+
self.assertIn(expected, log)
|
|
45
|
+
self.assertNotIn('private-token', log)
|
|
46
|
+
|
|
47
|
+
def test_malformed_response_content_is_not_logged(self):
|
|
48
|
+
for body in ('{"password":"private-password",',
|
|
49
|
+
'{"api_key":"private-api-key",',
|
|
50
|
+
'<html>password=private-password</html>'):
|
|
51
|
+
with self.subTest(body=body), self.assertLogs(self.logger, level='ERROR') as captured:
|
|
52
|
+
result = self.retrieve(body)
|
|
53
|
+
self.assertTrue(result.empty)
|
|
54
|
+
log = ' '.join(captured.output)
|
|
55
|
+
self.assertNotIn('private-', log)
|
|
56
|
+
self.assertNotIn('response_body=', log)
|
|
57
|
+
|
|
58
|
+
def test_exception_message_is_not_logged(self):
|
|
59
|
+
from unittest.mock import patch
|
|
60
|
+
with patch.object(pd, 'read_json', side_effect=ValueError('private-payload')):
|
|
61
|
+
with self.assertLogs(self.logger, level='ERROR') as captured:
|
|
62
|
+
self.assertTrue(self.retrieve('invalid').empty)
|
|
63
|
+
self.assertNotIn('private-payload', ' '.join(captured.output))
|
|
64
|
+
|
|
65
|
+
def test_empty_and_valid_responses_do_not_log_parse_failures(self):
|
|
66
|
+
for body in ('', '[]', '[{"CarbonExpression":"[Value] * 0.5"}]'):
|
|
67
|
+
with self.subTest(body=body), self.assertLogs(self.logger, level='INFO') as captured:
|
|
68
|
+
result = self.retrieve(body)
|
|
69
|
+
self.assertFalse(any('Failed to parse' in entry for entry in captured.output))
|
|
70
|
+
self.assertEqual(result.empty, body in ('', '[]'))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == '__main__':
|
|
74
|
+
unittest.main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Offline payload regression tests for both Timer deployment helpers."""
|
|
2
|
+
import ast
|
|
3
|
+
import itertools
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest.mock import Mock
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TimerDeploymentTests(unittest.TestCase):
|
|
14
|
+
def test_delivery_settings(self):
|
|
15
|
+
root = Path(__file__).resolve().parents[1]
|
|
16
|
+
for relative, is_api in (
|
|
17
|
+
('switch_api/pipeline/automation.py', True),
|
|
18
|
+
('switch_guides/switch_guides/pipeline/automation.py', False),
|
|
19
|
+
):
|
|
20
|
+
# Execute the actual helper in isolation from unrelated Azure/task imports.
|
|
21
|
+
tree = ast.parse((root / relative).read_text(encoding='utf-8'))
|
|
22
|
+
owner = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == 'Automation') if is_api else tree
|
|
23
|
+
function = next(n for n in owner.body if isinstance(n, ast.FunctionDef) and n.name == 'deploy_on_timer')
|
|
24
|
+
function.decorator_list = []
|
|
25
|
+
function.returns = None
|
|
26
|
+
for argument in function.args.args:
|
|
27
|
+
argument.annotation = None
|
|
28
|
+
requests = SimpleNamespace(post=Mock(return_value=SimpleNamespace(status_code=200, reason='OK', text='', request=SimpleNamespace(url='https://example.invalid'))))
|
|
29
|
+
namespace = dict(uuid=uuid, logger=logging.getLogger(__name__), requests=requests,
|
|
30
|
+
pandas=SimpleNamespace(DataFrame=lambda: None))
|
|
31
|
+
for name, values in dict(EXPECTED_DELIVERY=('Daily',), SCHEDULE_TIMEZONE=('Local',),
|
|
32
|
+
QUEUE_NAME=('task',), TASK_FRAMEWORK=('TaskInsightsEngine',),
|
|
33
|
+
TASK_PRIORITY=('standard',)).items():
|
|
34
|
+
namespace[name] = SimpleNamespace(__args__=values)
|
|
35
|
+
exec(compile(ast.fix_missing_locations(ast.Module(body=[function], type_ignores=[])), relative, 'exec'), namespace)
|
|
36
|
+
deploy = namespace['deploy_on_timer']
|
|
37
|
+
inputs = SimpleNamespace(api_base_url='https://example.invalid', bearer_token='test',
|
|
38
|
+
api_headers=SimpleNamespace(default={}), api_projects_endpoint='https://example.invalid/projects',
|
|
39
|
+
api_project_id=str(uuid.uuid4()))
|
|
40
|
+
task = SimpleNamespace(id=uuid.uuid4(), mapping_entities=['Readings']) if is_api else str(uuid.uuid4())
|
|
41
|
+
args = (task, inputs, uuid.uuid4(), 'Daily', '0 * * * *')
|
|
42
|
+
configurations = dict(email={'emailAddressDomain': 'example.invalid'},
|
|
43
|
+
ftp={'ftpUserName': 'test-user', 'ftpPassword': 'test-only'},
|
|
44
|
+
upload={'uploadUrl': 'enabled'},
|
|
45
|
+
webhook={'clientSigningSecret': 'test-only'})
|
|
46
|
+
for enabled in itertools.product((False, True), repeat=4):
|
|
47
|
+
optional = {key: value for (key, value), include in zip(configurations.items(), enabled) if include}
|
|
48
|
+
with self.subTest(helper=relative, enabled=enabled):
|
|
49
|
+
deploy(*args, settings={'custom': 'value'}, **optional)
|
|
50
|
+
payload = json.loads(json.dumps(requests.post.call_args.kwargs['json']))
|
|
51
|
+
self.assertEqual(payload['sourceType'], 'Timer')
|
|
52
|
+
self.assertEqual(payload['timer'], dict(cronSchedule='0 * * * *', timezoneOffsetMinutes=0, scheduleTimezone='Local'))
|
|
53
|
+
self.assertEqual(payload['settings'], {'custom': 'value'})
|
|
54
|
+
for key in configurations:
|
|
55
|
+
self.assertEqual(payload[key], optional.get(key))
|
|
56
|
+
deploy(*args, **dict.fromkeys(configurations, {}))
|
|
57
|
+
for key in configurations:
|
|
58
|
+
self.assertEqual(requests.post.call_args.kwargs['json'][key], {})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == '__main__':
|
|
62
|
+
unittest.main()
|
|
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.30 → switch_api-0.6.33}/switch_api/_authentication/_credentials_store/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{switch_api-0.6.30 → switch_api-0.6.33}/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
|
|
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
|