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.
Files changed (77) hide show
  1. {switch_api-0.6.27 → switch_api-0.6.29}/HISTORY.md +12 -0
  2. {switch_api-0.6.27 → switch_api-0.6.29}/PKG-INFO +13 -1
  3. {switch_api-0.6.27 → switch_api-0.6.29}/setup.py +1 -1
  4. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/__init__.py +1 -1
  5. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_authentication/_authentication.py +41 -14
  6. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_utils/_constants.py +10 -5
  7. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_utils/_mqtt.py +20 -14
  8. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/initialize.py +7 -3
  9. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/integration/_utils.py +1 -1
  10. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/realtime/_mqtt.py +14 -14
  11. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/realtime/_redis.py +45 -18
  12. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/realtime/realtime.py +1 -1
  13. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api.egg-info/PKG-INFO +13 -1
  14. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api.egg-info/SOURCES.txt +1 -0
  15. switch_api-0.6.29/tests/test_authentication.py +57 -0
  16. {switch_api-0.6.27 → switch_api-0.6.29}/AUTHORS.rst +0 -0
  17. {switch_api-0.6.27 → switch_api-0.6.29}/LICENCE +0 -0
  18. {switch_api-0.6.27 → switch_api-0.6.29}/MANIFEST.in +0 -0
  19. {switch_api-0.6.27 → switch_api-0.6.29}/README.md +0 -0
  20. {switch_api-0.6.27 → switch_api-0.6.29}/pyproject.toml +0 -0
  21. {switch_api-0.6.27 → switch_api-0.6.29}/setup.cfg +0 -0
  22. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_authentication/__init__.py +0 -0
  23. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_authentication/_credentials_store/__init__.py +0 -0
  24. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_authentication/_credentials_store/_credentials_store.py +0 -0
  25. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_authentication/_msal/__init__.py +0 -0
  26. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_authentication/_msal/_custom_application.py +0 -0
  27. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_compute/__init__.py +0 -0
  28. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_compute/_commands.py +0 -0
  29. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_compute/_compute.py +0 -0
  30. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_compute/_iotservice.py +0 -0
  31. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_compute/_models.py +0 -0
  32. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_compute/_utils.py +0 -0
  33. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_guide/__init__.py +0 -0
  34. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_guide/main.py +0 -0
  35. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_guide/processor.py +0 -0
  36. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_utils/__init__.py +0 -0
  37. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_utils/_marketplace.py +0 -0
  38. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_utils/_platform.py +0 -0
  39. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/_utils/_utils.py +0 -0
  40. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/analytics/__init__.py +0 -0
  41. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/analytics/analytics.py +0 -0
  42. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/cache/__init__.py +0 -0
  43. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/cache/cache.py +0 -0
  44. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/controls/__init__.py +0 -0
  45. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/controls/_constants.py +0 -0
  46. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/controls/_enums.py +0 -0
  47. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/controls/_mqtt.py +0 -0
  48. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/controls/controls.py +0 -0
  49. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/dataset/__init__.py +0 -0
  50. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/dataset/dataset.py +0 -0
  51. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/email/__init__.py +0 -0
  52. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/email/email_sender.py +0 -0
  53. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/error_handlers/__init__.py +0 -0
  54. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/error_handlers/error_handlers.py +0 -0
  55. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/extensions/__init__.py +0 -0
  56. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/extensions/extensions.py +0 -0
  57. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/extensions/field_meta.py +0 -0
  58. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/extensions/helpers.py +0 -0
  59. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/extensions/pipeline.py +0 -0
  60. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/integration/__init__.py +0 -0
  61. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/integration/helpers.py +0 -0
  62. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/integration/integration.py +0 -0
  63. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/pipeline/__init__.py +0 -0
  64. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/pipeline/automation.py +0 -0
  65. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/pipeline/definitions.py +0 -0
  66. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/pipeline/pipeline.py +0 -0
  67. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/platform_insights/__init__.py +0 -0
  68. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/platform_insights/platform_insights.py +0 -0
  69. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api/realtime/__init__.py +0 -0
  70. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api.egg-info/dependency_links.txt +0 -0
  71. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api.egg-info/requires.txt +0 -0
  72. {switch_api-0.6.27 → switch_api-0.6.29}/switch_api.egg-info/top_level.txt +0 -0
  73. {switch_api-0.6.27 → switch_api-0.6.29}/tests/test_dataset.py +0 -0
  74. {switch_api-0.6.27 → switch_api-0.6.29}/tests/test_error_handlers.py +0 -0
  75. {switch_api-0.6.27 → switch_api-0.6.29}/tests/test_initialize.py +0 -0
  76. {switch_api-0.6.27 → switch_api-0.6.29}/tests/test_integration.py +0 -0
  77. {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.27
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.27",
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',
@@ -44,4 +44,4 @@ from . import _guide
44
44
  # log.addHandler(_ch)
45
45
  # https://dev.to/joaomcteixeira/setting-up-python-logging-for-a-library-app-6ml
46
46
 
47
- __version__ = "0.6.27"
47
+ __version__ = "0.6.29"
@@ -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
@@ -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 _control_ws_ssl_pwd
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
- _control_ws_ssl_pwd = ''
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, ws_host: str, user_name: str, password: str, ws_ssl_pwd: str,
73
- ws_port: int = WS_DEFAULT_PORT, max_timeout: int = WS_MQTT_DEFAULT_MAX_TIMEOUT, bypass_username_password: bool = False):
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 _control_ws_ssl_pwd
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
- # # Check if host is a valid URL
109
- # if not ws_host.startswith('wss://'):
110
- # raise ValueError(
111
- # "Invalid IoT Websocket MQTT Host. The IoT host should start with 'wss://'.")
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
- _control_ws_ssl_pwd = ws_ssl_pwd
131
- _control_ws_max_timeout = max_timeout
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) -> 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"
@@ -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
- # self.client.tls_set()
61
-
62
- # -- adhoc for cds engine server --
63
- self.client.ws_set_options(path="/mqtt")
64
- self.client.tls_set(
65
- ca_certs='/usr/share/ca-certificates/mozilla/Go_Daddy_Root_Certificate_Authority_-_G2.crt',
66
- cert_reqs=ssl.CERT_REQUIRED,
67
- tls_version=ssl.PROTOCOL_TLS_CLIENT,
68
- )
69
- self.client.tls_insecure_set(False)
70
- logger.info("certifi CA bundle: %s", certifi.where())
71
- # -- adhoc for cds engine server --
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
- logger.info(
261
- f"Configured Redis realtime state. host={host}, port={port}, "
262
- f"ssl={ssl}, db={db}, key_prefix={key_prefix}, "
263
- f"max_retries={max_retries}, timeout_max_retries={timeout_max_retries}, "
264
- f"retry_min_delay={retry_min_delay}, retry_max_delay={retry_max_delay}"
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
- _redis_client = redis.Redis(
300
- host=_redis_config["host"],
301
- port=_redis_config["port"],
302
- username=_redis_config["username"],
303
- password=_redis_config["password"],
304
- ssl=_redis_config["ssl"],
305
- db=_redis_config["db"],
306
- decode_responses=True,
307
- socket_timeout=_redis_config["socket_timeout"],
308
- socket_connect_timeout=_redis_config["socket_connect_timeout"],
309
- ssl_ca_certs='/usr/share/ca-certificates/mozilla/DigiCert_Global_Root_G2.crt'
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.27
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:
@@ -68,6 +68,7 @@ switch_api/realtime/__init__.py
68
68
  switch_api/realtime/_mqtt.py
69
69
  switch_api/realtime/_redis.py
70
70
  switch_api/realtime/realtime.py
71
+ tests/test_authentication.py
71
72
  tests/test_dataset.py
72
73
  tests/test_error_handlers.py
73
74
  tests/test_initialize.py
@@ -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