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,16 +13,45 @@ See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
14
  """
15
15
 
16
- from authlib.integrations.httpx_client import OAuth2Client
16
+ import json
17
+ import logging
18
+ import os
19
+ import sys
20
+ import threading
17
21
  from http.client import HTTPResponse
22
+ from typing import Callable, Optional, Type
23
+
18
24
  import httpx
19
- from typing import Callable
20
- import logging
21
- import json
25
+ from authlib.integrations.httpx_client import OAuth2Client
26
+ from pydantic import BaseModel
27
+
22
28
  from hcs_core.ctxp import jsondot
23
29
 
24
30
  log = logging.getLogger(__name__)
25
31
 
32
+ _print_http_error = True
33
+
34
+
35
+ def parse(resp: httpx.Response):
36
+ if not resp.content:
37
+ return None
38
+ content_type = resp.headers["Content-Type"]
39
+ if content_type.startswith("text"):
40
+ return resp.text
41
+ if len(resp.content) > 3:
42
+ try:
43
+ data = resp.json()
44
+ if isinstance(data, list):
45
+ return [jsondot.dotify(obj) for obj in data]
46
+ elif isinstance(data, dict):
47
+ return jsondot.dotify(data)
48
+ except:
49
+ log.info("--- Fail parsing json. Dump content ---")
50
+ log.info(resp.content)
51
+ raise
52
+ else:
53
+ return None
54
+
26
55
 
27
56
  def _raise_on_4xx_5xx(response: httpx.Response):
28
57
  if not response.is_success:
@@ -97,66 +126,135 @@ def on404ReturnNone(func):
97
126
  raise
98
127
 
99
128
 
129
+ def _raise_http_error(e: httpx.HTTPStatusError):
130
+ if _print_http_error:
131
+ print(e.response.text, file=sys.stderr)
132
+ raise e
133
+
134
+
100
135
  class EzClient:
101
- def __init__(self, base_url: str, oauth_client: OAuth2Client = None, lazy_oauth_client: Callable = None) -> None:
136
+ def __init__(self, base_url: str = None, oauth_client: OAuth2Client = None, lazy_init: Callable = None) -> None:
102
137
  # self._client = httpx.Client(base_url=base_url, timeout=30, event_hooks=event_hooks)
103
- self._base_url = base_url
104
- self._client_impl = oauth_client
105
- self._lazy_oauth_client = lazy_oauth_client
138
+
139
+ self._client_impl = None
140
+ self._lazy_init = lazy_init
141
+ self._lock = threading.Lock()
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
106
169
 
107
170
  def _client(self):
108
- if not self._client_impl:
109
- client = self._lazy_oauth_client()
110
- client.base_url = self._base_url
111
- client.timeout = 30
112
- request_hooks = client.event_hooks["request"]
113
- response_hooks = client.event_hooks["response"]
114
- if _log_request not in request_hooks:
115
- request_hooks.append(_log_request)
116
- if _log_response not in response_hooks:
117
- response_hooks.append(_log_response)
118
- if _raise_on_4xx_5xx not in response_hooks:
119
- response_hooks.append(_raise_on_4xx_5xx)
120
- self._client_impl = client
121
- return self._client_impl
122
-
123
- def post(self, url: str, json: dict = None, text: str = None, files=None, headers: dict = None):
124
- resp = self._client().post(url, json=json, content=text, files=files, headers=headers)
125
- return _parse_resp(resp)
171
+ self._lock.acquire()
172
+ try:
173
+ if self._lazy_init:
174
+ base_url, client = self._lazy_init()
175
+ self._init_client(base_url, client)
176
+ self._lazy_init = None
177
+
178
+ self._client_impl.ensure_token()
179
+ return self._client_impl
180
+ finally:
181
+ self._lock.release()
126
182
 
127
- def get(self, url: str, raise_on_404: bool = False):
183
+ def post(
184
+ self,
185
+ url: str,
186
+ json: dict = None,
187
+ text: str = None,
188
+ files=None,
189
+ headers: dict = None,
190
+ type: Optional[Type[BaseModel]] = None,
191
+ timeout: float = 30.0,
192
+ ):
193
+ # import json as jsonlib
194
+ # print("->", self._client().base_url, url, jsonlib.dumps(json, indent=4))
128
195
  try:
129
- resp = self._client().get(url)
130
- return _parse_resp(resp)
196
+ resp = self._client().post(url, json=json, content=text, files=files, headers=headers, timeout=timeout)
197
+ except httpx.HTTPStatusError as e:
198
+ _raise_http_error(e)
199
+ data = _parse_resp(resp)
200
+ if data and type:
201
+ try:
202
+ return type.model_validate(data)
203
+ except:
204
+ log.info("--- Fail converting model. Dump content ---")
205
+ log.info(data)
206
+ raise
207
+ return data
208
+
209
+ def get(self, url: str, headers: dict = None, raise_on_404: bool = False, type: Optional[Type[BaseModel]] = None):
210
+ try:
211
+ # print("->", self._client().base_url, url)
212
+ resp = self._client().get(url, headers=headers)
213
+ data = _parse_resp(resp)
214
+ if data and type:
215
+ try:
216
+ return type.model_validate(data)
217
+ except:
218
+ log.info("--- Fail converting model. Dump content ---")
219
+ log.info(data)
220
+ raise
221
+ return data
131
222
  except httpx.HTTPStatusError as e:
132
223
  if _is_404(e):
133
224
  if raise_on_404:
134
- raise e
225
+ _raise_http_error(e)
135
226
  else:
136
227
  pass
137
228
  else:
138
- raise
229
+ _raise_http_error(e)
139
230
 
140
- def patch(self, url: str, json: dict):
141
- resp = self._client().patch(url, json=json)
142
- return _parse_resp(resp)
231
+ def patch(self, url: str, json: dict = None, text=None, headers: dict = None):
232
+ try:
233
+ resp = self._client().patch(url, json=json, content=text, headers=headers)
234
+ return _parse_resp(resp)
235
+ except httpx.HTTPStatusError as e:
236
+ _raise_http_error(e)
237
+ raise
143
238
 
144
- def delete(self, url: str, raise_on_404: bool = False):
239
+ def delete(self, url: str, headers: dict = None, raise_on_404: bool = False):
145
240
  try:
146
- resp = self._client().delete(url)
241
+ resp = self._client().delete(url, headers=headers)
147
242
  return _parse_resp(resp)
148
243
  except httpx.HTTPStatusError as e:
149
244
  if _is_404(e):
150
245
  if raise_on_404:
151
- raise
246
+ _raise_http_error(e)
152
247
  else:
153
248
  pass
154
249
  else:
155
- raise
250
+ _raise_http_error(e)
156
251
 
157
- def put(self, url: str, json: dict):
158
- resp = self._client().put(url, json=json)
159
- return _parse_resp(resp)
252
+ def put(self, url: str, json: dict = None, text=None, headers: dict = None):
253
+ try:
254
+ resp = self._client().put(url, json=json, content=text, headers=headers)
255
+ return _parse_resp(resp)
256
+ except httpx.HTTPStatusError as e:
257
+ _raise_http_error(e)
160
258
 
161
259
  def close(self):
162
260
  self._client().close()
@@ -13,15 +13,9 @@ See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
14
  """
