switch-api 0.6.27__tar.gz → 0.6.29__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.27 → switch_api-0.6.29}/HISTORY.md +12 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/PKG-INFO +13 -1
- {switch_api-0.6.27 → switch_api-0.6.29}/setup.py +1 -1
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/__init__.py +1 -1
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_authentication/_authentication.py +41 -14
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_utils/_constants.py +10 -5
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_utils/_mqtt.py +20 -14
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/initialize.py +7 -3
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/integration/_utils.py +1 -1
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/realtime/_mqtt.py +14 -14
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/realtime/_redis.py +45 -18
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/realtime/realtime.py +1 -1
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api.egg-info/PKG-INFO +13 -1
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api.egg-info/SOURCES.txt +1 -0
- switch_api-0.6.29/tests/test_authentication.py +57 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/AUTHORS.rst +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/LICENCE +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/MANIFEST.in +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/README.md +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/pyproject.toml +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/setup.cfg +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_authentication/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_authentication/_credentials_store/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_authentication/_credentials_store/_credentials_store.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_authentication/_msal/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_authentication/_msal/_custom_application.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_compute/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_compute/_commands.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_compute/_compute.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_compute/_iotservice.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_compute/_models.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_compute/_utils.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_guide/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_guide/main.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_guide/processor.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_utils/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_utils/_marketplace.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_utils/_platform.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_utils/_utils.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/analytics/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/analytics/analytics.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/cache/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/cache/cache.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/controls/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/controls/_constants.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/controls/_enums.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/controls/_mqtt.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/controls/controls.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/dataset/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/dataset/dataset.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/email/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/email/email_sender.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/error_handlers/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/error_handlers/error_handlers.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/extensions/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/extensions/extensions.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/extensions/field_meta.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/extensions/helpers.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/extensions/pipeline.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/integration/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/integration/helpers.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/integration/integration.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/pipeline/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/pipeline/automation.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/pipeline/definitions.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/pipeline/pipeline.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/platform_insights/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/platform_insights/platform_insights.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/realtime/__init__.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api.egg-info/dependency_links.txt +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api.egg-info/requires.txt +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/switch_api.egg-info/top_level.txt +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/tests/test_dataset.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/tests/test_error_handlers.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/tests/test_initialize.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/tests/test_integration.py +0 -0
- {switch_api-0.6.27 → switch_api-0.6.29}/tests/test_pipeline.py +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# History
|
|
2
2
|
|
|
3
|
+
#0.6.29
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
In the `authentication` module:
|
|
8
|
+
|
|
9
|
+
- Added optional `data_center` support to `initialize()`, with `AU` and `US` as the supported values.
|
|
10
|
+
- Authentication login-context and user-profile requests now use the endpoint associated with the selected data center.
|
|
11
|
+
- Interactive production authentication now uses the B2C authority associated with the portfolio's resolved data center (`AustraliaEast` or `CentralUs`).
|
|
12
|
+
- When `data_center` is omitted, authentication defaults to `US` and logs a warning to preserve backward compatibility.
|
|
13
|
+
- Invalid data-center values now raise a `ValueError` with the supported options.
|
|
14
|
+
|
|
3
15
|
#0.6.18
|
|
4
16
|
### Changed
|
|
5
17
|
In the `realtime` module:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: switch_api
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.29
|
|
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,18 @@ pip install switch_api
|
|
|
58
58
|
|
|
59
59
|
# History
|
|
60
60
|
|
|
61
|
+
#0.6.29
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
In the `authentication` module:
|
|
66
|
+
|
|
67
|
+
- Added optional `data_center` support to `initialize()`, with `AU` and `US` as the supported values.
|
|
68
|
+
- Authentication login-context and user-profile requests now use the endpoint associated with the selected data center.
|
|
69
|
+
- Interactive production authentication now uses the B2C authority associated with the portfolio's resolved data center (`AustraliaEast` or `CentralUs`).
|
|
70
|
+
- When `data_center` is omitted, authentication defaults to `US` and logs a warning to preserve backward compatibility.
|
|
71
|
+
- Invalid data-center values now raise a `ValueError` with the supported options.
|
|
72
|
+
|
|
61
73
|
#0.6.18
|
|
62
74
|
### Changed
|
|
63
75
|
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.
|
|
19
|
+
version="0.6.29",
|
|
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',
|
|
@@ -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
|
|
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
|
-
"
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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', '
|
|
14
|
-
'SWITCH_ENVIRONMENT', '
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
@@ -17,7 +17,7 @@ global _control_ws_port
|
|
|
17
17
|
global _control_ws_username
|
|
18
18
|
global _control_ws_password
|
|
19
19
|
global _control_ws_max_timeout
|
|
20
|
-
global
|
|
20
|
+
global _control_ws_ca_certs
|
|
21
21
|
|
|
22
22
|
_control_api_endpoint = ''
|
|
23
23
|
_control_ws_host = ''
|
|
@@ -25,7 +25,7 @@ _control_ws_port = WS_DEFAULT_PORT
|
|
|
25
25
|
_control_ws_username = ''
|
|
26
26
|
_control_ws_password = ''
|
|
27
27
|
_control_ws_max_timeout = WS_MQTT_DEFAULT_MAX_TIMEOUT
|
|
28
|
-
|
|
28
|
+
_control_ws_ca_certs = ''
|
|
29
29
|
|
|
30
30
|
def get_control_config():
|
|
31
31
|
"""
|
|
@@ -53,10 +53,10 @@ def get_control_config():
|
|
|
53
53
|
Username for MQTT authentication.
|
|
54
54
|
- password : str
|
|
55
55
|
Password for MQTT authentication.
|
|
56
|
-
- ssl_pwd : str
|
|
57
|
-
Password for SSL authentication.
|
|
58
56
|
- max_timeout : int
|
|
59
57
|
Maximum timeout (in seconds) for control operations.
|
|
58
|
+
- ca_certs : str (optional)
|
|
59
|
+
A string path to the Certificate Authority certificate files in Server
|
|
60
60
|
"""
|
|
61
61
|
return {
|
|
62
62
|
"api_endpoint": _control_api_endpoint,
|
|
@@ -64,13 +64,19 @@ def get_control_config():
|
|
|
64
64
|
"ws_port": _control_ws_port,
|
|
65
65
|
"username": _control_ws_username,
|
|
66
66
|
"password": _control_ws_password,
|
|
67
|
-
"ssl_pwd": _control_ws_ssl_pwd,
|
|
68
67
|
"max_timeout": _control_ws_max_timeout,
|
|
68
|
+
"ca_certs": _control_ws_ca_certs
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
def set_mqtt_broker_config(api_endpoint: str,
|
|
73
|
-
|
|
72
|
+
def set_mqtt_broker_config(api_endpoint: str,
|
|
73
|
+
ws_host: str,
|
|
74
|
+
user_name: str,
|
|
75
|
+
password: str,
|
|
76
|
+
ws_port: int = WS_DEFAULT_PORT,
|
|
77
|
+
max_timeout: int = WS_MQTT_DEFAULT_MAX_TIMEOUT,
|
|
78
|
+
ca_certs: str = None,
|
|
79
|
+
bypass_username_password: bool = False):
|
|
74
80
|
"""Set Control Variables
|
|
75
81
|
|
|
76
82
|
Set Control Variables needed to enable control request to MQTT Broker when running locally.
|
|
@@ -98,17 +104,17 @@ def set_mqtt_broker_config(api_endpoint: str, ws_host: str, user_name: str, pass
|
|
|
98
104
|
global _control_ws_username
|
|
99
105
|
global _control_ws_password
|
|
100
106
|
global _control_ws_max_timeout
|
|
101
|
-
global
|
|
107
|
+
global _control_ws_ca_certs
|
|
102
108
|
|
|
103
109
|
# Check if endpoint is a valid URL
|
|
104
110
|
if not api_endpoint.startswith('https://'):
|
|
105
111
|
raise ValueError(
|
|
106
112
|
"Invalid IoT API Endpoint. The IoT host should start with 'https://'.")
|
|
107
113
|
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
# Check if host is a valid URL
|
|
115
|
+
if not ws_host.startswith('wss://'):
|
|
116
|
+
raise ValueError(
|
|
117
|
+
"Invalid IoT Websocket MQTT Host. The IoT host should start with 'wss://'.")
|
|
112
118
|
|
|
113
119
|
# Check if user_name and password are not empty
|
|
114
120
|
if not bypass_username_password:
|
|
@@ -127,5 +133,5 @@ def set_mqtt_broker_config(api_endpoint: str, ws_host: str, user_name: str, pass
|
|
|
127
133
|
_control_ws_port = ws_port
|
|
128
134
|
_control_ws_username = user_name
|
|
129
135
|
_control_ws_password = password
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
_control_ws_max_timeout = max_timeout
|
|
137
|
+
_control_ws_ca_certs = ca_certs
|
|
@@ -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
|
|
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"
|
|
@@ -201,7 +201,7 @@ def _timezone_dst_offsets(api_inputs: ApiInputs, date_from: datetime.date, date_
|
|
|
201
201
|
else:
|
|
202
202
|
return timezone_offsets
|
|
203
203
|
except Exception as ex:
|
|
204
|
-
logger.error("API Call was not successful.", ex)
|
|
204
|
+
logger.error("API Call was not successful. %s", ex)
|
|
205
205
|
return pandas.DataFrame()
|
|
206
206
|
|
|
207
207
|
def _upsert_entities_affected_count(api_inputs: ApiInputs, entities_affected_count: int):
|
|
@@ -44,10 +44,10 @@ class SwitchRealtime:
|
|
|
44
44
|
port: int,
|
|
45
45
|
username: str,
|
|
46
46
|
password: str,
|
|
47
|
-
ssl_pwd: str,
|
|
48
47
|
client_id: str,
|
|
49
48
|
email: str,
|
|
50
|
-
project_id: str
|
|
49
|
+
project_id: str,
|
|
50
|
+
ca_certs: str = None
|
|
51
51
|
):
|
|
52
52
|
self.host = host
|
|
53
53
|
self.port = port
|
|
@@ -57,18 +57,18 @@ class SwitchRealtime:
|
|
|
57
57
|
|
|
58
58
|
self.client_id = client_id
|
|
59
59
|
self.client = mqtt.Client(client_id=client_id, transport='websockets', clean_session=True)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
|
|
61
|
+
if ca_certs:
|
|
62
|
+
# -- adhoc for cds engine server --
|
|
63
|
+
self.client.ws_set_options(path="/mqtt")
|
|
64
|
+
self.client.tls_set(
|
|
65
|
+
ca_certs=ca_certs,
|
|
66
|
+
cert_reqs=ssl.CERT_REQUIRED,
|
|
67
|
+
tls_version=ssl.PROTOCOL_TLS_CLIENT,
|
|
68
|
+
)
|
|
69
|
+
self.client.tls_insecure_set(False)
|
|
70
|
+
else:
|
|
71
|
+
self.client.tls_set()
|
|
72
72
|
|
|
73
73
|
if username and password:
|
|
74
74
|
self.client.username_pw_set(username, password)
|
|
@@ -102,6 +102,7 @@ def set_realtime_redis_config(
|
|
|
102
102
|
retry_max_delay: float = 0.05,
|
|
103
103
|
socket_timeout: int = None,
|
|
104
104
|
socket_connect_timeout: int = None,
|
|
105
|
+
ca_cert: str = None
|
|
105
106
|
):
|
|
106
107
|
"""
|
|
107
108
|
Configure Redis state storage for realtime MQTT dataset updates.
|
|
@@ -199,6 +200,8 @@ def set_realtime_redis_config(
|
|
|
199
200
|
socket_connect_timeout : int, optional
|
|
200
201
|
Redis socket connection timeout in seconds.
|
|
201
202
|
Defaults to None.
|
|
203
|
+
ca_cert: str, optional
|
|
204
|
+
A string path to the Certificate Authority certificate files in Server
|
|
202
205
|
|
|
203
206
|
Returns
|
|
204
207
|
-------
|
|
@@ -255,14 +258,23 @@ def set_realtime_redis_config(
|
|
|
255
258
|
"retry_max_delay": retry_max_delay,
|
|
256
259
|
"socket_timeout": socket_timeout,
|
|
257
260
|
"socket_connect_timeout": socket_connect_timeout,
|
|
261
|
+
"ca_cert": ca_cert
|
|
258
262
|
}
|
|
259
263
|
_redis_client = None
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
if ca_cert:
|
|
265
|
+
logger.info(
|
|
266
|
+
f"Configured Redis realtime state. host={host}, port={port}, "
|
|
267
|
+
f"ssl={ssl}, db={db}, key_prefix={key_prefix}, "
|
|
268
|
+
f"max_retries={max_retries}, timeout_max_retries={timeout_max_retries}, "
|
|
269
|
+
f"retry_min_delay={retry_min_delay}, retry_max_delay={retry_max_delay}, ca_cert={ca_cert}"
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
logger.info(
|
|
273
|
+
f"Configured Redis realtime state. host={host}, port={port}, "
|
|
274
|
+
f"ssl={ssl}, db={db}, key_prefix={key_prefix}, "
|
|
275
|
+
f"max_retries={max_retries}, timeout_max_retries={timeout_max_retries}, "
|
|
276
|
+
f"retry_min_delay={retry_min_delay}, retry_max_delay={retry_max_delay}"
|
|
277
|
+
)
|
|
266
278
|
|
|
267
279
|
|
|
268
280
|
def get_realtime_redis_config():
|
|
@@ -296,18 +308,33 @@ def get_realtime_redis_client():
|
|
|
296
308
|
f"port={_redis_config['port']}, ssl={_redis_config['ssl']}, "
|
|
297
309
|
f"db={_redis_config['db']}"
|
|
298
310
|
)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
+
|
|
312
|
+
if _redis_config["ca_cert"]:
|
|
313
|
+
_redis_client = redis.Redis(
|
|
314
|
+
host=_redis_config["host"],
|
|
315
|
+
port=_redis_config["port"],
|
|
316
|
+
username=_redis_config["username"],
|
|
317
|
+
password=_redis_config["password"],
|
|
318
|
+
ssl=_redis_config["ssl"],
|
|
319
|
+
db=_redis_config["db"],
|
|
320
|
+
decode_responses=True,
|
|
321
|
+
socket_timeout=_redis_config["socket_timeout"],
|
|
322
|
+
socket_connect_timeout=_redis_config["socket_connect_timeout"],
|
|
323
|
+
ssl_ca_certs=_redis_config["ca_cert"]
|
|
324
|
+
)
|
|
325
|
+
else:
|
|
326
|
+
_redis_client = redis.Redis(
|
|
327
|
+
host=_redis_config["host"],
|
|
328
|
+
port=_redis_config["port"],
|
|
329
|
+
username=_redis_config["username"],
|
|
330
|
+
password=_redis_config["password"],
|
|
331
|
+
ssl=_redis_config["ssl"],
|
|
332
|
+
db=_redis_config["db"],
|
|
333
|
+
decode_responses=True,
|
|
334
|
+
socket_timeout=_redis_config["socket_timeout"],
|
|
335
|
+
socket_connect_timeout=_redis_config["socket_connect_timeout"]
|
|
336
|
+
)
|
|
337
|
+
|
|
311
338
|
|
|
312
339
|
return _redis_client
|
|
313
340
|
|
|
@@ -300,10 +300,10 @@ def submit_realtime_updates(
|
|
|
300
300
|
port=mqtt_config["ws_port"],
|
|
301
301
|
username=mqtt_config["username"],
|
|
302
302
|
password=mqtt_config["password"],
|
|
303
|
-
ssl_pwd=mqtt_config["ssl_pwd"],
|
|
304
303
|
client_id=api_inputs.data_feed_id,
|
|
305
304
|
email=api_inputs.email_address,
|
|
306
305
|
project_id=api_inputs.api_project_id,
|
|
306
|
+
ca_certs=mqtt_config["ca_certs"]
|
|
307
307
|
)
|
|
308
308
|
|
|
309
309
|
# ----------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: switch_api
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.29
|
|
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,18 @@ pip install switch_api
|
|
|
58
58
|
|
|
59
59
|
# History
|
|
60
60
|
|
|
61
|
+
#0.6.29
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
In the `authentication` module:
|
|
66
|
+
|
|
67
|
+
- Added optional `data_center` support to `initialize()`, with `AU` and `US` as the supported values.
|
|
68
|
+
- Authentication login-context and user-profile requests now use the endpoint associated with the selected data center.
|
|
69
|
+
- Interactive production authentication now uses the B2C authority associated with the portfolio's resolved data center (`AustraliaEast` or `CentralUs`).
|
|
70
|
+
- When `data_center` is omitted, authentication defaults to `US` and logs a warning to preserve backward compatibility.
|
|
71
|
+
- Invalid data-center values now raise a `ValueError` with the supported options.
|
|
72
|
+
|
|
61
73
|
#0.6.18
|
|
62
74
|
### Changed
|
|
63
75
|
In the `realtime` module:
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import Mock, patch
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from switch_api._authentication._authentication import fetch_login_context, fetch_user_profile_details
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FetchLoginContextTests(unittest.TestCase):
|
|
10
|
+
@staticmethod
|
|
11
|
+
def _response():
|
|
12
|
+
response = Mock(status_code=200, reason='OK', text='{}')
|
|
13
|
+
response.json.return_value = {}
|
|
14
|
+
return response
|
|
15
|
+
|
|
16
|
+
def test_uses_data_center_host(self):
|
|
17
|
+
data_centers = {
|
|
18
|
+
'AU': 'restapi-au.switchautomation.com/auth',
|
|
19
|
+
'US': 'restapi-us.switchautomation.com/auth',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for data_center, expected_host in data_centers.items():
|
|
23
|
+
with self.subTest(data_center=data_center):
|
|
24
|
+
with patch('switch_api._authentication._authentication.requests.request',
|
|
25
|
+
return_value=self._response()) as request:
|
|
26
|
+
fetch_login_context('Production', uuid4(), data_center)
|
|
27
|
+
|
|
28
|
+
self.assertTrue(request.call_args.args[1].startswith(
|
|
29
|
+
f'https://{expected_host}/login-context?'))
|
|
30
|
+
|
|
31
|
+
def test_defaults_to_us_with_warning(self):
|
|
32
|
+
with patch('switch_api._authentication._authentication.requests.request',
|
|
33
|
+
return_value=self._response()) as request:
|
|
34
|
+
with self.assertLogs(level=logging.WARNING) as logs:
|
|
35
|
+
fetch_login_context('Production', uuid4())
|
|
36
|
+
|
|
37
|
+
self.assertIn('No data center was provided. Defaulting to US.', logs.output[0])
|
|
38
|
+
self.assertIn('restapi-us.switchautomation.com/auth',
|
|
39
|
+
request.call_args.args[1])
|
|
40
|
+
|
|
41
|
+
def test_rejects_unknown_data_center(self):
|
|
42
|
+
with self.assertRaisesRegex(ValueError, "data_center must be either 'AU' or 'US'"):
|
|
43
|
+
fetch_login_context('Production', uuid4(), 'EU')
|
|
44
|
+
|
|
45
|
+
def test_user_profile_uses_data_center_host(self):
|
|
46
|
+
data_centers = {
|
|
47
|
+
'AU': 'https://restapi-au.switchautomation.com/auth/user-profile?',
|
|
48
|
+
'US': 'https://restapi-us.switchautomation.com/auth/user-profile?',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for data_center, expected_prefix in data_centers.items():
|
|
52
|
+
with self.subTest(data_center=data_center):
|
|
53
|
+
with patch('switch_api._authentication._authentication.requests.request',
|
|
54
|
+
return_value=self._response()) as request:
|
|
55
|
+
fetch_user_profile_details('token', 'Production', uuid4(), data_center)
|
|
56
|
+
|
|
57
|
+
self.assertTrue(request.call_args.args[1].startswith(expected_prefix))
|
|
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.27 → switch_api-0.6.29}/switch_api/_authentication/_credentials_store/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{switch_api-0.6.27 → switch_api-0.6.29}/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
|