hcs-core 0.1.250__py3-none-any.whl → 0.1.316__py3-none-any.whl

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 (61) hide show
  1. hcs_core/__init__.py +1 -0
  2. hcs_core/ctxp/__init__.py +12 -4
  3. hcs_core/ctxp/_init.py +94 -22
  4. hcs_core/ctxp/built_in_cmds/_ut.py +4 -3
  5. hcs_core/ctxp/built_in_cmds/context.py +16 -1
  6. hcs_core/ctxp/built_in_cmds/profile.py +30 -11
  7. hcs_core/ctxp/cli_options.py +34 -13
  8. hcs_core/ctxp/cli_processor.py +33 -20
  9. hcs_core/ctxp/cmd_util.py +87 -0
  10. hcs_core/ctxp/config.py +1 -1
  11. hcs_core/ctxp/context.py +82 -3
  12. hcs_core/ctxp/data_util.py +56 -20
  13. hcs_core/ctxp/dispatcher.py +82 -0
  14. hcs_core/ctxp/duration.py +65 -0
  15. hcs_core/ctxp/extension.py +7 -6
  16. hcs_core/ctxp/fn_util.py +57 -0
  17. hcs_core/ctxp/fstore.py +39 -22
  18. hcs_core/ctxp/jsondot.py +259 -78
  19. hcs_core/ctxp/logger.py +7 -6
  20. hcs_core/ctxp/profile.py +53 -21
  21. hcs_core/ctxp/profile_store.py +1 -0
  22. hcs_core/ctxp/recent.py +3 -3
  23. hcs_core/ctxp/state.py +4 -3
  24. hcs_core/ctxp/task_schd.py +168 -0
  25. hcs_core/ctxp/telemetry.py +145 -0
  26. hcs_core/ctxp/template_util.py +21 -0
  27. hcs_core/ctxp/timeutil.py +11 -0
  28. hcs_core/ctxp/util.py +194 -33
  29. hcs_core/ctxp/var_template.py +3 -4
  30. hcs_core/plan/__init__.py +11 -5
  31. hcs_core/plan/base_provider.py +1 -0
  32. hcs_core/plan/core.py +29 -26
  33. hcs_core/plan/dag.py +15 -12
  34. hcs_core/plan/helper.py +4 -2
  35. hcs_core/plan/kop.py +21 -8
  36. hcs_core/plan/provider/dev/dummy.py +3 -3
  37. hcs_core/sglib/auth.py +137 -95
  38. hcs_core/sglib/cli_options.py +20 -5
  39. hcs_core/sglib/client_util.py +230 -62
  40. hcs_core/sglib/csp.py +73 -6
  41. hcs_core/sglib/ez_client.py +139 -41
  42. hcs_core/sglib/hcs_client.py +3 -9
  43. hcs_core/sglib/init.py +17 -0
  44. hcs_core/sglib/login_support.py +22 -83
  45. hcs_core/sglib/payload_util.py +3 -1
  46. hcs_core/sglib/requtil.py +38 -0
  47. hcs_core/sglib/utils.py +107 -0
  48. hcs_core/util/check_license.py +0 -2
  49. hcs_core/util/duration.py +6 -3
  50. hcs_core/util/job_view.py +35 -15
  51. hcs_core/util/pki_util.py +48 -1
  52. hcs_core/util/query_util.py +54 -8
  53. hcs_core/util/scheduler.py +3 -3
  54. hcs_core/util/ssl_util.py +1 -1
  55. hcs_core/util/versions.py +15 -12
  56. hcs_core-0.1.316.dist-info/METADATA +54 -0
  57. hcs_core-0.1.316.dist-info/RECORD +69 -0
  58. {hcs_core-0.1.250.dist-info → hcs_core-0.1.316.dist-info}/WHEEL +1 -2
  59. hcs_core-0.1.250.dist-info/METADATA +0 -36
  60. hcs_core-0.1.250.dist-info/RECORD +0 -59
  61. hcs_core-0.1.250.dist-info/top_level.txt +0 -1