15
15
 
16
- from hcs_core.ctxp import profile
17
- from .ez_client import EzClient
18
16
  from . import auth as auth
17
+ from .ez_client import EzClient
19
18
 
20
19
 
21
- def hcs_client(url: str) -> EzClient:
22
- if not url:
23
- url = profile.current().hcs.url
24
- if url.endswith("/"):
25
- url = url[:-1]
26
-
27
- 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/sglib/init.py ADDED
@@ -0,0 +1,17 @@
1
+ import atexit
2
+ import logging
3
+
4
+ log = logging.getLogger(__name__)
5
+ _sg_client = None
6
+
7
+
8
+ # Work around issue:
9
+ # http11.py, line 211, in close: AttributeError: 'NoneType' object has no attribute 'CLOSED'
10
+ def _on_exit():
11
+ if _sg_client:
12
+ _sg_client.close()
13
+
14
+ log.info("Exit")
15
+
16
+
17
+ atexit.register(_on_exit)
@@ -16,17 +16,16 @@ limitations under the License.
16
16
  # nanw @ 2023
17
17
  # PKCE with authlib: https://docs.authlib.org/en/latest/specs/rfc7636.html
18
18
 
19
+ import logging
20
+ import platform
21
+ import secrets
19
22
  import threading
20
- import time
21
- from typing import Callable
22
23
  import webbrowser
23
- import secrets
24
- import logging
25
- from hcs_core.util.scheduler import ThreadPoolScheduler
24
+ from http.server import BaseHTTPRequestHandler, HTTPServer
25
+ from urllib.parse import parse_qs, urlparse
26
+
26
27
  from authlib.integrations.httpx_client import OAuth2Client
