switch-api 0.6.28__tar.gz → 0.6.30__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 (79) hide show
  1. {switch_api-0.6.28 → switch_api-0.6.30}/HISTORY.md +33 -0
  2. {switch_api-0.6.28 → switch_api-0.6.30}/PKG-INFO +34 -1
  3. {switch_api-0.6.28 → switch_api-0.6.30}/setup.py +1 -1
  4. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/__init__.py +3 -2
  5. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_authentication/_authentication.py +41 -14
  6. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_utils/_constants.py +10 -5
  7. switch_api-0.6.30/switch_api/_utils/_sql.py +104 -0
  8. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_utils/_utils.py +91 -2
  9. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/controls/controls.py +18 -7
  10. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/initialize.py +7 -3
  11. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/integration/__init__.py +3 -3
  12. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/integration/helpers.py +144 -1
  13. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/integration/integration.py +296 -1
  14. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api.egg-info/PKG-INFO +34 -1
  15. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api.egg-info/SOURCES.txt +3 -0
  16. switch_api-0.6.30/tests/test_authentication.py +57 -0
  17. switch_api-0.6.30/tests/test_controls.py +136 -0
  18. {switch_api-0.6.28 → switch_api-0.6.30}/AUTHORS.rst +0 -0
  19. {switch_api-0.6.28 → switch_api-0.6.30}/LICENCE +0 -0
  20. {switch_api-0.6.28 → switch_api-0.6.30}/MANIFEST.in +0 -0
  21. {switch_api-0.6.28 → switch_api-0.6.30}/README.md +0 -0
  22. {switch_api-0.6.28 → switch_api-0.6.30}/pyproject.toml +0 -0
  23. {switch_api-0.6.28 → switch_api-0.6.30}/setup.cfg +0 -0
  24. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_authentication/__init__.py +0 -0
  25. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_authentication/_credentials_store/__init__.py +0 -0
  26. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_authentication/_credentials_store/_credentials_store.py +0 -0
  27. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_authentication/_msal/__init__.py +0 -0
  28. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_authentication/_msal/_custom_application.py +0 -0
  29. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_compute/__init__.py +0 -0
  30. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_compute/_commands.py +0 -0
  31. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_compute/_compute.py +0 -0
  32. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_compute/_iotservice.py +0 -0
  33. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_compute/_models.py +0 -0
  34. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_compute/_utils.py +0 -0
  35. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_guide/__init__.py +0 -0
  36. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_guide/main.py +0 -0
  37. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_guide/processor.py +0 -0
  38. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_utils/__init__.py +0 -0
  39. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_utils/_marketplace.py +0 -0
  40. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_utils/_mqtt.py +0 -0
  41. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/_utils/_platform.py +0 -0
  42. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/analytics/__init__.py +0 -0
  43. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/analytics/analytics.py +0 -0
  44. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/cache/__init__.py +0 -0
  45. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/cache/cache.py +0 -0
  46. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/controls/__init__.py +0 -0
  47. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/controls/_constants.py +0 -0
  48. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/controls/_enums.py +0 -0
  49. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/controls/_mqtt.py +0 -0
  50. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/dataset/__init__.py +0 -0
  51. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/dataset/dataset.py +0 -0
  52. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/email/__init__.py +0 -0
  53. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/email/email_sender.py +0 -0
  54. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/error_handlers/__init__.py +0 -0
  55. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/error_handlers/error_handlers.py +0 -0
  56. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/extensions/__init__.py +0 -0
  57. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/extensions/extensions.py +0 -0
  58. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/extensions/field_meta.py +0 -0
  59. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/extensions/helpers.py +0 -0
  60. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/extensions/pipeline.py +0 -0
  61. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/integration/_utils.py +0 -0
  62. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/pipeline/__init__.py +0 -0
  63. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/pipeline/automation.py +0 -0
  64. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/pipeline/definitions.py +0 -0
  65. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/pipeline/pipeline.py +0 -0
  66. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/platform_insights/__init__.py +0 -0
  67. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/platform_insights/platform_insights.py +0 -0
  68. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/realtime/__init__.py +0 -0
  69. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/realtime/_mqtt.py +0 -0
  70. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/realtime/_redis.py +0 -0
  71. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api/realtime/realtime.py +0 -0
  72. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api.egg-info/dependency_links.txt +0 -0
  73. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api.egg-info/requires.txt +0 -0
  74. {switch_api-0.6.28 → switch_api-0.6.30}/switch_api.egg-info/top_level.txt +0 -0
  75. {switch_api-0.6.28 → switch_api-0.6.30}/tests/test_dataset.py +0 -0
  76. {switch_api-0.6.28 → switch_api-0.6.30}/tests/test_error_handlers.py +0 -0
  77. {switch_api-0.6.28 → switch_api-0.6.30}/tests/test_initialize.py +0 -0
  78. {switch_api-0.6.28 → switch_api-0.6.30}/tests/test_integration.py +0 -0
  79. {switch_api-0.6.28 → switch_api-0.6.30}/tests/test_pipeline.py +0 -0