@@ -13,78 +13,228 @@ See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
14
  """
15
15
 
16
- from typing import Callable
16
+ import json
17
+ import logging
18
+ import sys
19
+ import threading
17
20
  import time
18
- from hcs_core.ctxp import profile, panic, CtxpException
19
- from hcs_core.sglib import hcs_client
20
- from hcs_core.util.query_util import with_query, PageRequest
21
- from hcs_core.util import duration
22
- from hcs_core.util import exit
21
+ from typing import Callable, Iterator
23
22
 
23
+ from hcs_core.ctxp import CtxpException, panic, profile
24
+ from hcs_core.sglib.ez_client import EzClient
25
+ from hcs_core.util import duration, exit
26
+ from hcs_core.util.query_util import PageRequest, with_query
24
27
 
25
- def hdc_service_client(service_name: str):
26
- url = profile.current().hcs.url
27
- if not url.endswith("/"):
28
- url += "/"
29
- url += service_name
30
- return hcs_client(url)
28
+ log = logging.getLogger(__name__)
31
29
 
30
+ _caches = {}
32
31
 
33
- def _get_region_url(region_name: str):
32
+ _client_instance_lock = threading.RLock()
33
+
34
+
35
+ def _get_service_override(service_name: str):
36
+ profile_data = profile.current()
37
+ override = profile_data.get("override", {})
38
+ service_override = {}
39
+ for k, v in override.items():
40
+ if service_name == k.lower():
41
+ service_override = v
42
+
43
+ if not service_override:
44
+ if service_name == "org-service":
45
+ service_override = override.get("org", {})
46
+ elif service_name.find("-") >= 0:
47
+ camel_name = "".join(word.capitalize() for word in service_name.split("-"))
48
+ camel_name = camel_name[0].lower() + camel_name[1:]
49
+ service_override = override.get(camel_name, {})
50
+ return service_override
51
+
52
+
53
+ def _lazy_init(service_name: str, hdc: str = None, region: str = None): # make it deferred so no need to initialize profile
54
+ if region and hdc:
55
+ raise Exception("region and hdc cannot be specified at the same time.")
56
+
57
+ profile_data = profile.current()
58
+
59
+ service_override = _get_service_override(service_name)
60
+ service_override_url = service_override.get("url")
61
+ if service_override_url:
62
+ log.debug(f"Using per-service override for {service_name}: {service_override_url}")
63
+ url = service_override_url
64
+ else:
65
+ if hdc:
66
+ # prod only
67
+ hdc = hdc.upper()
68
+ if hdc == "US":
69
+ url = profile_data.hcs.url
70
+ elif hdc == "EU":
71
+ url = profile_data.hcs.url.replace("cloud-sg-us", "cloud-sg-eu")
72
+ elif hdc == "JP":
73
+ url = profile_data.hcs.url.replace("cloud-sg-us", "cloud-sg-jp")
74
+ else:
75
+ raise CtxpException(f"Invalid HDC name: {hdc}. Supported HDC names: US, EU, JP.")
76
+ elif region:
77
+ url = _get_region_url(region)
78
+ if not url:
79
+ panic("Missing profile property: hcs.regions")
80
+ else:
81
+ url = profile_data.hcs.url
82
+ url = url.rstrip("/") + "/" + service_name
83
+
84
+ # TODO
85
+ client_id = service_override.get("client-id", None)
86
+ if not client_id:
87
+ client_id = service_override.get("clientId", None)
88
+ client_secret = service_override.get("client-secret", None)
89
+ if not client_secret:
90
+ client_secret = service_override.get("clientSecret", None)
91
+ api_token = service_override.get("api-token", None)
92
+ if not api_token:
93
+ api_token = service_override.get("apiToken", None)
94
+ provider = service_override.get("provider", "vmwarecsp")
95
+ if provider == "vmwarecsp":
96
+ token_url = profile_data.csp.url
97
+ elif provider == "auth-service":
98
+ token_url = profile_data.auth["tokenUrl"]
99
+ else:
100
+ raise CtxpException(f"Unknown provider: {provider}. Supported providers: vmwarecsp, auth-service.")
101
+
102
+ if client_id:
103
+ if not client_secret:
104
+ panic(f"Client ID is specified but missing clientSecret for service {service_name} in profile override.")
105
+ else:
106
+ if client_secret:
107
+ panic(f"Client secret is specified but missing clientId for service {service_name} in profile override.")
108
+
109
+ if client_id:
110
+ auth_config = {
111
+ "url": token_url,
112
+ "client_id": client_id,
113
+ "client_secret": client_secret,
114
+ }
115
+ elif api_token:
116
+ auth_config = {
117
+ "url": token_url,
118
+ "api_token": api_token,
119
+ }
120
+ else:
121
+ auth_config = None
122
+
123
+ from hcs_core.sglib import auth
124
+
125
+ oauth_client = auth.oauth_client(auth_config=auth_config)
126
+ return url, oauth_client
127
+
128
+
129
+ def hdc_service_client(service_name: str, hdc: str = None) -> EzClient:
130
+ """A client for HDC service. Cached per service name, thread-safe, lazy initialization."""
131
+ if hdc:
132
+ hdc = hdc.upper()
133
+ if hdc not in ["US", "EU", "JP"]:
134
+ panic(f"Invalid HDC name: {hdc}. Supported HDC names: US, EU, JP.")
135
+ else:
136
+ hdc = "US"
137
+
138
+ k = f"{service_name}#{hdc}"
139
+ with _client_instance_lock:
140
+ instance = _caches.get(k)
141
+ if not instance:
142
+ instance = EzClient(lazy_init=lambda: _lazy_init(service_name=service_name, hdc=hdc))
143
+ _caches[k] = instance
144
+ return instance
145
+
146
+
147
+ def _get_region_url(region: str):
34
148
  regions = profile.current().hcs.regions
35
- if not region_name:
149
+ if not region:
36
150
  return regions[0].url
37
151
  for r in regions:
38
- if r.name.lower() == region_name.lower():
152
+ if r.name.lower() == region.lower():
39
153
  return r.url
40
154
  names = []
41
155
  for r in regions:
42
156
  names.append(r.name)
43
- panic(f"Region not found: {region_name}. Available regions: {names}")
157
+ panic(f"Region not found: {region}. Available regions: {names}")
44
158
 
45
159
 
46
- def regional_service_client(region_name: str, service_name: str):
160
+ def regional_service_client(service_name: str, region: str = None):
47
161
  # 'https://dev1b-westus2-cp103a.azcp.horizon.vmware.com/vmhub'
48
- url = _get_region_url(region_name)
49
- if not url:
50
- panic("Missing profile property: hcs.regions")
51
- if not url.endswith("/"):
52
- url += "/"
53
- url += service_name
54
- return hcs_client(url)
162
+ region_names = [r.name.lower() for r in profile.current().hcs.regions]
163
+ if region:
164
+ region = region.lower()
165
+ if region not in region_names:
166
+ panic(f"Invalid region name: {region}. Available regions: {', '.join(region_names)}")
167
+ else:
168
+ region = region_names[0]
55
169
 
170
+ k = f"{service_name}#{region}"
171
+ with _client_instance_lock:
172
+ instance = _caches.get(k)
173
+ if not instance:
174
+ instance = EzClient(lazy_init=lambda: _lazy_init(service_name=service_name, region=region))
175
+ _caches[k] = instance
176
+ return instance
56
177
 
57
- def _with_org_id(url: str, org_id: str):
58
- if org_id:
59
- if url.find("?") < 0:
60
- url += "?"
61
- url += "org_id=" + org_id
62
- return url
178
+
179
+ def is_regional_service(service_name: str):
180
+ regional_services = ["vmhub", "connection-service"]
181
+ return service_name in regional_services
182
+
183
+
184
+ def service_client(service_name: str, region: str = None, hdc: str = None):
185
+ if is_regional_service(service_name):
186
+ return regional_service_client(service_name, region)
187
+ else:
188
+ return hdc_service_client(service_name, hdc)
63
189
 
64
190
 
65
191
  class default_crud:
66
192
  def __init__(self, client, base_context: str, resource_type_name: str):
67
- self._client = client
193
+ self._client_impl = client
68
194
  self._base_context = base_context
69
195
  self._resource_type_name = resource_type_name
70
196
 
197
+ def _client(self):
198
+ if callable(self._client_impl):
199
+ self._client_impl = self._client_impl()
200
+ elif isinstance(self._client_impl, str):
201
+ self._client_impl = hdc_service_client(self._client_impl)
202
+ else:
203
+ pass
204
+ if isinstance(self._client_impl, EzClient):
205
+ return self._client_impl
206
+ raise CtxpException(f"Invalid client implementation: {self._client_impl}")
207
+
71
208
  def get(self, id: str, org_id: str, **kwargs):
72
209
  if org_id:
73
210
  kwargs["org_id"] = org_id
211
+ kwargs["orgId"] = org_id
74
212
  url = with_query(f"{self._base_context}/{id}", **kwargs)
75
213
  # print(url)
76
- return self._client.get(url)
214
+ return self._client().get(url)
77
215
 
78
- def list(self, org_id: str, **kwargs):
216
+ def list(self, org_id: str, fn_filter: Callable = None, **kwargs) -> list:
79
217
  if org_id:
80
218
  kwargs["org_id"] = org_id
219
+ kwargs["orgId"] = org_id
81
220
 
82
221
  def _get_page(query_string):
83
222
  url = self._base_context + "?" + query_string
84
223
  # print(url)
85
- return self._client.get(url)
224
+ return self._client().get(url)
225
+
226
+ return PageRequest(_get_page, fn_filter, **kwargs).get()
227
+
228
+ def items(self, org_id: str, fn_filter: Callable = None, **kwargs) -> Iterator:
229
+ if org_id:
230
+ kwargs["org_id"] = org_id
231
+ kwargs["orgId"] = org_id
232
+
233
+ def _get_page(query_string):
234
+ url = self._base_context + "?" + query_string
235
+ return self._client().get(url)
86
236
 
87
- return PageRequest(_get_page, **kwargs).get()
237
+ return PageRequest(_get_page, fn_filter, **kwargs).items()
88
238
 
89
239
  def create(self, payload: dict, headers: dict = None, **kwargs):
90
240
  url = with_query(f"{self._base_context}", **kwargs)
@@ -92,32 +242,37 @@ class default_crud:
92
242
  # import json
93
243
  # print(json.dumps(payload, indent=4))
94
244
  if isinstance(payload, str):
95
- return self._client.post(url, text=payload, headers=headers)
245
+ return self._client().post(url, text=payload, headers=headers)
96
246
  if isinstance(payload, dict):
97
- return self._client.post(url, json=payload, headers=headers)
98
- return self._client.post(url, json=payload, headers=headers)
247
+ return self._client().post(url, json=payload, headers=headers)
248
+ return self._client().post(url, json=payload, headers=headers)
99
249
 
100
250
  def upload(self, files, **kwargs):
101
251
  url = with_query(f"{self._base_context}", **kwargs)
102
- return self._client.post(url, files=files)
252
+ return self._client().post(url, files=files)
103
253
 
104
254
  def delete(self, id: str, org_id: str, **kwargs):
105
255
  if org_id:
106
256
  kwargs["org_id"] = org_id
257
+ kwargs["orgId"] = org_id
107
258
  url = with_query(f"{self._base_context}/{id}", **kwargs)
108
259
  # print(url)
109
- return self._client.delete(url)
260
+ return self._client().delete(url)
110
261
 
111
262
  def wait_for_deleted(self, id: str, org_id: str, timeout: str, fn_is_error: Callable = None):
112
263
  name = self._resource_type_name + "/" + id
113
- fn_get = lambda: self.get(id, org_id)
264
+
265
+ def fn_get():
266
+ return self.get(id, org_id)
267
+
114
268
  return wait_for_res_deleted(name, fn_get, timeout=timeout, fn_is_error=fn_is_error)
115
269
 
116
270
  def update(self, id: str, org_id: str, data: dict, **kwargs):
117
271
  if org_id:
118
272
  kwargs["org_id"] = org_id
273
+ kwargs["orgId"] = org_id
119
274
  url = with_query(f"{self._base_context}/{id}")
120
- return self._client.patch(url, data)
275
+ return self._client().patch(url, data)
121
276
 
122
277
 
123
278
  def _parse_timeout(timeout: str):
@@ -133,10 +288,13 @@ def wait_for_res_deleted(
133
288
  resource_name: str,
134
289
  fn_get: Callable,
135
290
  timeout: str,
136
- polling_interval_seconds: int = 10,
291
+ polling_interval: int = 10,
137
292
  fn_is_error: Callable = None,
138
293
  ):
139
294
  timeout_seconds = _parse_timeout(timeout)
295
+ polling_interval_seconds = _parse_timeout(polling_interval)
296
+ if polling_interval_seconds < 3:
297
+ polling_interval_seconds = 3
140
298
  start = time.time()
141
299
  while True:
142
300
  t = fn_get()
@@ -155,9 +313,10 @@ def wait_for_res_deleted(
155
313
  sleep_seconds = remaining_seconds
156
314
  if sleep_seconds > polling_interval_seconds:
157
315
  sleep_seconds = polling_interval_seconds
158
- exit.sleep(sleep_seconds)
316
+ time.sleep(sleep_seconds)
159
317
 
160
318
 
319
+ # flake8: noqa=E731
161
320
  def wait_for_res_status(
162
321
  resource_name: str,
163
322
  fn_get: Callable,
@@ -181,21 +340,25 @@ def wait_for_res_status(
181
340
  field_name = get_status
182
341
  get_status = lambda t: t[field_name]
183
342
  if status_map:
184
- if isinstance(status_map["ready"], str):
185
- status_map["ready"] = [status_map["ready"]]
186
- if isinstance(status_map["transition"], str):
187
- status_map["transition"] = [status_map["transition"]]
188
- if isinstance(status_map["error"], str):
189
- status_map["error"] = [status_map["error"]]
190
343
  if is_ready:
191
344
  raise CtxpException("Can not specify is_ready when status_map is provided.")
192
345
  if is_error:
193
346
  raise CtxpException("Can not specify is_error when status_map is provided.")
194
347
  if is_transition:
195
348
  raise CtxpException("Can not specify is_transition when status_map is provided.")
196
- is_ready = lambda s: s in status_map["ready"]
197
- is_error = lambda s: s in status_map["error"]
198
- is_transition = lambda s: s in status_map["transition"]
349
+
350
+ ready_status = status_map["ready"]
351
+ error_status = status_map["error"]
352
+ transition_status = status_map["transition"]
353
+ if isinstance(ready_status, str):
354
+ ready_status = [ready_status]
355
+ if isinstance(error_status, str):
356
+ error_status = [error_status]
357
+ if isinstance(transition_status, str):
358
+ transition_status = [transition_status]
359
+ is_ready = lambda s: s in ready_status
360
+ is_error = lambda s: error_status is None or s in error_status
361
+ is_transition = lambda s: transition_status is None or s in transition_status
199
362
  else:
200
363
  if not is_ready:
201
364
  raise CtxpException("Either status_map or is_ready must be specified.")
@@ -203,6 +366,9 @@ def wait_for_res_status(
203
366
  raise CtxpException("Either status_map or is_error must be specified.")
204
367
  if not is_transition:
205
368
  raise CtxpException("Either status_map or is_transition must be specified.")
369
+ ready_status = None
370
+ error_status = None
371
+ transition_status = None
206
372
 
207
373
  while True:
208
374
  t = fn_get()
@@ -211,26 +377,28 @@ def wait_for_res_status(
211
377
  return
212
378
  raise CtxpException(prefix + "Not found.")
213
379
  status = get_status(t)
214
- # print(f"DDD - status {status}")
215
380
  if is_error(status):
216
- msg = prefix + f"Status error. Actual={status}"
217
- if status_map:
218
- msg += f", expected={status_map['ready']}"
381
+ msg = prefix + f"Unexpected terminal state. Actual={status}"
382
+ if ready_status:
383
+ msg += f", expected={ready_status}"
384
+ print("-- DUMP START --", file=sys.stderr)
385
+ print(json.dumps(t, indent=4), file=sys.stderr)
386
+ print("-- DUMP END --", file=sys.stderr)
219
387
  raise CtxpException(msg)
220
388
  if is_ready(status):
221
389
  return t
222
390
  if not is_transition(status):
223
- raise CtxpException(
224
- prefix + f"Unexpected status: {status}. If this is a transition, add it to status_map['transition']."
225
- )
391
+ raise CtxpException(prefix + f"Unexpected status: {status}. If this is a transition, add it to status_map['transition'].")
226
392
 
227
393
  now = time.time()
228
394
  remaining_seconds = timeout_seconds - (now - start)
229
395
  if remaining_seconds < 1:
230
- raise TimeoutError(prefix + "Timeout.")
396
+ msg = prefix + f"Timeout. Current: {status}."
397
+ if ready_status:
398
+ msg += f" Expected: {ready_status}."
399
+ raise TimeoutError(msg)
231
400
  sleep_seconds = remaining_seconds
232
401
  if sleep_seconds > polling_interval_seconds:
233
402
  sleep_seconds = polling_interval_seconds
234
403
 
235
- # print(f"DDD sleeping {sleep_seconds}")
236
404
  exit.sleep(sleep_seconds)
hcs_core/sglib/csp.py CHANGED
@@ -13,10 +13,11 @@ See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
14
  """