27
28
  from authlib.oauth2.rfc7636 import create_s256_code_challenge
28
- from http.server import BaseHTTPRequestHandler, HTTPServer
29
- from urllib.parse import urlparse, parse_qs
30
29
 
31
30
  log = logging.getLogger(__name__)
32
31
 
@@ -35,10 +34,12 @@ _callback_url = "/hcs-cli/oauth/callback"
35
34
 
36
35
  _public_client_ids = {
37
36
  "production": "ldjmWbBAUcSB1w3HSbzoYcdKoloYFqT2dWK",
37
+ # "production": "WbvEYXK4abljPJUuY2zODLpOaX5ddH2uhMX", #collie mobile
38
38
  "staging": "BbwHlgWt0vFwPUZMJTb5IKltynXjDmb46IO",
39
39
  }
40
40
 
41
41
  _auth_code_event = threading.Event()
42
+ _auth_code_event.value = None
42
43
  _state = None
43
44
 
44
45
  _auth_success_html = """
@@ -62,10 +63,10 @@ _auth_success_html = """
62
63
  </style>
63
64
  </head>
64
65
  <body>
65
- <h3>You have successfully logged into VMware Horizon Cloud Service.</h3>
66
+ <h3>You have successfully logged into Omnissa Horizon Cloud Service.</h3>
66
67
  <p>You can close this window, and return to the terminal.</p>
67
68
  <br/>
68
- <p><a href="https://github.com/euc-eng/hcs-cli">HCS CLI</a> is in beta phase. <a href="https://github.com/euc-eng/hcs-cli/blob/main/doc/hcs-cli-cheatsheet.md">Cheatsheet</a>:</p>
69
+ <p><a href="https://github.com/euc-eng/hcs-cli/blob/dev/README.md">HCS CLI</a> is in tech preview. <a href="https://github.com/euc-eng/hcs-cli/blob/main/doc/hcs-cli-cheatsheet.md">Cheatsheet</a>:</p>
69
70
  <code>
70
71
  # To get the login details: <br/>
71
72
  hcs login -d <br/><br/>
@@ -167,9 +168,18 @@ def do_oauth2_pkce(csp_url: str, client_id: str, org_id: str):
167
168
  webbrowser.open(authorization_url, new=0, autoraise=True)
168
169
 
169
170
  try:
170
- _auth_code_event.wait()
171
- except:
172
- log.info("Aborted")
171
+ # On Windows, wait() without timeout doesn't respond to CTRL+C properly
172
+ # Use a polling loop with timeout to allow KeyboardInterrupt to be caught
173
+ if platform.system() == "Windows":
174
+ while not _auth_code_event.wait(timeout=0.5):
175
+ pass # Keep waiting until the event is set
176
+ else:
177
+ _auth_code_event.wait()
178
+ except KeyboardInterrupt:
179
+ log.info("Aborted by user")
180
+ return
181
+ except Exception as e:
182
+ log.error(f"Login error: {e}")
173
183
  return
174
184
  # Once the user is redirected back to your app with an authorization code, exchange it for an access token
175
185
  authorization_code = _auth_code_event.value
@@ -206,77 +216,6 @@ def login_via_browser(csp_url: str, org_id: str):
206
216
  return do_oauth2_pkce(csp_url, client_id, org_id)
207
217
 
208
218
 