@@ -1,5 +1,38 @@
1
1
  # History
2
2
 
3
+ #0.6.30
4
+
5
+ ### Added
6
+
7
+ In the `integration` module:
8
+
9
+ - Added `upsert_sensor_state_history()` to append sensor state history records with site-local, UTC, or named-timezone timestamp handling.
10
+ - Added the `update_current_state` option to `upsert_sensor_state_history()` to update each sensor's current state from its latest history record.
11
+ - Added `update_sensor_state()` to update current sensor states in SQL using validated, batched writes with retry handling.
12
+
13
+ In the `_utils` module:
14
+
15
+ - Added `set_sql_write_config()` and cached SQL write connection helpers using `pyodbc`.
16
+ - Added `get_connection_string_from_local_file()` to load datacentre-specific MQTT, Redis, and SQL write configuration from a local credentials file.
17
+
18
+ ### Changed
19
+
20
+ In the `controls` module:
21
+
22
+ - Updated control components to accept either a single integer priority or a DataFrame column name containing row-level priority values.
23
+
24
+ #0.6.29
25
+
26
+ ### Changed
27
+
28
+ In the `authentication` module:
29
+
30
+ - Added optional `data_center` support to `initialize()`, with `AU` and `US` as the supported values.
31
+ - Authentication login-context and user-profile requests now use the endpoint associated with the selected data center.
32
+ - Interactive production authentication now uses the B2C authority associated with the portfolio's resolved data center (`AustraliaEast` or `CentralUs`).
33
+ - When `data_center` is omitted, authentication defaults to `US` and logs a warning to preserve backward compatibility.
34
+ - Invalid data-center values now raise a `ValueError` with the supported options.
35
+
3
36
  #0.6.18
4
37
  ### Changed
5
38
  In the `realtime` module:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: switch_api
3
- Version: 0.6.28
3
+ Version: 0.6.30
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,39 @@ pip install switch_api
58
58
 
59
59
  # History
60
60
 
61
+ #0.6.30
62
+
63
+ ### Added
64
+
65
+ In the `integration` module:
66
+
67
+ - Added `upsert_sensor_state_history()` to append sensor state history records with site-local, UTC, or named-timezone timestamp handling.
68
+ - Added the `update_current_state` option to `upsert_sensor_state_history()` to update each sensor's current state from its latest history record.
69
+ - Added `update_sensor_state()` to update current sensor states in SQL using validated, batched writes with retry handling.
70
+
71
+ In the `_utils` module:
72
+
73
+ - Added `set_sql_write_config()` and cached SQL write connection helpers using `pyodbc`.
74
+ - Added `get_connection_string_from_local_file()` to load datacentre-specific MQTT, Redis, and SQL write configuration from a local credentials file.
75
+
76
+ ### Changed
77
+
78
+ In the `controls` module:
79
+
80
+ - Updated control components to accept either a single integer priority or a DataFrame column name containing row-level priority values.
81
+
82
+ #0.6.29
83
+
84
+ ### Changed
85
+
86
+ In the `authentication` module:
87
+
88
+ - Added optional `data_center` support to `initialize()`, with `AU` and `US` as the supported values.
89
+ - Authentication login-context and user-profile requests now use the endpoint associated with the selected data center.
90
+ - Interactive production authentication now uses the B2C authority associated with the portfolio's resolved data center (`AustraliaEast` or `CentralUs`).
91
+ - When `data_center` is omitted, authentication defaults to `US` and logs a warning to preserve backward compatibility.
92
+ - Invalid data-center values now raise a `ValueError` with the supported options.
93
+
61
94
  #0.6.18
62
95
  ### Changed
63
96
  In the `realtime` 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.28",