15
15
 
16
- import httpx
17
16
  import json
18
17
  import logging
19
18
 
19
+ import httpx
20
+
20
21
  log = logging.getLogger(__name__)
21
22
 
22
23
  log_http_details = False
@@ -64,8 +65,25 @@ def _log_response(response: httpx.Response):
64
65
  log.debug("\n")
65
66
 
66
67
 
68
+ def _decode_http_basic_auth_token(basic_token: str):
69
+ import base64
70
+
71
+ try:
72
+ decoded = base64.b64decode(basic_token).decode("utf-8")
73
+ client_id, client_secret = decoded.split(":")
74
+ return client_id, client_secret
75
+ except Exception as e:
76
+ raise Exception(f"Invalid basic http auth token: {e}")
77
+
78
+
67
79
  class CspClient:
68
80
  def __init__(self, url: str, oauth_token: dict = None, org_id: str = None) -> None:
81
+ if url.endswith("/auth/v1/oauth/token"):
82
+ # To workaround that post with "" results "/" and fail the process.
83
+ url = url[: -len("/auth/v1/oauth/token")]
84
+ self._auth_mode = "hcs-auth-svc"
85
+ else:
86
+ self._auth_mode = "csp"
69
87
  self._base_url = url
70
88
  self._oauth_token = oauth_token