209
- def create_oauth_client(
210
- oauth_token: dict, csp_url: str, fn_on_new_oauth_token: Callable = None, fn_custom_refresh: Callable = None
211
- ):
212
- client_id = identify_client_id(csp_url)
213
-
214
- if not fn_custom_refresh and client_id:
215
- client = OAuth2Client(
216
- client_id=client_id,
217
- client_secret="", # Required by CSP auth
218
- token=oauth_token,
219
- token_endpoint=csp_url + "/csp/gateway/am/api/auth/token",
220
- token_endpoint_auth_method="client_secret_basic",
221
- update_token=fn_on_new_oauth_token,
222
- )
223
- else:
224
- # This is the case for production today, no csp-oauth-app yet,
225
- # and auth with API token case.
226
- client = OAuth2Client(
227
- # client_id=client_id,
228
- # client_secret="", # Required by CSP auth
229
- token=oauth_token,
230
- # token_endpoint=csp_url + "/csp/gateway/am/api/auth/token",
231
- # token_endpoint_auth_method="client_secret_basic",
232
- # update_token=fn_on_new_oauth_token,
233
- )
234
- # start our own token refresh timer. Refresh using provided method
235
- if oauth_token and fn_on_new_oauth_token:
236
- _register_custom_refresher(client, oauth_token, fn_on_new_oauth_token, fn_custom_refresh)
237
- else:
238
- # not even initialized.
239
- pass
240
- return client
241
-
242
-
243
- _scheduler: ThreadPoolScheduler = None
244
-
245
-
246
- def _register_custom_refresher(
247
- client: OAuth2Client, oauth_token, fn_on_new_oauth_token: Callable, fn_custom_refresh: Callable
248
- ):
249
- global _scheduler
250
- if not _scheduler:
251
- _scheduler = ThreadPoolScheduler()
252
-
253
- def get_refresh_delay(token):
254
- return token["expires_at"] - time.time() - 30
255
-
256
- def refresh_impl():
257
- if client.is_closed:
258
- return
259
- new_token = fn_custom_refresh()
260
- client.token = new_token
261
- fn_on_new_oauth_token(new_token)
262
- delay = get_refresh_delay(new_token)
263
- _scheduler.submit(refresh_impl, delay)
264
-
265
- delay = get_refresh_delay(oauth_token)
266
- _scheduler.submit(refresh_impl, delay)
267
-
268
-
269
- def refresh_oauth_token(old_oauth_token: dict, csp_url: str):
270
- with create_oauth_client(old_oauth_token, csp_url) as client:
271
- token_endpoint = csp_url + "/csp/gateway/am/api/auth/token"
272
- new_token = client.refresh_token(token_endpoint)
273
- if not new_token:
274
- raise Exception("CSP auth refresh failed.")
275
- if "cspErrorCode" in new_token:
276
- raise Exception(f"CSP auth failed: {new_token.get('message')}")
277
- return new_token
278
-
279
-
280
219
  # def _test():
281
220
  # import jwt
282
221
  # import json
@@ -1,7 +1,9 @@
1
1
  import json
2
+
2
3
  import yaml
3
- from .cli_options import get_org_id
4
+
4
5
  from ..ctxp import CtxpException
6
+ from .cli_options import get_org_id
5
7
 
6
8
 
7
9
  def get_payload_with_defaults(file_arg, org: str):