19
+ version="0.6.30",
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',
@@ -11,7 +11,7 @@ Complete package for ingestion data into the Switch Automation Platform.
11
11
  """
12
12
  __all__ = ['dataset', 'analytics', 'pipeline', 'error_handlers', 'integration',
13
13
  'initialize', 'cache', 'extensions', 'email', 'controls', 'realtime', '_compute', 'generate_filepath',
14
- 'set_mqtt_broker_config', 'set_realtime_redis_config']
14
+ 'set_mqtt_broker_config', 'set_realtime_redis_config', 'set_sql_write_config']
15
15
 
16
16
  from .initialize import initialize
17
17
  from . import dataset
@@ -28,6 +28,7 @@ from . import realtime
28
28
  from . import _compute
29
29
  from ._utils._utils import generate_filepath
30
30
  from ._utils._mqtt import set_mqtt_broker_config
31
+ from ._utils._sql import set_sql_write_config
31
32
  from .realtime import set_realtime_redis_config
32
33
  from . import _guide
33
34
 
@@ -44,4 +45,4 @@ from . import _guide
44
45
  # log.addHandler(_ch)
45
46
  # https://dev.to/joaomcteixeira/setting-up-python-logging-for-a-library-app-6ml
46
47
 
47
- __version__ = "0.6.28"
48
+ __version__ = "0.6.30"
@@ -5,13 +5,14 @@
5
5
  # --------------------------------------------------------------------------
6
6
  import logging
7
7
  import sys
8
+ from typing import Optional
8
9
  from uuid import UUID
9
10
  from msal.oauth2cli.oidc import decode_id_token
10
11
  import requests
11
12
  from ._credentials_store._credentials_store import SwitchCredentials, store_credentials
12
13
  from ._credentials_store._credentials_store import read_credentials
13
14
  from ._msal._custom_application import CustomPublicClientApplication
14
- from .._utils._constants import AUTH_ENDPOINT_DEV, AUTH_ENDPOINT_PROD, SWITCH_ENVIRONMENT
15
+ from .._utils._constants import AUTH_ENDPOINTS, DATA_CENTER, SWITCH_ENVIRONMENT
15
16
 
16
17
  logger = logging.getLogger(__name__)
17
18
  logger.setLevel(logging.DEBUG)
@@ -30,8 +31,14 @@ _authority = {
30
31
  "base_url": "https://switchdevb2c.b2clogin.com/switchdevb2c.onmicrosoft.com",
31
32
  },
32
33
  "prod": {
33
- "base_url": "https://switchautomation.b2clogin.com/switchautomation.onmicrosoft.com",
34
- "uitemplateid": "default"
34
+ "AustraliaEast": {
35
+ "base_url": "https://switchautomationapac.b2clogin.com/7d5aa686-9840-42a5-9334-084b75385e24",
36
+ "uitemplateid": "default"
37
+ },
38
+ "CentralUs": {
39
+ "base_url": "https://switchautomation.b2clogin.com/switchautomation.onmicrosoft.com",
40
+ "uitemplateid": "default"
41
+ }
35
42
  }
36
43
  }
37
44
 
@@ -69,7 +76,17 @@ def _validate_credentials(creds: SwitchCredentials, version: str):
69
76
  return False
70
77
 
71
78
 
72
- def get_switch_credentials(environment: SWITCH_ENVIRONMENT, portfolio_id, port: int):
79
+ def _resolve_data_center(data_center: Optional[DATA_CENTER]) -> DATA_CENTER:
80
+ if data_center is None:
81
+ logger.warning('No data center was provided. Defaulting to US.')
82
+ return 'US'
83
+ if data_center not in AUTH_ENDPOINTS:
84
+ raise ValueError("data_center must be either 'AU' or 'US'.")
85
+ return data_center
86
+
87
+
88
+ def get_switch_credentials(environment: SWITCH_ENVIRONMENT, portfolio_id, port: int,
89
+ data_center: Optional[DATA_CENTER] = None):
73
90
  """Read Credentials
74
91
 
75
92
  Reads credentials from credential store
