hcs-core 0.1.288__py3-none-any.whl → 0.1.290__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.
hcs_core/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.288"
1
+ __version__ = "0.1.290"
@@ -53,7 +53,7 @@ def use(name: str):
53
53
  if ret:
54
54
  if profile.use(ret) is None:
55
55
  panic("No such profile: " + name)
56
- return ret
56
+ return profile.current()
57
57
  else:
58
58
  # aborted
59
59
  return "", 1
@@ -103,9 +103,11 @@ def get(name: str):
103
103
 
104
104
 
105
105
  @profile_cmd_group.command()
106
- @click.argument("name")
106
+ @click.argument("name", required=False)
107
107
  def delete(name: str):
108
108
  """Delete a profile by name."""
109
+ if not name:
110
+ name = profile.name()
109
111
  profile.delete(name)
110
112
 
111
113
 
@@ -27,7 +27,7 @@ verbose = click.option(
27
27
  output = click.option(
28
28
  "--output",
29
29
  "-o",
30
- type=click.Choice(["json", "json-compact", "yaml", "text", "table"], case_sensitive=False),
30
+ type=click.Choice(["json", "json-compact", "yaml", "yml", "text", "table"], case_sensitive=False),
31
31
  default=None,
32
32
  hidden=True,
33
33
  help="Specify output format",
@@ -41,6 +41,14 @@ field = click.option(
41
41
  help="Specify fields to output, in comma separated field names.",
42
42
  )
43
43
 
44
+ exclude_field = click.option(
45
+ "--exclude-field",
46
+ type=str,
47
+ required=False,
48
+ hidden=True,
49
+ help="Specify fields to exclude from output, in comma separated field names.",
50
+ )
51
+
44
52
  wait = click.option(
45
53
  "--wait",
46
54
  "-w",
@@ -75,6 +75,7 @@ def _ensure_sub_group(current: Group, mod_path: Path):
75
75
  meta = _read_group_meta(mod_path)
76
76
  help = meta.get("help")
77
77
  extension = meta.get("extension")
78
+ hidden = meta.get("hidden", False)
78
79
 
79
80
  subgroup = current.commands.get(name)
80
81
  if subgroup and isinstance(subgroup, Group):
@@ -82,7 +83,7 @@ def _ensure_sub_group(current: Group, mod_path: Path):
82
83
  subgroup.mod_path = mod_path
83
84
  return subgroup
84
85
 
85
- subgroup = LazyGroup(extension=extension, name=name, help=help, mod_path=mod_path)
86
+ subgroup = LazyGroup(extension=extension, name=name, help=help, mod_path=mod_path, hidden=hidden)
86
87
  current.add_command(subgroup)
87
88
  return subgroup
88
89
 
@@ -166,13 +167,14 @@ def _import_cmd_file(mod_path: Path, parent: click.core.Group):
166
167
  # Create a new decorator that combines the individual decorators
167
168
  def _default_io(cmd: click.Command):
168
169
 
169
- from .cli_options import field, first, ids, output
170
+ from .cli_options import exclude_field, field, first, ids, output
170
171
 
171
172
  cmd = output(cmd)
172
173
  # cmd = cli_options.verbose(cmd)
173
174
  cmd = field(cmd)
174
175
  cmd = ids(cmd)
175
176
  cmd = first(cmd)
177
+ cmd = exclude_field(cmd)
176
178
  callback = cmd.callback
177
179
 
178
180
  def inner(*args, **kwargs):
@@ -182,6 +184,7 @@ def _default_io(cmd: click.Command):
182
184
  "field": kwargs.pop("field"),
183
185
  "ids": kwargs.pop("ids"),
184
186
  "first": kwargs.pop("first"),
187
+ "exclude_field": kwargs.pop("exclude_field"),
185
188
  }
186
189
  if io_args["output"] == "table":
187
190
 
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import os
2
3
  import re
3
4
  from io import TextIOWrapper
4
5
  from os import chmod, path
@@ -260,22 +261,26 @@ def process_variables(obj: dict, fn_get_var=None, use_env: bool = True):
260
261
  fn_get_var = _fn_get_var
261
262
 
262
263
  if use_env:
263
- import os
264
264
 
265
265
  prev_fn_get_var = fn_get_var
266
266
 
267
267
  def _fn_get_var_from_env(name: str):
268
268
  if not name.startswith("env."):
269
269
  return prev_fn_get_var(name)
270
- v = None
271
270
  actual_name = name[4:]
272
- v = os.environ.get(actual_name, None)
273
- if v:
274
- return v, True
275
- v, found = prev_fn_get_var(name)
276
- if found:
277
- return v
278
- raise CtxpException(f"Environment variable '{actual_name}' is used in template, but not found. ")
271
+ if actual_name.endswith("?"):
272
+ # optional
273
+ actual_name = actual_name[:-1]
274
+ required = False
275
+ else:
276
+ # required
277
+ required = True
278
+
279
+ if actual_name not in os.environ:
280
+ if required:
281
+ raise CtxpException(f"Environment variable '{actual_name}' is used in template, but not found. ")
282
+ return None, True
283
+ return os.environ[actual_name], True
279
284
 
280
285
  fn_get_var = _fn_get_var_from_env
281
286
 
hcs_core/ctxp/fstore.py CHANGED
@@ -199,7 +199,7 @@ class fstore:
199
199
  outfile.write(str(data))
200
200
  elif format == "json-compact":
201
201
  jsondot.save(data=data, file=file_path, pretty=False, lock=True)
202
- elif format == "yaml":
202
+ elif format == "yaml" or format == "yml":
203
203
  import yaml
204
204
 
205
205
  with open(file_path, "w", encoding="utf-8") as outfile:
hcs_core/ctxp/profile.py CHANGED
@@ -63,7 +63,7 @@ def create(name: str, data: dict, auto_use: bool = True, overwrite: bool = False
63
63
  save_data_file(data, file(name), "yaml", 0o600)
64
64
  if auto_use:
65
65
  use(name)
66
- return get(name)
66
+ return get(name, reload=True)
67
67
 
68
68
 
69
69
  def copy(from_name: str, to_name: str, overwrite: bool = True):
@@ -126,6 +126,8 @@ def use(name: str) -> str:
126
126
  return
127
127
 
128
128
  _set_active_profile_name(name)
129
+ global _data
130
+ _data = _load_data_file_with_env_overrides(name, None)
129
131
  return name
130
132
 
131
133
 
@@ -223,10 +225,10 @@ class auth:
223
225
  if json.dumps(data) == json.dumps(_auth_cache):
224
226
  return
225
227
  _auth_cache = deepcopy(data)
226
- file_name = auth._file_name()
227
228
  if os.environ.get("CTXP_AUTH_PERSIST", "true").lower() == "false":
228
229
  pass
229
230
  else:
231
+ file_name = auth._file_name()
230
232
  save_data_file(data, file_name, "yaml", 0o600)
231
233
 
232
234
  @staticmethod
hcs_core/ctxp/util.py CHANGED
@@ -57,6 +57,7 @@ def validate_error_return(reason, return_code):
57
57
  def print_output(data: Any, args: dict, file=sys.stdout):
58
58
  output = args.get("output", "json")
59
59
  fields = args.get("field")
60
+ exclude_field = args.get("exclude_field")
60
61
  ids = args.get("ids", False)
61
62
  first = args.get("first", False)
62
63
 
@@ -74,14 +75,17 @@ def print_output(data: Any, args: dict, file=sys.stdout):
74
75
  if fields:
75
76
  raise CtxpException("--ids and --fields should not be used together.")
76
77
  data = _convert_to_id_only(data)
77
- elif fields:
78
- data = _filter_fields(data, fields)
78
+ else:
79
+ if fields:
80
+ data = _filter_fields(data, fields)
81
+ if exclude_field:
82
+ data = _exclude_fields(data, exclude_field)
79
83
 
80
84
  if output is None or output == "json":
81
85
  text = json.dumps(data, default=vars, indent=4)
82
86
  elif output == "json-compact":
83
87
  text = json.dumps(data, default=vars)
84
- elif output == "yaml":
88
+ elif output == "yaml" or output == "yml":
85
89
  from . import jsondot
86
90
 
87
91
  text = yaml.dump(jsondot.plain(data), sort_keys=False)
@@ -168,6 +172,22 @@ def _filter_fields(obj: Any, fields: str):
168
172
  return _filter_obj(obj)
169
173
 
170
174
 
175
+ def _exclude_fields(obj: Any, fields_exclude: str):
176
+ parts = fields_exclude.split(",")
177
+
178
+ def _filter_obj(o):
179
+ if not isinstance(o, dict):
180
+ return o
181
+ for k in list(o.keys()):
182
+ if k in parts:
183
+ del o[k]
184
+ return o
185
+
186
+ if isinstance(obj, list):
187
+ return list(map(_filter_obj, obj))
188
+ return _filter_obj(obj)
189
+
190
+
171
191
  def panic(reason: Any = None, code: int = 1):
172
192
  if isinstance(reason, Exception):
173
193
  text = error_details(reason)
hcs_core/plan/__init__.py CHANGED
@@ -5,6 +5,7 @@ from .core import clear as clear
5
5
  from .core import destroy as destroy
6
6
  from .core import get_deployment_data as get_deployment_data
7
7
  from .core import graph as graph
8
+ from .core import resolve as resolve
8
9
  from .helper import PlanException as PlanException
9
10
  from .helper import PluginException as PluginException
10
11
  from .kop import attach_job_view as attach_job_view
hcs_core/plan/core.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- Copyright 2023-2023 VMware Inc.
2
+ Copyright 2025-2025 Omnissa Inc.
3
3
  SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  Licensed under the Apache License, Version 2.0 (the "License");
@@ -93,6 +93,17 @@ def _prepare_data(data: dict, additional_context: dict, target_resource_name: st
93
93
  # return True
94
94
 
95
95
 
96
+ def resolve(data: dict, additional_context: dict = None, target_resource_name: str = None):
97
+ blueprint, state, state_file = _prepare_data(data, additional_context, None)
98
+ if target_resource_name:
99
+ if target_resource_name in blueprint["resource"]:
100
+ return blueprint["resource"][target_resource_name]["data"]
101
+ if target_resource_name in blueprint["runtime"]:
102
+ return blueprint["runtime"][target_resource_name]["data"]
103
+ raise PlanException("Target resource or runtime not found: " + target_resource_name)
104
+ return blueprint
105
+
106
+
96
107
  def apply(
97
108
  data: dict,
98
109
  additional_context: dict = None,
@@ -249,7 +260,7 @@ def _deploy_res(name, res, state):
249
260
  action = handler.decide(res_data, res_state)
250
261
 
251
262
  if action == actions.skip:
252
- kop.skip("Already deployed")
263
+ kop.skip(_get_res_text(handler, res_state))
253
264
  return
254
265
 
255
266
  if action == actions.recreate:
hcs_core/plan/kop.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- Copyright 2023-2023 VMware Inc.
2
+ Copyright 2025-2025 Omnissa Inc.
3
3
  SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  Licensed under the Apache License, Version 2.0 (the "License");
hcs_core/sglib/auth.py CHANGED
@@ -30,96 +30,17 @@ from .csp import CspClient
30
30
  log = logging.getLogger(__name__)
31
31
 
32
32
 
33
- def _get_profile_auth_hash(effective_profile):
34
- csp = effective_profile.csp
35
- text = json.dumps(csp, default=vars)
36
- return profile.name() + "#" + hashlib.md5(text.encode("ascii"), usedforsecurity=False).hexdigest()
37
-
38
-
39
- def _is_auth_valid(auth_data, effective_profile):
40
- if not auth_data:
41
- return
42
- if not auth_data.token:
43
- return
44
- if auth_data.hash != _get_profile_auth_hash(effective_profile):
45
- return
33
+ def _is_auth_valid(auth_data):
46
34
  leeway = 60
47
- if time.time() + leeway >= auth_data.token.expires_at:
48
- return
49
- return True
50
-
51
-
52
- def _decode_http_basic_auth_token(basic_token: str):
53
- import base64
54
-
55
- try:
56
- decoded = base64.b64decode(basic_token).decode("utf-8")
57
- client_id, client_secret = decoded.split(":")
58
- return client_id, client_secret
59
- except Exception as e:
60
- raise CtxpException(f"Invalid basic http auth token: {e}")
35
+ return auth_data and time.time() + leeway < auth_data["expires_at"]
61
36
 
62
37
 
63
38
  _login_lock = threading.Lock()
64
39
 
65
40
 
66
- def login(force_refresh: bool = False, panic_on_failure: bool = True):
41
+ def login(force_refresh: bool = False):
67
42
  """Ensure login state, using credentials from the current profile. Return oauth token."""
68
-
69
- _login_lock.acquire()
70
- try:
71
- effective_profile = profile.current()
72
-
73
- auth_data = profile.auth.get()
74
- if force_refresh or not _is_auth_valid(auth_data, effective_profile):
75
- oauth_token = _get_new_oauth_token(auth_data.token, effective_profile)
76
- if oauth_token:
77
- use_oauth_token(oauth_token, effective_profile)
78
- elif panic_on_failure:
79
- panic(
80
- "Login failed. If this is configured API key or client credential, refresh the credential from CSP and update profile config. If this is browser based interactive login, login again."
81
- )
82
- else:
83
- return None
84
- else:
85
- oauth_token = auth_data.token
86
- return oauth_token
87
- finally:
88
- _login_lock.release()
89
-
90
-
91
- def _get_new_oauth_token(old_oauth_token, effective_profile):
92
- csp_config = effective_profile.csp
93
-
94
- csp_client = CspClient(url=csp_config.url)
95
-
96
- if csp_config.apiToken:
97
- oauth_token = csp_client.login_with_api_token(csp_config.apiToken)
98
- elif csp_config.clientId:
99
- oauth_token = csp_client.login_with_client_id_and_secret(
100
- csp_config.clientId, csp_config.clientSecret, csp_config.orgId
101
- )
102
- elif csp_config.basic:
103
- client_id, client_secret = _decode_http_basic_auth_token(csp_config.basic)
104
- oauth_token = csp_client.login_with_client_id_and_secret(client_id, client_secret, csp_config.orgId)
105
- else:
106
- # This should be a config from interactive login.
107
- # Use existing oauth_token to refresh.
108
- if not old_oauth_token:
109
- old_oauth_token = profile.auth.get().token
110
- if old_oauth_token:
111
- try:
112
- oauth_token = refresh_oauth_token(old_oauth_token, csp_config.url)
113
- except Exception as e:
114
- oauth_token = None
115
- log.warning(e)
116
- else:
117
- oauth_token = None
118
-
119
- if oauth_token and not oauth_token.get("expires_at"):
120
- oauth_token["expires_at"] = int(time.time() + oauth_token["expires_in"])
121
-
122
- return oauth_token
43
+ return _populate_token_with_cache(profile.current().csp, force_refresh)
123
44
 
124
45
 
125
46
  def refresh_oauth_token(old_oauth_token: dict, csp_url: str):
@@ -139,18 +60,102 @@ def refresh_oauth_token(old_oauth_token: dict, csp_url: str):
139
60
  return new_token
140
61
 
141
62
 
142
- class MyOAuth2Client(OAuth2Client):
143
- def __init__(self):
63
+ def _has_credential(auth_config: dict):
64
+ return (
65
+ auth_config.get("apiToken")
66
+ or auth_config.get("api_token")
67
+ or auth_config.get("clientId")
68
+ or auth_config.get("client_id")
69
+ or auth_config.get("basic")
70
+ )
71
+
72
+
73
+ def get_auth_cache(auth_config: dict):
74
+ cache, hash, token = _get_auth_cache(auth_config)
75
+ return token
76
+
77
+
78
+ def _get_auth_cache(auth_config: dict):
79
+ text = json.dumps(auth_config, default=vars)
80
+ hash = hashlib.md5(text.encode("ascii"), usedforsecurity=False).hexdigest()
81
+ auth_cache = profile.auth.get()
82
+ token = auth_cache.get(hash, None)
83
+ if token and not _is_auth_valid(token):
84
+ token = None
85
+ return auth_cache, hash, token
86
+
87
+
88
+ def save_auth_cache(auth_config: dict, token: dict):
89
+ """Save the auth token to the profile auth cache."""
90
+ cache, hash, _ = _get_auth_cache(auth_config)
91
+ if not token:
92
+ if hash in cache:
93
+ del cache[hash]
94
+ return
95
+ if not token.get("expires_at"):
96
+ token["expires_at"] = int(time.time() + token["expires_in"])
97
+ cache[hash] = token
98
+ profile.auth.set(cache)
99
+
100
+
101
+ def _populate_token_with_cache(auth_config: dict, force_refresh: bool = False):
102
+
103
+ with _login_lock:
104
+ cache, hash, token = _get_auth_cache(auth_config)
105
+ if token and not force_refresh:
106
+ return token
107
+
108
+ # invalid token. Refresh or recreate it.
109
+ if token:
110
+ # try using refresh token if possible
111
+ if auth_config.get("provider", "vmwarecsp") == "vmwarecsp":
112
+ try:
113
+ token = refresh_oauth_token(token, auth_config.url)
114
+ except Exception as e:
115
+ log.debug(f"Failed to refresh OAuth token: {e}")
116
+ token = None
117
+ else:
118
+ # hcs auth-service. Does not support refresh token.
119
+ token = None
120
+
121
+ if not token:
122
+ if _has_credential(auth_config):
123
+ token = CspClient.create(**auth_config).oauth_token()
124
+ else:
125
+ if auth_config.get("browser"):
126
+ from .login_support import login_via_browser
127
+
128
+ token = login_via_browser(auth_config.url, auth_config.orgId)
129
+ if not token:
130
+ raise CtxpException("Browser auth failed.")
131
+ else:
132
+ raise CtxpException(
133
+ "Browser auth was never attempted and no client credentials or API token provided."
134
+ )
135
+
136
+ if not token.get("expires_at"):
137
+ token["expires_at"] = int(time.time() + token["expires_in"])
138
+
139
+ cache[hash] = token
140
+ profile.auth.set(cache)
141
+ return token
142
+
143
+
144
+ class CustomOAuth2Client(OAuth2Client):
145
+ def __init__(self, auth_config: dict):
144
146
  super().__init__()
147
+ self.auth_config = auth_config
145
148
 
146
149
  def ensure_token(self):
147
150
  # pylint: disable=access-member-before-definition
148
151
  if self.token is None or not super().ensure_active_token():
149
- self.token = login()
152
+ self.token = _populate_token_with_cache(self.auth_config)
150
153
 
151
154
 
152
- def oauth_client():
153
- return MyOAuth2Client()
155
+ def oauth_client(auth_config: dict = None):
156
+ if not auth_config:
157
+ auth_config = profile.current().csp
158
+ return CustomOAuth2Client(auth_config)
154
159
 
155
160
 
156
161
  def details(get_org_details: bool = False) -> dotdict:
@@ -179,9 +184,3 @@ def details_from_token(oauth_token, get_org_details: bool = False):
179
184
  def get_org_id_from_token(oauth_token: str) -> str:
180
185
  decoded = jwt.decode(oauth_token["access_token"], options={"verify_signature": False})
181
186
  return decoded["context_name"]
182
-
183
-
184
- def use_oauth_token(oauth_token, effective_profile=None):
185
- if effective_profile is None:
186
- effective_profile = profile.current()
187
- profile.auth.set({"token": oauth_token, "hash": _get_profile_auth_hash(effective_profile)})
@@ -14,95 +14,162 @@ limitations under the License.
14
14
  """
15
15
 
16
16
  import json
17
- import os
17
+ import logging
18
18
  import sys
19
19
  import threading
20
20
  import time
21
21
  from typing import Callable, Iterator
22
22
 
23
23
  from hcs_core.ctxp import CtxpException, panic, profile
24
- from hcs_core.sglib import hcs_client
25
24
  from hcs_core.sglib.ez_client import EzClient
26
25
  from hcs_core.util import duration, exit
27
26
  from hcs_core.util.query_util import PageRequest, with_query
28
27
 
28
+ log = logging.getLogger(__name__)
29
+
29
30
  _caches = {}
30
31
 
31
32
  _client_instance_lock = threading.RLock()
32
33
 
33
34
 
34
- def hdc_service_client(service_name: str) -> EzClient:
35
- _client_instance_lock.acquire()
36
- try:
37
- instance = _caches.get(service_name)
38
- if not instance:
39
-
40
- def _get_url(): # make it deferred so no need to initialize profile
41
- url = _get_hcs_url_considering_env_override(service_name)
42
- if not url.endswith("/"):
43
- url += "/"
44
- url += service_name
45
- return url
46
-
47
- instance = hcs_client(_get_url)
48
- _caches[service_name] = instance
49
- return instance
50
- finally:
51
- _client_instance_lock.release()
52
-
53
-
54
- def _get_hcs_url_considering_env_override(service_name: str):
55
- # service_name = service_name.replace("-", "_")
56
- # env_name = f"hcs_{service_name}_url"
57
- # url = os.environ.get(env_name)
58
- # if url:
59
- # print(f"Using env override for {env_name}: {url}")
60
- # return url
61
- # env_name = env_name.upper()
62
- # url = os.environ.get(env_name)
63
- # if url:
64
- # print(f"Using env override for {env_name}: {url}")
65
- # return url
35
+ def _lazy_init(
36
+ service_name: str, hdc: str = None, region: str = None
37
+ ): # make it deferred so no need to initialize profile
38
+ if region and hdc:
39
+ raise Exception("region and hdc cannot be specified at the same time.")
66
40
 
67
41
  # check per-service override in profile
68
- service_override = profile.current().get("overrides", {}).get(service_name, {})
42
+ profile_data = profile.current()
43
+ override = profile_data.get("override", {})
44
+ service_override = {}
45
+ for k, v in override.items():
46
+ if service_name == k.lower():
47
+ service_override = v
69
48
  service_override_url = service_override.get("url")
70
49
  if service_override_url:
71
- print(f"Using per-service override for {service_name}: {service_override_url}")
72
- return service_override_url
73
- return profile.current().hcs.url
50
+ log.debug(f"Using per-service override for {service_name}: {service_override_url}")
51
+ url = service_override_url
52
+ else:
53
+ if hdc:
54
+ # prod only
55
+ hdc = hdc.upper()
56
+ if hdc == "US":
57
+ url = profile_data.hcs.url
58
+ elif hdc == "EU":
59
+ url = profile_data.hcs.url.replace("cloud-sg-us", "cloud-sg-eu")
60
+ elif hdc == "JP":
61
+ url = profile_data.hcs.url.replace("cloud-sg-us", "cloud-sg-jp")
62
+ else:
63
+ raise CtxpException(f"Invalid HDC name: {hdc}. Supported HDC names: US, EU, JP.")
64
+ elif region:
65
+ url = _get_region_url(region)
66
+ if not url:
67
+ panic("Missing profile property: hcs.regions")
68
+ else:
69
+ url = profile_data.hcs.url
70
+ url = url.rstrip("/") + "/" + service_name
71
+
72
+ # TODO
73
+ client_id = service_override.get("client-id", None)
74
+ if not client_id:
75
+ client_id = service_override.get("clientId", None)
76
+ client_secret = service_override.get("client-secret", None)
77
+ if not client_secret:
78
+ client_secret = service_override.get("clientSecret", None)
79
+ api_token = service_override.get("api-token", None)
80
+ if not api_token:
81
+ api_token = service_override.get("apiToken", None)
82
+ provider = service_override.get("provider", "vmwarecsp")
83
+ if provider == "vmwarecsp":
84
+ token_url = profile_data.csp.url
85
+ elif provider == "auth-service":
86
+ token_url = profile_data.auth.tokenUrl
87
+ else:
88
+ raise CtxpException(f"Unknown provider: {provider}. Supported providers: vmwarecsp, auth-service.")
89
+
90
+ if client_id:
91
+ if not client_secret:
92
+ panic(f"Client ID is specified but missing clientSecret for service {service_name} in profile override.")
93
+ else:
94
+ if client_secret:
95
+ panic(f"Client secret is specified but missing clientId for service {service_name} in profile override.")
96
+
97
+ if client_id:
98
+ auth_config = {
99
+ "url": token_url,
100
+ "client_id": client_id,
101
+ "client_secret": client_secret,
102
+ }
103
+ elif api_token:
104
+ auth_config = {
105
+ "url": token_url,
106
+ "api_token": api_token,
107
+ }
108
+ else:
109
+ auth_config = None
110
+
111
+ from hcs_core.sglib import auth
112
+
113
+ oauth_client = auth.oauth_client(auth_config=auth_config)
114
+ return url, oauth_client
74
115
 
75
116
 
76
- def _get_region_url(region_name: str):
117
+ def hdc_service_client(service_name: str, hdc: str = None) -> EzClient:
118
+ """A client for HDC service. Cached per service name, thread-safe, lazy initialization."""
119
+ if hdc:
120
+ hdc = hdc.upper()
121
+ if hdc not in ["US", "EU", "JP"]:
122
+ panic(f"Invalid HDC name: {hdc}. Supported HDC names: US, EU, JP.")
123
+ else:
124
+ hdc = "US"
125
+
126
+ k = f"{service_name}#{hdc}"
127
+ with _client_instance_lock:
128
+ instance = _caches.get(k)
129
+ if not instance:
130
+ instance = EzClient(lazy_init=lambda: _lazy_init(service_name=service_name, hdc=hdc))
131
+ _caches[k] = instance
132
+ return instance
133
+
134
+
135
+ def _get_region_url(region: str):
77
136
  regions = profile.current().hcs.regions
78
- if not region_name:
137
+ if not region:
79
138
  return regions[0].url
80
139
  for r in regions:
81
- if r.name.lower() == region_name.lower():
140
+ if r.name.lower() == region.lower():
82
141
  return r.url
83
142
  names = []
84
143
  for r in regions:
85
144
  names.append(r.name)
86
- panic(f"Region not found: {region_name}. Available regions: {names}")
145
+ panic(f"Region not found: {region}. Available regions: {names}")
87
146
 
88
147
 
89
- def regional_service_client(region_name: str, service_name: str):
148
+ def regional_service_client(service_name: str, region: str = None):
90
149
  # 'https://dev1b-westus2-cp103a.azcp.horizon.vmware.com/vmhub'
91
- url = _get_region_url(region_name)
92
- if not url:
93
- panic("Missing profile property: hcs.regions")
94
- if not url.endswith("/"):
95
- url += "/"
96
- url += service_name
97
- return hcs_client(url)
98
-
99
-
100
- def _with_org_id(url: str, org_id: str):
101
- if org_id:
102
- if url.find("?") < 0:
103
- url += "?"
104
- url += "org_id=" + org_id
105
- return url
150
+ region_names = [r.name.lower() for r in profile.current().hcs.regions]
151
+ if region:
152
+ region = region.lower()
153
+ if region not in region_names:
154
+ panic(f"Invalid region name: {region}. Available regions: {', '.join(region_names)}")
155
+ else:
156
+ region = region_names[0]
157
+
158
+ k = f"{service_name}#{region}"
159
+ with _client_instance_lock:
160
+ instance = _caches.get(k)
161
+ if not instance:
162
+ instance = EzClient(lazy_init=lambda: _lazy_init(service_name=service_name, region=region))
163
+ _caches[k] = instance
164
+ return instance
165
+
166
+
167
+ def service_client(service_name: str, region: str = None, hdc: str = None):
168
+ regional_services = ["vmhub", "connection-service"]
169
+ if service_name in regional_services:
170
+ return regional_service_client(service_name, region)
171
+ else:
172
+ return hdc_service_client(service_name, hdc)
106
173
 
107
174
 
108
175
  class default_crud:
hcs_core/sglib/csp.py CHANGED
@@ -65,8 +65,25 @@ def _log_response(response: httpx.Response):
65
65
  log.debug("\n")
66
66
 
67
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
+
68
79
  class CspClient:
69
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"
70
87
  self._base_url = url
71
88
  self._oauth_token = oauth_token
72
89
  self._org_id = org_id
@@ -81,6 +98,7 @@ class CspClient:
81
98
  )
82
99
 
83
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}")
84
102
  # https://console-stg.cloud.vmware.com/csp/gateway/authn/api/swagger-ui.html#/Authentication/getAccessTokenByApiRefreshTokenUsingPOST
85
103
 
86
104
  # curl -X 'POST' \
@@ -88,7 +106,7 @@ class CspClient:
88
106
  # -H 'accept: application/json' \
89
107
  # -H 'Content-Type: application/x-www-form-urlencoded' \
90
108
  # -d 'refresh_token=<the-refresh-token>'
91
-
109
+ # print(f"Logging in with api_token: {api_token}, url: {self._base_url}")
92
110
  headers = {
93
111
  "Content-Type": "application/x-www-form-urlencoded",
94
112
  "Accept": "application/json",
@@ -101,6 +119,7 @@ class CspClient:
101
119
  return self._oauth_token
102
120
 
103
121
  def login_with_client_id_and_secret(self, client_id: str, client_secret: str, org_id: str) -> dict:
122
+ log.debug(f"Logging in with client_id: {client_id}, org_id: {org_id}, url: {self._base_url}")
104
123
  headers = {
105
124
  "Content-Type": "application/x-www-form-urlencoded",
106
125
  "Accept": "application/json",
@@ -110,8 +129,9 @@ class CspClient:
110
129
  }
111
130
  if org_id:
112
131
  params["orgId"] = org_id
132
+ url = "/auth/v1/oauth/token" if self._auth_mode == "hcs-auth-svc" else "/csp/gateway/am/api/auth/authorize"
113
133
  resp = self._client.post(
114
- "/csp/gateway/am/api/auth/authorize",
134
+ url,
115
135
  auth=(client_id, client_secret),
116
136
  headers=headers,
117
137
  params=params,
@@ -119,6 +139,54 @@ class CspClient:
119
139
  self._oauth_token = resp.json()
120
140
  return self._oauth_token
121
141
 
142
+ @staticmethod
143
+ def create(
144
+ url: str,
145
+ org_id: str = None,
146
+ client_id: str = None,
147
+ client_secret: str = None,
148
+ api_token: str = None,
149
+ basic: str = None,
150
+ **kwargs,
151
+ ) -> "CspClient":
152
+ client = CspClient(url=url, org_id=org_id)
153
+
154
+ if not client_id:
155
+ client_id = kwargs.get("clientId")
156
+ if not client_secret:
157
+ client_secret = kwargs.get("clientSecret")
158
+ if not api_token:
159
+ api_token = kwargs.get("apiToken")
160
+
161
+ if client_id:
162
+ if not client_secret:
163
+ raise ValueError("client_secret is required when client_id is provided")
164
+ if api_token:
165
+ raise ValueError("api_token and client_id/client_secret cannot be used together")
166
+ if basic:
167
+ raise ValueError("basic auth and client_id/client_secret cannot be used together")
168
+ client.login_with_client_id_and_secret(client_id=client_id, client_secret=client_secret, org_id=org_id)
169
+ elif api_token:
170
+ if client_id or client_secret:
171
+ raise ValueError("api_token and client_id/client_secret cannot be used together")
172
+ if basic:
173
+ raise ValueError("api_token and basic auth cannot be used together")
174
+ client.login_with_api_token(api_token)
175
+ elif basic:
176
+ if client_id or client_secret:
177
+ raise ValueError("basic auth and client_id/client_secret cannot be used together")
178
+ if api_token:
179
+ raise ValueError("basic auth and api_token cannot be used together")
180
+ client_id, client_secret = _decode_http_basic_auth_token(basic)
181
+ client.login_with_client_id_and_secret(client_id=client_id, client_secret=client_secret, org_id=org_id)
182
+ else:
183
+ raise Exception("Unrecognized CSP authentication method.")
184
+
185
+ return client
186
+
187
+ def oauth_token(self) -> dict:
188
+ return self._oauth_token
189
+
122
190
  # def get_oauth_token(self, force=False):
123
191
  # if self._oauth_token and not force:
124
192
  # return self._oauth_token
@@ -133,32 +133,47 @@ def _raise_http_error(e: httpx.HTTPStatusError):
133
133
 
134
134
 
135
135
  class EzClient:
136
- def __init__(
137
- self, base_url: Union[str, Callable], oauth_client: OAuth2Client = None, lazy_oauth_client: Callable = None
138
- ) -> None:
136
+ def __init__(self, base_url: str = None, oauth_client: OAuth2Client = None, lazy_init: Callable = None) -> None:
139
137
  # self._client = httpx.Client(base_url=base_url, timeout=30, event_hooks=event_hooks)
140
- self._base_url = base_url
141
- self._client_impl = oauth_client
142
- self._lazy_oauth_client = lazy_oauth_client
138
+
139
+ self._client_impl = None
140
+ self._lazy_init = lazy_init
143
141
  self._lock = threading.Lock()
144
142
 
143
+ if lazy_init:
144
+ if base_url:
145
+ raise ValueError("Cannot use both base_url and lazy_init")
146
+ if oauth_client:
147
+ raise ValueError("Cannot use both oauth_client and lazy_init")
148
+ else:
149
+ if not base_url:
150
+ raise ValueError("base_url must be provided if lazy_init is not used")
151
+ if not oauth_client:
152
+ raise ValueError("oauth_client must be provided if lazy_init is not used")
153
+ self._init_client(base_url, oauth_client)
154
+
155
+ def _init_client(self, base_url: str, client: OAuth2Client):
156
+ if base_url.endswith("/"):
157
+ base_url = base_url[:-1]
158
+ client.base_url = base_url
159
+ client.timeout = int(os.environ.get("HCS_TIMEOUT", 30))
160
+ request_hooks = client.event_hooks["request"]
161
+ response_hooks = client.event_hooks["response"]
162
+ if _log_request not in request_hooks:
163
+ request_hooks.append(_log_request)
164
+ if _log_response not in response_hooks:
165
+ response_hooks.append(_log_response)
166
+ if _raise_on_4xx_5xx not in response_hooks:
167
+ response_hooks.append(_raise_on_4xx_5xx)
168
+ self._client_impl = client
169
+
145
170
  def _client(self):
146
171
  self._lock.acquire()
147
172
  try:
148
- if not self._client_impl:
149
- client = self._lazy_oauth_client()
150
- base_url = self._base_url() if callable(self._base_url) else self._base_url
151
- client.base_url = base_url
152
- client.timeout = int(os.environ.get("HCS_TIMEOUT", 30))
153
- request_hooks = client.event_hooks["request"]
154
- response_hooks = client.event_hooks["response"]
155
- if _log_request not in request_hooks:
156
- request_hooks.append(_log_request)
157
- if _log_response not in response_hooks:
158
- response_hooks.append(_log_response)
159
- if _raise_on_4xx_5xx not in response_hooks:
160
- response_hooks.append(_raise_on_4xx_5xx)
161
- self._client_impl = client
173
+ if self._lazy_init:
174
+ base_url, client = self._lazy_init()
175
+ self._init_client(base_url, client)
176
+ self._lazy_init = None
162
177
 
163
178
  self._client_impl.ensure_token()
164
179
  return self._client_impl
@@ -13,13 +13,9 @@ 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, Union
17
-
18
16
  from . import auth as auth
19
17
  from .ez_client import EzClient
20
18
 
21
19
 
22
- def hcs_client(url: Union[str, Callable]) -> EzClient:
23
- if isinstance(url, str) and url.endswith("/"):
24
- url = url[:-1]
25
- return EzClient(url, lazy_oauth_client=auth.oauth_client)
20
+ def hcs_client(url: str, custom_auth: dict = None) -> EzClient:
21
+ return EzClient(base_url=url, oauth_client=auth.oauth_client(custom_auth))
hcs_core/util/job_view.py CHANGED
@@ -206,9 +206,10 @@ class JobView:
206
206
  def skip(self, id: str, reason: str) -> None:
207
207
  self._ensure_started(id)
208
208
  task_id = self._map[id]
209
- details = "skipped"
209
+ details = "<skipped>"
210
210
  if reason:
211
- details += f": {reason}"
211
+ details = reason + " " + details
212
+
212
213
  self._job_ctl.update(task_id, completed=sys.float_info.max, details=details)
213
214
  self._todo.discard(id)
214
215
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hcs-core
3
- Version: 0.1.288
3
+ Version: 0.1.290
4
4
  Summary: Horizon Cloud Service CLI module.
5
5
  Project-URL: Homepage, https://github.com/euc-eng/hcs-cli
6
6
  Project-URL: Bug Tracker, https://github.com/euc-eng/hcs-cli/issues
@@ -26,6 +26,7 @@ Requires-Dist: psutil>=5.9.4
26
26
  Requires-Dist: pydantic>=2.0.0
27
27
  Requires-Dist: pyjwt>=2.8.0
28
28
  Requires-Dist: pyopenssl>=24.1.0
29
+ Requires-Dist: python-dotenv>=1.1.0
29
30
  Requires-Dist: pyyaml>=6.0.1
30
31
  Requires-Dist: questionary>=2.0.1
31
32
  Requires-Dist: rel>=0.4.7
@@ -1,52 +1,52 @@
1
- hcs_core/__init__.py,sha256=h2YaDwqNzDQolRs8e5QZZuOdmuGRXaYq2mLZASylOIc,24
1
+ hcs_core/__init__.py,sha256=bwKnrwO9ZdAegnigTknEQxy0oRqb3ET-uKciKxJ5eh0,24
2
2
  hcs_core/ctxp/__init__.py,sha256=bHVHhJP10Luz1a3Kk3zFx14dAO4SY6Q20Lrv8rNWWGc,1075
3
3
  hcs_core/ctxp/_init.py,sha256=yq46VOty4zs_WcLt1SrtePxbW8S4RIz4YUCP3xIBvCA,2797
4
- hcs_core/ctxp/cli_options.py,sha256=5CkwKVIUNJxumBHUmrefawiIZR5Yf3bWEcpJBaEMuDU,2554
5
- hcs_core/ctxp/cli_processor.py,sha256=2RhBz7zFm2UDRwbMDwBpeAQQCt4sjZJzn1nePAcMA_o,7329
4
+ hcs_core/ctxp/cli_options.py,sha256=j4LUukG1k6etJFATb8mGXvqB7DiEC-VOP23w7KVO9sY,2751
5
+ hcs_core/ctxp/cli_processor.py,sha256=L4ai8ZAKBkhII_6dwehxNGPrrvFOrq0CFaMHav0F2Y0,7485
6
6
  hcs_core/ctxp/cmd_util.py,sha256=_-VwQSmkfi52qWC3uHQI06mSiIPfsZroDruTDYHXiMA,3119
7
7
  hcs_core/ctxp/config.py,sha256=vRdzPxi3Yrt04cnR6b5mJwEOtYBh21qvmlSSsgyGoI4,931
8
8
  hcs_core/ctxp/context.py,sha256=y0ouOzXyYTg9MOuWjzjls9WfOKYaO31_MGJJ2S-Y9p4,3375
9
- hcs_core/ctxp/data_util.py,sha256=v041qG3UH8ff4iLVY4oaeFDT1jBdFhUNOnGLraPMT-g,14812
9
+ hcs_core/ctxp/data_util.py,sha256=ODVts-Hy8zB76dGv0KaPYVoVqqh214H3l_Ux7B1HOKA,14985
10
10
  hcs_core/ctxp/dispatcher.py,sha256=GAK-jGXDpyX0C88oRDtDi4yeM0qlvID96k98z_vqJkg,2261
11
11
  hcs_core/ctxp/duration.py,sha256=xmAw-nKnul3VeSXzvbGZ-W2ByBwyK23KxLLm3_aFW_M,2117
12
12
  hcs_core/ctxp/extension.py,sha256=UqlDCXJMRKXcoceVak8hVGI_7HggjUWYSgKOFbGVetU,1636
13
13
  hcs_core/ctxp/fn_util.py,sha256=bEYmj5WgUkRE5S4oQjT_zutuViXWYU9q5MR-MlTA_bY,1573
14
- hcs_core/ctxp/fstore.py,sha256=qgd4aDLrRWqSsB-7StBv1TAqd0wxIB-scvt6FZSTtyQ,8958
14
+ hcs_core/ctxp/fstore.py,sha256=lU6et0-w_QPYpQNjdA0QGEdnn7lsmBVuw9XbDvLjwso,8977
15
15
  hcs_core/ctxp/jsondot.py,sha256=U2_ToR8j3-hex75X3OSqTgADiw5cqjRDVO38-IbvapM,11369
16
16
  hcs_core/ctxp/logger.py,sha256=6Sh1WXUrjrQNU7K6zUlBzE34fIS6o6oMU3eqSknUOEo,6321
17
- hcs_core/ctxp/profile.py,sha256=DQUQXThHNZa49tDcyGfh-w658CJBE1YIKmNHWXSDaiw,7673
17
+ hcs_core/ctxp/profile.py,sha256=JgDhRQD00p4jIsiTEjDzpRfKyicA-TgAheXxMBiNCvw,7766
18
18
  hcs_core/ctxp/profile_store.py,sha256=v4RvKSaKSJhAt5rEGbC6v-NfaI3N67YffPm-qlrQWdg,1566
19
19
  hcs_core/ctxp/recent.py,sha256=uKSEdU4L2szbyl-5nYLa3bigZSvSPW8G24Tuk_wYMck,1797
20
20
  hcs_core/ctxp/state.py,sha256=gjcMRVONKBq7WiFQo-xca1726ngZe90mnPrF3vbXDrU,1634
21
21
  hcs_core/ctxp/task_schd.py,sha256=mvZMeKDSSo2p7VidSoZY1XZj433TQn_YF9SGJEzl9lg,4586
22
22
  hcs_core/ctxp/template_util.py,sha256=XslvIuRBlTVsUW0Y9M_D8gUPc1jWq6X2p4At2VAe1KU,731
23
23
  hcs_core/ctxp/timeutil.py,sha256=RyRrIRdFHbIghdB8wbC8VdABvc7hki2v51b1x2JvHgo,445
24
- hcs_core/ctxp/util.py,sha256=FnJMbKMCnGDGc579VSrGsnWy-0H1GEnLioLcFvH6UcM,10869
24
+ hcs_core/ctxp/util.py,sha256=5RrAjSAM3GRu23a6mnKAnJ6X-WyW9NptngPEEDzm5QM,11434
25
25
  hcs_core/ctxp/var_template.py,sha256=cTjj1UJ58ac6s5z4Oh5hSDQwKixq-rdbCF1D8akjAo0,3219
26
26
  hcs_core/ctxp/built_in_cmds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  hcs_core/ctxp/built_in_cmds/_ut.py,sha256=e50XBmPim2qRe0RYk_XAuRz1HWYyLJuMu-6dVxWR_fQ,3356
28
28
  hcs_core/ctxp/built_in_cmds/context.py,sha256=bU-BG60DyA8rpv1sw3LpHnPQb1wQiZQCoIMYLW_g5_c,2389
29
- hcs_core/ctxp/built_in_cmds/profile.py,sha256=4r1Yi9lR0oWXeVNzQTblYYkmX94s1aM8mYHV4u0Yfqw,4324
30
- hcs_core/plan/__init__.py,sha256=-aEHqDPQd9Noqwg8bV8YBKxE9xrY25PFp2pknFXZiIc,428
29
+ hcs_core/ctxp/built_in_cmds/profile.py,sha256=XjFEIPBAgLJjOQpAPqL-HtTmycWZEps2_S6yY-0wT-k,4401
30
+ hcs_core/plan/__init__.py,sha256=5klVuXXLQLn2Hj6Gz2Hc-1XSyDXKE9o-BjQ82oXekiI,465
31
31
  hcs_core/plan/actions.py,sha256=UvCynzApJUxi39HUoPIQ_uownlbvFbJ4aAs6pmulrLY,125
32
32
  hcs_core/plan/base_provider.py,sha256=CSJFN8lopWTUblw8CW78Epnef9BbJVLhPLk9bPfZceM,1299
33
33
  hcs_core/plan/context.py,sha256=5NI5Otk0jGKtBGvO8Sc1xdOHUCu_lXQYNrSctT3Hyq8,116
34
- hcs_core/plan/core.py,sha256=21pSOJPcJCidQwNkLDgILaYVOElUZmc0nEwDA5itP_U,21132
34
+ hcs_core/plan/core.py,sha256=1hZ9Y8ltEFGAT1MbbxpxLSwMU3TwEEEcbRJeO_KrZ-Y,21722
35
35
  hcs_core/plan/dag.py,sha256=pyyoMSmbhCsS8BMZI6hW6YDkHqBgjmJOwys9CUDrIts,12926
36
36
  hcs_core/plan/helper.py,sha256=__b8tlzLBT1Rm3vgiS-pWnZImaoZbsmKSRJtt_1gj8E,7763
37
- hcs_core/plan/kop.py,sha256=rhA4L8PUqImUTLvqH94jGlh4gmFk1UElVJLjhtZP-jQ,5773
37
+ hcs_core/plan/kop.py,sha256=iDnRbYHFCtS3Rs95UVvRgZdSzs-NMwdyr8SztIQQqyg,5774
38
38
  hcs_core/plan/provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
39
  hcs_core/plan/provider/dev/__init__.py,sha256=WpPDB-h5z5uF2BhcRlsdeJyZxBU5dun6tCf5ZZeaTJU,30
40
40
  hcs_core/plan/provider/dev/_prepare.py,sha256=PvVnheEQwpj8sWYz2lDONQVTs4pHPYuo2c4VD2if-hc,34
41
41
  hcs_core/plan/provider/dev/dummy.py,sha256=zKEr9J4WHhlN5gFdmrFyEfCF0xlQlCJg0CC1dG9VaLA,1958
42
42
  hcs_core/plan/provider/dev/fibonacci.py,sha256=8WhDr5c9harNAlVPZomQJEqbWe0hUqbppO6ZkwEUcJ0,1104
43
43
  hcs_core/sglib/__init__.py,sha256=oT0etW7vsEbHlXiGL5x23ZXyyFqeLi81RxQQ5lfKSV0,654
44
- hcs_core/sglib/auth.py,sha256=otsiBEvKae3B0-bPt5Ggir7ceTRLWG1x7ZaSblHisdA,6443
44
+ hcs_core/sglib/auth.py,sha256=axgzgTRsXPHEo9VvZ6FLF2uitvFf8vVgHXc3ITTtgkM,6297
45
45
  hcs_core/sglib/cli_options.py,sha256=NvdiHpX_o6IYPEM8cQYLb_R7T4aiXVvYLqn6Vk4Q2-Y,1761
46
- hcs_core/sglib/client_util.py,sha256=ZCXmNfmUL4_iry4yU2bPWL0dLLNIGu3zp541cFt6Or0,10876
47
- hcs_core/sglib/csp.py,sha256=anZqbQLIZw831PgW9Z4pweqfAeBH7rXPIAkwkyPkfGY,8054
48
- hcs_core/sglib/ez_client.py,sha256=iUhhKAeo-9Mm3JQdYJROXxvAkZypCLSNvraE5DaP3n8,7640
49
- hcs_core/sglib/hcs_client.py,sha256=pxRZQ79ABhOyihAI8oOeTxw2dXkce5-NyRkJAoo0OL0,888
46
+ hcs_core/sglib/client_util.py,sha256=JvULJXwZzg0NBVEEfqbmiHA123KwGbWJevOO9X-0oj4,13551
47
+ hcs_core/sglib/csp.py,sha256=o10XdRO1-vVzzgYcn8kvUJ6EMRsv6ruDh6Q_cqy4pvs,11013
48
+ hcs_core/sglib/ez_client.py,sha256=_u9HvPXW5H8zDjaBr3bsTZqzveOlehq4EWDMeF_cwFY,8135
49
+ hcs_core/sglib/hcs_client.py,sha256=pjrAVQDEy2tiQMypMpOzODFntT6aNHXjje8or_mkL2U,804
50
50
  hcs_core/sglib/init.py,sha256=w_0ZU70Q1TGbSZsqSi7ewKQqpExFlepOT7mIqH0wnho,310
51
51
  hcs_core/sglib/login_support.py,sha256=tSkfmThubbWDUFLfx0hzdHp_QW0TR3gYISzvw6FKpjs,7561
52
52
  hcs_core/sglib/payload_util.py,sha256=Hnj7rjzrQ1j5gpbrwZX34biN8MIZjy6dOJZ63ulmzdw,685
@@ -57,12 +57,12 @@ hcs_core/util/check_license.py,sha256=-ZBMVoVcEUwICZ0QRK-e2uMMla7gFbjP9upun93lPE
57
57
  hcs_core/util/duration.py,sha256=e7Nw22aYXJK-pLM59QDel5atLZxShKBiVILFgrtLJks,4194
58
58
  hcs_core/util/exit.py,sha256=UStMZKlfCFN7GBouc1y3pPFGPFQ66qfcRZ_fYQXFD0E,838
59
59
  hcs_core/util/hcs_constants.py,sha256=Ic1Tx_UNJiQchfsdnRDzgiOaCjKHnsWXx997nElppm4,1755
60
- hcs_core/util/job_view.py,sha256=DuFdcvIqmbiKrzTjlZycF32ndvQNhq-2CL4No-HHBCw,8397
60
+ hcs_core/util/job_view.py,sha256=SsACAqHTXSCmKFDD0DDDNK2eKykGgjo_DlLw8CJbRxM,8408
61
61
  hcs_core/util/pki_util.py,sha256=Lt3-IzIoGcaQKNE7KUszxR7JSZkpXduVZJ262TszsIs,6685
62
62
  hcs_core/util/query_util.py,sha256=5bh3bUVIQuY9qerndfuyfyzkTExYJ8zD0_e3PSN7y-4,3142
63
63
  hcs_core/util/scheduler.py,sha256=bPpCmGUL1UctJMfLPAg-h4Hl2YZr96FiI78-G_Usn08,2958
64
64
  hcs_core/util/ssl_util.py,sha256=MvU102fGwWWh9hhSmLnn1qQIWuD6TjZnN0iH0MXUtW0,1239
65
65
  hcs_core/util/versions.py,sha256=urMtShfoBx_Eqq0D-450LP0i-rW447k9yX8q8j5H_qA,1721
66
- hcs_core-0.1.288.dist-info/METADATA,sha256=OfTNqAnTG99Mi9CeE9Sikd8DtFGU1BfNBjVb43CRM1M,1875
67
- hcs_core-0.1.288.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
68
- hcs_core-0.1.288.dist-info/RECORD,,
66
+ hcs_core-0.1.290.dist-info/METADATA,sha256=JsjF5lDDx6OWqmoe_8T2qZF5yRNEsPpNZ-N53y1r-po,1911
67
+ hcs_core-0.1.290.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
68
+ hcs_core-0.1.290.dist-info/RECORD,,