@@ -0,0 +1,38 @@
1
+ import logging
2
+
3
+ import httpx
4
+
5
+ from hcs_core.ctxp import jsondot
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+
10
+ def parse(resp: httpx.Response):
11
+ if not resp.content:
12
+ return None
13
+ content_type = resp.headers["Content-Type"]
14
+ if content_type.startswith("text"):
15
+ return resp.text
16
+ if len(resp.content) > 3:
17
+ try:
18
+ data = resp.json()
19
+ if isinstance(data, list):
20
+ return [jsondot.dotify(obj) for obj in data]
21
+ elif isinstance(data, dict):
22
+ return jsondot.dotify(data)
23
+ except:
24
+ log.info("--- Fail parsing json. Dump content ---")
25
+ log.info(resp.content)
26
+ raise
27
+ else:
28
+ return None
29
+
30
+
31
+ def on404ReturnNone(func):
32
+ try:
33
+ resp = func()
34
+ return parse(resp)
35
+ except httpx.HTTPStatusError as e:
36
+ if e.response.status_code == 404:
37
+ return None
38
+ raise
@@ -0,0 +1,107 @@
1
+ import os
2
+ import subprocess
3
+ import time
4
+ import zipfile
5
+
6
+ from hcs_core.ctxp import jsondot
7
+
8
+
9
+ def _print_output(text, name):
10
+ if len(text) == 0:
11
+ print(name + ": <EMPTY>")
12
+ else:
13
+ print(name + ":")
14
+ lines = text.splitlines()
15
+ for line in lines:
16
+ print(">> " + line)
17
+
18
+
19
+ def run(cmd, check=True):
20
+ print("CMD: " + cmd)
21
+ parts = cmd.split(" ")
22
+ proc = subprocess.run(
23
+ parts,
24
+ shell=False,
25
+ check=False,
26
+ text=True,
27
+ stdout=subprocess.PIPE,
28
+ stderr=subprocess.STDOUT,
29
+ encoding="utf-8",
30
+ )
31
+ if check and proc.returncode != 0:
32
+ _print_output(proc.stdout, "STDOUT")
33
+ _print_output(proc.stderr, "STDERR")
34
+ proc.check_returncode()
35
+ return proc
36
+
37
+
38
+ def _run_and_capture_output(cmd):
39
+ proc = subprocess.run(cmd, shell=True, check=False, text=True, capture_output=True, encoding="utf-8")
40
+ if proc.returncode != 0:
41
+ _print_output(proc.stdout, "STDOUT")
42
+ _print_output(proc.stderr, "STDERR")
43
+ proc.check_returncode()
44
+ return proc.stdout.strip()
45
+
46
+
47
+ def run_govc_guest_script(script, args):
48
+ print("Running guest script: " + script + " " + args)
49
+ cmd = f'govc guest.start {script} "{args} >/tmp/script-out 2>/tmp/script-err"'
50
+ output = _run_and_capture_output(cmd)
51
+
52
+ # wait for the process to finish
53
+ cmd = f"govc guest.ps -X -x -json -p {output}"
54
+ output = _run_and_capture_output(cmd)
55
+ ret = jsondot.parse(output)
56
+
57
+ output = _run_and_capture_output("govc guest.download /tmp/script-out -")
58
+ _print_output(output, "STDOUT")
59
+ output = _run_and_capture_output("govc guest.download /tmp/script-err -")
60
+ if len(output) > 0:
61
+ _print_output(output, "STDERR")
62
+
63
+ if ret.ProcessInfo[0].ExitCode != 0:
64
+ raise Exception("Fail running script: " + script)
65
+
66
+
67
+ def waitForVmPowerOff(iPath):
68
+
69
+ def current_millis():
70
+ return int(round(time.time() * 1000))
71
+
72
+ start_time = current_millis()
73
+ while current_millis() - start_time < 5 * 60 * 1000:
74
+ p = subprocess.run(
75
+ "govc vm.info -vm.ipath=" + iPath,
76
+ stdout=subprocess.PIPE,
77
+ stderr=subprocess.STDOUT,
78
+ shell=True,
79
+ )
80
+ if str(p.stdout).find("poweredOff") > 0:
81
+ return
82
+ time.sleep(5)
83
+ raise Exception("Timeout customization. Check log in deployer VM.")
84
+
85
+
86
+ def zipdir(src_dir, target_file):
87
+ with zipfile.ZipFile(target_file, "w", zipfile.ZIP_DEFLATED) as file:
88
+ for root, dirs, files in os.walk(src_dir):
89
+ for name in files:
90
+ filePath = os.path.join(root, name)
91
+ arcname = filePath[len(src_dir) :]
92
+ file.write(filePath, arcname)
93
+
94
+
95
+ def dos2unix(file):
96
+ file = os.path.abspath(file)
97
+ content = ""
98
+ outsize = 0
99
+ with open(file, "rb") as infile:
100
+ content = infile.read()
101
+ with open(file, "wb") as output:
102
+ for line in content.splitlines():
103
+ outsize += len(line) + 1
104
+ output.write(line + b"\n")
105
+ stripped_bytes = len(content) - outsize
106
+ if stripped_bytes != 0:
107
+ print("dos2unix: Stripped %s bytes. %s" % (stripped_bytes, file))
@@ -13,8 +13,6 @@ See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
14
  """
15
15
 
16
- #!/usr/bin/env -S python3 -W ignore
17
-
18
16
  import os
19
17
  import sys
20
18
 
hcs_core/util/duration.py CHANGED
@@ -14,7 +14,7 @@ limitations under the License.
14
14
  """
15
15
 
16
16
  import re
17
- from datetime import timedelta, datetime, timezone
17
+ from datetime import datetime, timedelta, timezone
18
18
 
19
19
  PATTERN = re.compile("^(([0-9]+)D)?(([0-9]+)H)?(([0-9]+)M)?(([0-9]+)S)?$")
20
20
 
@@ -116,10 +116,13 @@ def to_utc(dt):
116
116
 
117
117
 
118
118
  def stale(when) -> str:
119
+ return format_timedelta(from_now(when), True)
120
+
121
+
122
+ def from_now(when) -> timedelta:
119
123
  when_utc = to_utc(when)
120
124
  now_utc = datetime.now().astimezone(timezone.utc)
121
- diff = now_utc - when_utc
122
- return format_timedelta(diff, True)
125
+ return now_utc - when_utc
123
126
 
124
127
 
125
128
  def _test():