71
89
  self._org_id = org_id
@@ -80,6 +98,7 @@ class CspClient:
80
98
  )
81
99
 
82
100
  def login_with_api_token(self, api_token: str) -> dict:
101
+ log.debug(f"Logging in with api_token: {api_token}, url: {self._base_url}")
83
102
  # https://console-stg.cloud.vmware.com/csp/gateway/authn/api/swagger-ui.html#/Authentication/getAccessTokenByApiRefreshTokenUsingPOST
84
103
 
85
104
  # curl -X 'POST' \
@@ -87,19 +106,18 @@ class CspClient:
87
106
  # -H 'accept: application/json' \
88
107
  # -H 'Content-Type: application/x-www-form-urlencoded' \
89
108
  # -d 'refresh_token=<the-refresh-token>'
90
-
109
+ # print(f"Logging in with api_token: {api_token}, url: {self._base_url}")
91
110
  headers = {
92
111
  "Content-Type": "application/x-www-form-urlencoded",
93
112
  "Accept": "application/json",
94
113
  }
95
114
  # <no org id for this API>
96
- resp = self._client.post(
97
- "/csp/gateway/am/api/auth/api-tokens/authorize", headers=headers, data=f"api_token={api_token}"
98
- )
115
+ resp = self._client.post("/csp/gateway/am/api/auth/api-tokens/authorize", headers=headers, data=f"api_token={api_token}")
99
116
  self._oauth_token = resp.json()