@@ -82,6 +99,8 @@ def get_switch_credentials(environment: SWITCH_ENVIRONMENT, portfolio_id, port:
82
99
  Get credentials for the given portfolio id
83
100
  port: int
84
101
  Port used for instantiating local server for authentication
102
+ data_center
103
+ Data center hosting the portfolio. Defaults to US with a warning when omitted.
85
104
 
86
105
  Returns
87
106
  -------
@@ -90,8 +109,9 @@ def get_switch_credentials(environment: SWITCH_ENVIRONMENT, portfolio_id, port:
90
109
 
91
110
  """
92
111
 
112
+ data_center = _resolve_data_center(data_center)
93
113
  login_context = fetch_login_context(
94
- environment=environment, api_project_id=portfolio_id)[1]
114
+ environment=environment, api_project_id=portfolio_id, data_center=data_center)[1]
95
115
 
96
116
  # Fetch from credentials store if it's already been cached
97
117
  iot_endpoint = 'https://realtime-au.switchautomation.com/iot'
@@ -109,11 +129,11 @@ def get_switch_credentials(environment: SWITCH_ENVIRONMENT, portfolio_id, port:
109
129
 
110
130
  # Credentials do not exist, take user through browser auth journey
111
131
  config_env = 'dev' if environment == 'Development' else 'prod'
112
- authority = f"{_authority[config_env]['base_url']}/{login_context['policyName']}"
132
+ authority = f"{_authority[config_env][datacenter]['base_url']}/{login_context['policyName']}"
113
133
  app = CustomPublicClientApplication(
114
134
  login_context['applicationId'], authority=authority)
115
135
 
116
- uitemplateid = _authority[config_env]['uitemplateid']
136
+ uitemplateid = _authority[config_env][datacenter]['uitemplateid']
117
137
  prompt = login_context['prompt'] if login_context['prompt'] != '' else None
118
138
  auth_response = app.acquire_token_interactive_custom(scopes=[], prompt=prompt, port=port,
119
139
  auth_params={
@@ -127,9 +147,8 @@ def get_switch_credentials(environment: SWITCH_ENVIRONMENT, portfolio_id, port:
127
147
 
128
148
  id_token = auth_response['id_token']
129
149
 
130
- api_base_url = AUTH_ENDPOINT_DEV if environment == 'Development' else AUTH_ENDPOINT_PROD
131
150
  _, profile = fetch_user_profile_details(
132
- api_base_url, id_token, environment, portfolio_id)
151
+ id_token, environment, portfolio_id, data_center)
133
152
 
134
153
  if profile is None:
135
154
  raise Exception(
@@ -145,20 +164,21 @@ def get_switch_credentials(environment: SWITCH_ENVIRONMENT, portfolio_id, port:
145
164
  return credentials
146
165
 
147
166
 
148
- def fetch_user_profile_details(api_base_url: str, token: str, environment: SWITCH_ENVIRONMENT, api_project_id: UUID):
167
+ def fetch_user_profile_details(token: str, environment: SWITCH_ENVIRONMENT, api_project_id: UUID,
168
+ data_center: Optional[DATA_CENTER] = None):
149
169
  """
150
170
  Fetch User Profile details with respect to the given environment and portfolio
151
171
 
152
172
  Parameters
153
173
  ----------
154
- api_base_url
155
- User Profile base url
156
174
  token
157
175
  Switch token for authentication
158
176
  environment
159
177
  Either development or production environment
160
178
  api_project_id
161
179
  Get UserProfile with respect to this portfolio id
180
+ data_center
181
+ Data center hosting the portfolio. Allowed values are AU and US. Defaults to US with a warning when omitted.
162
182
 
163
183
  Returns
164
184
  -------
@@ -166,6 +186,9 @@ def fetch_user_profile_details(api_base_url: str, token: str, environment: SWITC
166
186
  Returns the User Profile details containing information to call Switch APIs
167
187
  """
168
188
 
189
+ data_center = _resolve_data_center(data_center)
190
+ api_base_url = AUTH_ENDPOINTS[data_center]
191
+
169
192
  payload = {}
170
193
  headers = {
171
194
  'Content-Type': 'application/json; charset=utf-8',
@@ -188,7 +211,8 @@ def fetch_user_profile_details(api_base_url: str, token: str, environment: SWITC
188
211
  return response_status, response.json()
189
212
 
190
213
 
191
- def fetch_login_context(environment: SWITCH_ENVIRONMENT, api_project_id: UUID):
214
+ def fetch_login_context(environment: SWITCH_ENVIRONMENT, api_project_id: UUID,
215
+ data_center: Optional[DATA_CENTER] = None):
192
216
  """
193
217
  Fetches login context to assist SSO with Switch Platform
194
218
 
@@ -198,6 +222,8 @@ def fetch_login_context(environment: SWITCH_ENVIRONMENT, api_project_id: UUID):
198
222
  Either development or production environment
199
223
  api_project_id
200
224
  Get UserProfile with respect to this portfolio id
225
+ data_center
226
+ Data center hosting the portfolio. Allowed values are AU and US. Defaults to US with a warning when omitted.
201
227
 
202
228
  Returns
203
229
  -------
@@ -205,7 +231,8 @@ def fetch_login_context(environment: SWITCH_ENVIRONMENT, api_project_id: UUID):
205
231
  Returns the Login Context containing information to authenticate with Switch
206
232
  """
207
233
 
208
- base_url = AUTH_ENDPOINT_DEV if environment == 'Development' else AUTH_ENDPOINT_PROD
234
+ data_center = _resolve_data_center(data_center)
235
+ base_url = AUTH_ENDPOINTS[data_center]
209
236
 
210
237
  payload = {}
211
238
  headers = {
@@ -10,8 +10,9 @@ is not directly referenced by end users.
10
10
  from typing import Literal
11
11
  __all__ = ['api_prefix', 'argus_prefix', 'DATETIME_COL_FMT', 'DEPLOY_TYPE', 'EXPECTED_DELIVERY', 'WORK_ORDER_PRIORITY',
12
12
  'WORK_ORDER_STATUS', 'WORK_ORDER_CATEGORY', 'ERROR_TYPE', 'MAPPING_ENTITIES', 'QUEUE_NAME', 'QUERY_LANGUAGE',
13
- 'ACCOUNT', 'PROCESS_STATUS', 'AUTH_ENDPOINT_DEV', 'AUTH_ENDPOINT_PROD', 'SCHEDULE_TIMEZONE',
14
- 'SWITCH_ENVIRONMENT', 'INTEGRATION_SETTINGS_EDITORS', 'DATA_INGESTION_CONTAINER', 'CACHE_SCOPE', 'TAG_LEVEL']
13
+ 'ACCOUNT', 'PROCESS_STATUS', 'AUTH_ENDPOINTS', 'SCHEDULE_TIMEZONE',
14
+ 'SWITCH_ENVIRONMENT', 'DATA_CENTER', 'INTEGRATION_SETTINGS_EDITORS', 'DATA_INGESTION_CONTAINER',
15
+ 'CACHE_SCOPE', 'TAG_LEVEL']
15
16
 
16
17
  # @deprecated suggestion: Up for deprecation as this is not used anymore in the API
17
18
  # api_prefix = "http://localhost:7071/api/SwitchApi/"
@@ -21,8 +22,10 @@ api_prefix = "https://pivotstreams.azurewebsites.net/api/SwitchApi/"
21
22
  # argus_prefix = "http://localhost:7071/api/"
22
23
  argus_prefix = "https://arguslogicv4b.azurewebsites.net/api/"
23
24
 
24
- AUTH_ENDPOINT_DEV = "https://restapi-dev.switchautomation.com/auth"
25
- AUTH_ENDPOINT_PROD = "https://restapi-us.switchautomation.com/auth"
25
+ AUTH_ENDPOINTS = {
26
+ 'AU': 'https://restapi-ae.switchautomation.com/auth',
27
+ 'US': 'https://restapi-us.switchautomation.com/auth'
28
+ }
26
29
 
27
30
  DATETIME_COL_FMT = Literal['DateTime', 'Date', 'Time']
28
31
 
@@ -68,6 +71,8 @@ SCHEDULE_TIMEZONE = Literal['Local', 'Utc']
68
71
 
69
72
  SWITCH_ENVIRONMENT = Literal['Development', 'Staging', 'Testing', 'Production']
70
73
 
74
+ DATA_CENTER = Literal['AU', 'US']
75
+
71
76
  SUPPORT_PAYLOAD_TYPE = Literal['Sites', 'Sensors', 'Portfolio']
72
77
 
73
78
  INTEGRATION_SETTINGS_EDITORS = Literal['text_box', 'numeric_stepper', 'custom_combo', 'tag_groups_combo',
@@ -101,4 +106,4 @@ WS_DEFAULT_PORT = 443
101
106
  WS_MQTT_CONNECTION_TIMEOUT = 180
102
107
  WS_MQTT_DEFAULT_MAX_TIMEOUT = 180
103
108
  WS_MQTT_WAIT_TIME_INTERVAL = 0.1
104
- WS_MQTT_MESSAGE_WAIT_TIME_INTERVAL = 0.1
109
+ WS_MQTT_MESSAGE_WAIT_TIME_INTERVAL = 0.1
@@ -0,0 +1,104 @@
1
+ # -------------------------------------------------------------------------
2
+ # Copyright (c) Switch Automation Pty Ltd. All rights reserved.
3
+ # Licensed under the MIT License. See License.txt in the project root for
4
+ # license information.
5
+ # --------------------------------------------------------------------------
6
+ """
7
+ SQL write configuration and connection helpers.
8
+ """
9
+ import logging
10
+ import sys
11
+
12
+ import pyodbc
13
+
14
+ logger = logging.getLogger(__name__)
15
+ logger.setLevel(logging.DEBUG)
16
+ consoleHandler = logging.StreamHandler(stream=sys.stdout)
17
+ consoleHandler.setLevel(logging.INFO)
18
+
19
+ logger.addHandler(consoleHandler)
20
+ formatter = logging.Formatter('%(asctime)s switch_api.%(module)s.%(funcName)s %(levelname)s: %(message)s',
21
+ datefmt='%Y-%m-%dT%H:%M:%S')
22
+ consoleHandler.setFormatter(formatter)
23
+
24
+
25
+ global _sql_write_config
26
+ global _sql_write_conn
27
+
28
+ _sql_write_config = {
29
+ "connection_string": "",
30
+ }
31
+ _sql_write_conn = None
32
+
33
+
34
+ def _reset_sql_write_conn():
35
+ """
36
+ Close the cached SQL write connection and reset it to None.
37
+ """
38
+ global _sql_write_conn
39
+
40
+ if _sql_write_conn is not None:
41
+ try:
42
+ _sql_write_conn.close()
43
+ except Exception as e:
44
+ logger.warning(f"Failed to close cached SQL write connection: {e}")
45
+
46
+ _sql_write_conn = None
47
+
48
+
49
+ def set_sql_write_config(connection_string: str):
50
+ """
51
+ Set SQL configuration.
52
+
53
+ Parameters
54
+ ----------
55
+ connection_string : str
56
+ The pyodbc connection string used for SQL write operations.
57
+
58
+ Raises
59
+ ------
60
+ ValueError
61
+ If connection_string is not a non-empty string.
62
+ """
63
+ global _sql_write_config
64
+
65
+ if not isinstance(connection_string, str) or not connection_string.strip():
66
+ raise ValueError("connection_string must be a non-empty string.")
67
+
68
+ _reset_sql_write_conn()
69
+ _sql_write_config = {
70
+ "connection_string": connection_string,
71
+ }
72
+ logger.info("Set SQL configuration")
73
+
74
+
75
+ def is_sql_write_configured():
76
+ """
77
+ Return whether SQL write configuration has been set.
78
+ """
79
+ return bool(_sql_write_config["connection_string"])
80
+
81
+
82
+ def get_sql_write_config():
83
+ """
84
+ Return a copy of the current SQL write configuration.
85
+ """
86
+ return dict(_sql_write_config)
87
+
88
+
89
+ def _get_sql_conn():
90
+ """
91
+ Return the cached SQL write connection, creating it when required.
92
+ """
93
+ global _sql_write_conn
94
+
95
+ if not is_sql_write_configured():
96
+ raise ValueError("SQL write configuration has not been set.")
97
+
98
+ if _sql_write_conn is None:
99
+ logger.info("Creating SQL write connection.")
100
+ _sql_write_conn = pyodbc.connect(
101
+ _sql_write_config["connection_string"]
102
+ )
103
+
104
+ return _sql_write_conn
@@ -27,7 +27,8 @@ from .._utils._constants import (WORK_ORDER_STATUS, WORK_ORDER_CATEGORY, WORK_OR
27
27
  PERFORMANCE_STATISTIC_METRIC_SYSTEM, RESERVATION_STATUS, RESOURCE_TYPE)
28
28
 
29
29
  __all__ = ['generate_password', 'convert_to_sqm', 'convert_to_pascal_case', 'ValidatedSetterProperty', 'ApiInputs',
30
- 'DiscoveryIntegrationInput', 'DataFeedFileProcessOutput', 'requests_retry_session', 'is_valid_uuid', 'convert_bytes']
30
+ 'DiscoveryIntegrationInput', 'DataFeedFileProcessOutput', 'requests_retry_session', 'is_valid_uuid',
31
+ 'convert_bytes', 'get_connection_string_from_local_file']
31
32
 
32
33
  logger = logging.getLogger(__name__)
33
34
  logger.setLevel(logging.DEBUG)
@@ -132,6 +133,94 @@ DataFeedFileProcessOutput = namedtuple('DataFeedFileProcessOutput',
132
133
  ])
133
134
 
134
135
 
136
+ def get_connection_string_from_local_file(filepath: str, api_inputs: ApiInputs):
137
+ """Load local MQTT, Redis, and SQL write credentials for a datacentre.
138
+
139
+ The credentials file must contain ``KEY==VALUE`` entries for the AU and
140
+ US datacentres. The datacentre is determined from ``api_inputs.api_base_url``.
141
+
142
+ Parameters
143
+ ----------
144
+ filepath : str
145
+ Path to the local credentials file.
146
+ api_inputs : ApiInputs
147
+ Initialized API inputs containing the API base URL.
148
+
149
+ Returns
150
+ -------
151
+ dict or bool
152
+ MQTT, Redis, and SQL write configuration dictionaries, or False when
153
+ the file or datacentre configuration is invalid.
154
+ """
155
+ if not os.path.isfile(filepath):
156
+ logger.error(f"The provided filepath: {filepath} is not valid")
157
+ return False
158
+
159
+ try:
160
+ with open(filepath, 'r') as file:
161
+ file_content = file.read().splitlines()
162
+
163
+ tmp = dict(
164
+ line.strip().split('==', 1)
165
+ for line in file_content
166
+ if line.strip()
167
+ )
168
+ except (OSError, ValueError) as exc:
169
+ logger.error(f"Unable to read credentials from filepath: {filepath}. {exc}")
170
+ return False
171
+
172
+ try:
173
+ dc = api_inputs.api_base_url.split('-', 1)[1].split('.')[0]
174
+ except (AttributeError, IndexError):
175
+ logger.error("Unable to determine datacentre from ApiInputs['api_base_url']")
176
+ return False
177
+
178
+ if dc == "ae":
179
+ suffix = "_AU"
180
+ elif dc == "us":
181
+ suffix = "_US"
182
+ else:
183
+ logger.error("Unable to determine datacentre from ApiInputs['api_base_url']")
184
+ return False
185
+
186
+ try:
187
+ controls_platform_iot_endpoint = tmp['PLATFORM_IOT_ENDPOINT' + suffix]
188
+ controls_mqtt_host = tmp['MQTT_HOST' + suffix]
189
+ controls_mqtt_port = tmp['MQTT_PORT' + suffix]
190
+ controls_mqtt_username = tmp['MQTT_USERNAME' + suffix]
191
+ controls_mqtt_password = tmp['MQTT_PASSWORD' + suffix]
192
+ controls_max_timeout = tmp['CONTROLS_MAX_TIMEOUT' + suffix]
193
+
194
+ redis_host = tmp['REDIS_HOST' + suffix]
195
+ redis_password = tmp['REDIS_PASSWORD' + suffix]
196
+ sql_write_connection_string = tmp['SQL_WRITE_CONNECTION_STRING' + suffix]
197
+
198
+ control_variables = {
199
+ 'iot_endpoint': controls_platform_iot_endpoint,
200
+ 'mqtt_host': controls_mqtt_host,
201
+ 'mqtt_username': controls_mqtt_username,
202
+ 'mqtt_password': controls_mqtt_password,
203
+ 'mqtt_port': int(controls_mqtt_port),
204
+ 'max_timeout': int(controls_max_timeout)
205
+ }
206
+ redis_variables = {
207
+ 'redis_host': redis_host,
208
+ 'redis_password': redis_password
209
+ }
210
+ sql_write_variables = {
211
+ 'connection_string': sql_write_connection_string
212
+ }
213
+ except (KeyError, TypeError, ValueError) as exc:
214
+ logger.error(f"Invalid local credentials for datacentre {dc}: {exc}")
215
+ return False
216
+
217
+ return {
218
+ 'mqtt_config': control_variables,
219
+ 'redis_config': redis_variables,
220
+ 'sql_write_config': sql_write_variables
221
+ }
222
+
223
+
135
224
  def generate_password():
136
225
  """Generate a ten-character alphanumeric password with at least one lowercase character, at least one uppercase
137
226
  character, and at least three digits.
@@ -530,4 +619,4 @@ def ensure_uuid(value, field_name):
530
619
  return uuid.UUID(value)
531
620
  except ValueError:
532
621
  pass
533
- raise ValueError(f"{field_name} must be a valid UUID or UUID string.")
622
+ raise ValueError(f"{field_name} must be a valid UUID or UUID string.")
@@ -218,6 +218,11 @@ def submit_control_continue(api_inputs: ApiInputs, data: pandas.DataFrame, infor
218
218
  f"Default Value column {column_with_default_value} not found in the DataFrame")
219
219
  raise ValueError(
220
220
  f"Default Value column {column_with_default_value} not found in the DataFrame")
221
+ elif isinstance(priority, str) and priority not in data.columns:
222
+ logger.error(
223
+ f"Priority column {priority} not found in the DataFrame")
224
+ raise ValueError(
225
+ f"Priority column {priority} not found in the DataFrame")
221
226
 
222
227
  data[column_with_value] = pandas.to_numeric(
223
228
  data[column_with_value], errors='coerce')
@@ -228,6 +233,7 @@ def submit_control_continue(api_inputs: ApiInputs, data: pandas.DataFrame, infor
228
233
 
229
234
  control_df = pandas.DataFrame()
230
235
  group[COL_CONTROL_NAME] = name
236
+ control_priority = group[priority] if isinstance(priority, str) else priority
231
237
 
232
238
  if column_with_default_value == None:
233
239
  control_df = pandas.DataFrame({
@@ -237,7 +243,7 @@ def submit_control_continue(api_inputs: ApiInputs, data: pandas.DataFrame, infor
237
243
  COL_CONTROL_VALUE: group[column_with_value],
238
244
  COL_CONTROL_CONTINUE_VALUE: group[column_with_continue],
239
245
  COL_CONTROL_TTL: timeout_in_seconds,
240
- COL_CONTROL_PRIORITY: priority
246
+ COL_CONTROL_PRIORITY: control_priority
241
247
  })
242
248
  else:
243
249
  control_df = pandas.DataFrame({
@@ -248,7 +254,7 @@ def submit_control_continue(api_inputs: ApiInputs, data: pandas.DataFrame, infor
248
254
  COL_CONTROL_CONTINUE_VALUE: group[column_with_continue],
249
255
  COL_CONTROL_DEFAULT_CONTROL_VALUE: group[column_with_default_value],
250
256
  COL_CONTROL_TTL: timeout_in_seconds,
251
- COL_CONTROL_PRIORITY: priority
257
+ COL_CONTROL_PRIORITY: control_priority
252
258
  })
253
259
 
254
260
  try:
@@ -958,7 +964,7 @@ def set_control_cache(api_inputs: ApiInputs, key: str, scope_id: str, data: any)
958
964
  f"An unexpected error occurred in setting Control cache: {e}")
959
965
  return pandas.DataFrame()
960
966
 
961
- def add_control_component(name: str, column_with_object_installation_id: str, column_with_object_property_id: str, column_with_label: str, column_with_value: str, column_with_continue: str = None, column_with_default_value: str = None, timeout_in_seconds: int = 0, priority: int = 8):
967
+ def add_control_component(name: str, column_with_object_installation_id: str, column_with_object_property_id: str, column_with_label: str, column_with_value: str, column_with_continue: str = None, column_with_default_value: str = None, timeout_in_seconds: int = 0, priority: Union[int, str] = 8):
962
968
  """Add Control Components similar to batching the dataframe with named columns.
963
969
 
964
970
  Parameters
@@ -981,8 +987,9 @@ def add_control_component(name: str, column_with_object_installation_id: str, co
981
987
  Time until relinquish happens in seconds. Defaults to 0.
982
988
  Setting value to 0 means the control will not relinquish.
983
989
  If there's value > 0 then it will always relinquish control.
984
- priority (int, optional):
985
- Priority value for the control write command. Defaults to 8.
990
+ priority (int or str, optional):
991
+ Priority value for the control write command, or the name of the
992
+ DataFrame column containing row-level priority values. Defaults to 8.
986
993
 
987
994
  Returns
988
995
  -------
@@ -1019,9 +1026,13 @@ def add_control_component(name: str, column_with_object_installation_id: str, co
1019
1026
  if not isinstance(timeout_in_seconds, int) or timeout_in_seconds < 0:
1020
1027
  raise ValueError(
1021
1028
  "The 'timeout_in_seconds' parameter must be a non-negative integer.")
1022
- if not isinstance(priority, int) or priority < 0:
1029
+ if isinstance(priority, str):
1030
+ if not priority:
1031
+ raise ValueError(
1032
+ "The 'priority' parameter must be a non-empty string or non-negative integer.")
1033
+ elif not isinstance(priority, int) or priority < 0:
1023
1034
  raise ValueError(
1024
- "The 'priority' parameter must be a non-negative integer.")
1035
+ "The 'priority' parameter must be a non-empty string or non-negative integer.")
1025
1036
 
1026
1037
  _control_components[name] = (
1027
1038
  column_with_object_installation_id,
@@ -6,10 +6,11 @@
6
6
  import sys
7
7
  import uuid
8
8
  import logging
9
+ from typing import Optional
9
10
 
10
11
  from ._authentication._authentication import get_switch_credentials
11
12
  from ._utils._utils import ApiInputs, ApiHeaders
12
- from ._utils._constants import SWITCH_ENVIRONMENT
13
+ from ._utils._constants import DATA_CENTER, SWITCH_ENVIRONMENT
13
14
 
14
15
  logger = logging.getLogger(__name__)
15
16
  logger.setLevel(logging.DEBUG)
@@ -23,7 +24,8 @@ formatter = logging.Formatter('%(asctime)s switch_api.%(module)s.%(funcName)s
23
24
  consoleHandler.setFormatter(formatter)
24
25
 
25
26
 
26
- def initialize(api_project_id: uuid.UUID, environment: SWITCH_ENVIRONMENT = 'Production', custom_port: int = 51796) -> ApiInputs:
27
+ def initialize(api_project_id: uuid.UUID, environment: SWITCH_ENVIRONMENT = 'Production', custom_port: int = 51796,
28
+ data_center: Optional[DATA_CENTER] = None) -> ApiInputs:
27
29
  """
28
30
  Initialize session with API when running scripts on your local machine.
29
31
 
@@ -35,6 +37,8 @@ def initialize(api_project_id: uuid.UUID, environment: SWITCH_ENVIRONMENT = 'Pro
35
37
  (Optional) The Switch API environment to interact with
36
38
  custom_port: int
37
39
  (Optional) Custom network port to initialize instance of the server when authenticating
40
+ data_center: DATA_CENTER
41
+ (Optional) Data center hosting the portfolio. Defaults to US with a warning when omitted.
38
42
 
39
43
  Returns
40
44
  -------
@@ -48,7 +52,7 @@ def initialize(api_project_id: uuid.UUID, environment: SWITCH_ENVIRONMENT = 'Pro
48
52
  f"Invalid value provided '{api_project_id}'- please provide a valid api_project_id. ")
49
53
  return False
50
54
 
51
- creds = get_switch_credentials(environment, api_project_id, custom_port)
55
+ creds = get_switch_credentials(environment, api_project_id, custom_port, data_center)
52
56
 
53
57
  logger.info("Successfully initialized.")
54
58
  api_base_url = f"https://{creds.api_endpoint}/api/1.0"
@@ -10,12 +10,12 @@ A module for integrating asset creation, asset updates, data ingestion, etc into
10
10
  from .integration import (upsert_data, upsert_sites, upsert_workorders, upsert_device_sensors, upsert_device_sensors_ext,
11
11
  upsert_timeseries_ds, replace_data, append_data, upsert_file_row_count,
12
12
  upsert_discovered_records, upsert_timeseries, upsert_tags, upsert_device_metadata,
13
- upsert_reservations, upsert_device_sensors_iq)
13
+ upsert_reservations, upsert_device_sensors_iq, upsert_sensor_state_history)
14
14
 
15
15
  from .helpers import (get_sites, get_tag_groups, get_metadata_keys, get_data, get_device_sensors, get_templates,
16
16
  get_units_of_measure, get_states_by_country, get_operation_state, get_equipment_classes,
17
17
  load_data, get_timezones, amortise_across_days, get_metadata_where_clause, connect_to_sql,
18
- update_last_record_property_value)
18
+ update_last_record_property_value, update_sensor_state)
19
19
 
20
20
  __all__ = ['upsert_data', 'upsert_sites', 'upsert_workorders', 'upsert_device_sensors', 'upsert_device_sensors_ext',
21
21
  'upsert_timeseries_ds', 'replace_data', 'append_data', 'get_sites', 'get_tag_groups', 'get_metadata_keys',
@@ -23,4 +23,4 @@ __all__ = ['upsert_data', 'upsert_sites', 'upsert_workorders', 'upsert_device_se
23
23
  'get_operation_state', 'get_equipment_classes', 'load_data', 'upsert_file_row_count',
24
24
  'upsert_discovered_records', 'upsert_timeseries', 'upsert_tags', 'upsert_device_metadata', 'get_timezones',
25
25
  'amortise_across_days', 'get_metadata_where_clause', 'connect_to_sql', 'upsert_reservations', 'upsert_device_sensors_iq',
26
- 'update_last_record_property_value']
26
+ 'update_last_record_property_value', 'update_sensor_state', 'upsert_sensor_state_history']