100
117
  return self._oauth_token
101
118
 
102
119
  def login_with_client_id_and_secret(self, client_id: str, client_secret: str, org_id: str) -> dict:
120
+ log.debug(f"Logging in with client_id: {client_id}, org_id: {org_id}, url: {self._base_url}")
103
121
  headers = {
104
122
  "Content-Type": "application/x-www-form-urlencoded",
105
123
  "Accept": "application/json",
@@ -109,8 +127,9 @@ class CspClient:
109
127
  }
110
128
  if org_id:
111
129
  params["orgId"] = org_id
130
+ url = "/auth/v1/oauth/token" if self._auth_mode == "hcs-auth-svc" else "/csp/gateway/am/api/auth/authorize"
112
131
  resp = self._client.post(
113
- "/csp/gateway/am/api/auth/authorize",
132
+ url,
114
133
  auth=(client_id, client_secret),
115
134
  headers=headers,
116
135
  params=params,
@@ -118,6 +137,54 @@ class CspClient:
118
137
  self._oauth_token = resp.json()
119
138
  return self._oauth_token
120
139
 
140
+ @staticmethod
141
+ def create(
142
+ url: str,
143
+ org_id: str = None,
144
+ client_id: str = None,
145
+ client_secret: str = None,
146
+ api_token: str = None,
147
+ basic: str = None,
148
+ **kwargs,
149
+ ) -> "CspClient":
150
+ client = CspClient(url=url, org_id=org_id)
151
+
152
+ if not client_id:
153
+ client_id = kwargs.get("clientId")
154
+ if not client_secret:
155
+ client_secret = kwargs.get("clientSecret")
156
+ if not api_token:
157
+ api_token = kwargs.get("apiToken")
158
+
159
+ if client_id:
160
+ if not client_secret:
161
+ raise ValueError("client_secret is required when client_id is provided")
162
+ if api_token:
163
+ raise ValueError("api_token and client_id/client_secret cannot be used together")
164
+ if basic:
165
+ raise ValueError("basic auth and client_id/client_secret cannot be used together")
166
+ client.login_with_client_id_and_secret(client_id=client_id, client_secret=client_secret, org_id=org_id)
167
+ elif api_token:
168
+ if client_id or client_secret:
169
+ raise ValueError("api_token and client_id/client_secret cannot be used together")
170
+ if basic:
171
+ raise ValueError("api_token and basic auth cannot be used together")
172
+ client.login_with_api_token(api_token)
173
+ elif basic:
174
+ if client_id or client_secret:
175
+ raise ValueError("basic auth and client_id/client_secret cannot be used together")
176
+ if api_token:
177
+ raise ValueError("basic auth and api_token cannot be used together")
178
+ client_id, client_secret = _decode_http_basic_auth_token(basic)
179
+ client.login_with_client_id_and_secret(client_id=client_id, client_secret=client_secret, org_id=org_id)
180
+ else:
181
+ raise Exception("Unrecognized CSP authentication method.")
182
+
183
+ return client
184
+
185
+ def oauth_token(self) -> dict:
186
+ return self._oauth_token
187
+
121
188
  # def get_oauth_token(self, force=False):
122
189
  # if self._oauth_token and not force:
123
190
  # return self._oauth_token