hcs-core 0.1.283__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.
- hcs_core/__init__.py +1 -1
- hcs_core/ctxp/_init.py +8 -3
- hcs_core/ctxp/built_in_cmds/context.py +15 -1
- hcs_core/ctxp/built_in_cmds/profile.py +20 -12
- hcs_core/ctxp/cli_options.py +28 -8
- hcs_core/ctxp/cli_processor.py +23 -12
- hcs_core/ctxp/context.py +2 -0
- hcs_core/ctxp/data_util.py +35 -15
- hcs_core/ctxp/duration.py +1 -3
- hcs_core/ctxp/fstore.py +1 -1
- hcs_core/ctxp/jsondot.py +1 -1
- hcs_core/ctxp/logger.py +4 -4
- hcs_core/ctxp/profile.py +6 -7
- hcs_core/ctxp/recent.py +3 -3
- hcs_core/ctxp/state.py +2 -2
- hcs_core/ctxp/task_schd.py +0 -2
- hcs_core/ctxp/telemetry.py +145 -0
- hcs_core/ctxp/util.py +158 -25
- hcs_core/plan/__init__.py +1 -0
- hcs_core/plan/core.py +20 -17
- hcs_core/plan/dag.py +7 -8
- hcs_core/plan/kop.py +19 -7
- hcs_core/sglib/auth.py +111 -98
- hcs_core/sglib/cli_options.py +15 -1
- hcs_core/sglib/client_util.py +173 -75
- hcs_core/sglib/csp.py +71 -5
- hcs_core/sglib/ez_client.py +48 -32
- hcs_core/sglib/hcs_client.py +2 -6
- hcs_core/sglib/login_support.py +17 -9
- hcs_core/sglib/utils.py +4 -1
- hcs_core/util/check_license.py +0 -2
- hcs_core/util/job_view.py +28 -10
- hcs_core/util/query_util.py +17 -8
- hcs_core/util/versions.py +12 -10
- {hcs_core-0.1.283.dist-info → hcs_core-0.1.316.dist-info}/METADATA +19 -17
- hcs_core-0.1.316.dist-info/RECORD +69 -0
- {hcs_core-0.1.283.dist-info → hcs_core-0.1.316.dist-info}/WHEEL +1 -1
- hcs_core-0.1.283.dist-info/RECORD +0 -68
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,19 +106,18 @@ 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",
|
|
95
113
|
}
|
|
96
114
|
# <no org id for this API>
|
|
97
|
-
resp = self._client.post(
|
|
98
|
-
"/csp/gateway/am/api/auth/api-tokens/authorize", headers=headers, data=f"api_token={api_token}"
|
|
99
|
-
)
|
|
115
|
+
resp = self._client.post("/csp/gateway/am/api/auth/api-tokens/authorize", headers=headers, data=f"api_token={api_token}")
|
|
100
116
|
self._oauth_token = resp.json()
|
|
101
117
|
return self._oauth_token
|
|
102
118
|
|
|
103
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}")
|
|
104
121
|
headers = {
|
|
105
122
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
106
123
|
"Accept": "application/json",
|
|
@@ -110,8 +127,9 @@ class CspClient:
|
|
|
110
127
|
}
|
|
111
128
|
if org_id:
|
|
112
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"
|
|
113
131
|
resp = self._client.post(
|
|
114
|
-
|
|
132
|
+
url,
|
|
115
133
|
auth=(client_id, client_secret),
|
|
116
134
|
headers=headers,
|
|
117
135
|
params=params,
|
|
@@ -119,6 +137,54 @@ class CspClient:
|
|
|
119
137
|
self._oauth_token = resp.json()
|
|
120
138
|
return self._oauth_token
|
|
121
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
|
+
|
|
122
188
|
# def get_oauth_token(self, force=False):
|
|
123
189
|
# if self._oauth_token and not force:
|
|
124
190
|
# return self._oauth_token
|
hcs_core/sglib/ez_client.py
CHANGED
|
@@ -19,7 +19,7 @@ import os
|
|
|
19
19
|
import sys
|
|
20
20
|
import threading
|
|
21
21
|
from http.client import HTTPResponse
|
|
22
|
-
from typing import Callable, Optional, Type
|
|
22
|
+
from typing import Callable, Optional, Type
|
|
23
23
|
|
|
24
24
|
import httpx
|
|
25
25
|
from authlib.integrations.httpx_client import OAuth2Client
|
|
@@ -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
|
-
|
|
141
|
-
self._client_impl =
|
|
142
|
-
self.
|
|
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
|
|
149
|
-
client = self.
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
@@ -173,11 +188,12 @@ class EzClient:
|
|
|
173
188
|
files=None,
|
|
174
189
|
headers: dict = None,
|
|
175
190
|
type: Optional[Type[BaseModel]] = None,
|
|
191
|
+
timeout: float = 30.0,
|
|
176
192
|
):
|
|
177
193
|
# import json as jsonlib
|
|
178
|
-
# print(url, jsonlib.dumps(json, indent=4))
|
|
194
|
+
# print("->", self._client().base_url, url, jsonlib.dumps(json, indent=4))
|
|
179
195
|
try:
|
|
180
|
-
resp = self._client().post(url, json=json, content=text, files=files, headers=headers)
|
|
196
|
+
resp = self._client().post(url, json=json, content=text, files=files, headers=headers, timeout=timeout)
|
|
181
197
|
except httpx.HTTPStatusError as e:
|
|
182
198
|
_raise_http_error(e)
|
|
183
199
|
data = _parse_resp(resp)
|
|
@@ -190,10 +206,10 @@ class EzClient:
|
|
|
190
206
|
raise
|
|
191
207
|
return data
|
|
192
208
|
|
|
193
|
-
def get(self, url: str, raise_on_404: bool = False, type: Optional[Type[BaseModel]] = None):
|
|
209
|
+
def get(self, url: str, headers: dict = None, raise_on_404: bool = False, type: Optional[Type[BaseModel]] = None):
|
|
194
210
|
try:
|
|
195
|
-
# print("->", url)
|
|
196
|
-
resp = self._client().get(url)
|
|
211
|
+
# print("->", self._client().base_url, url)
|
|
212
|
+
resp = self._client().get(url, headers=headers)
|
|
197
213
|
data = _parse_resp(resp)
|
|
198
214
|
if data and type:
|
|
199
215
|
try:
|
|
@@ -212,17 +228,17 @@ class EzClient:
|
|
|
212
228
|
else:
|
|
213
229
|
_raise_http_error(e)
|
|
214
230
|
|
|
215
|
-
def patch(self, url: str, json: dict):
|
|
231
|
+
def patch(self, url: str, json: dict = None, text=None, headers: dict = None):
|
|
216
232
|
try:
|
|
217
|
-
resp = self._client().patch(url, json=json)
|
|
233
|
+
resp = self._client().patch(url, json=json, content=text, headers=headers)
|
|
218
234
|
return _parse_resp(resp)
|
|
219
235
|
except httpx.HTTPStatusError as e:
|
|
220
236
|
_raise_http_error(e)
|
|
221
237
|
raise
|
|
222
238
|
|
|
223
|
-
def delete(self, url: str, raise_on_404: bool = False):
|
|
239
|
+
def delete(self, url: str, headers: dict = None, raise_on_404: bool = False):
|
|
224
240
|
try:
|
|
225
|
-
resp = self._client().delete(url)
|
|
241
|
+
resp = self._client().delete(url, headers=headers)
|
|
226
242
|
return _parse_resp(resp)
|
|
227
243
|
except httpx.HTTPStatusError as e:
|
|
228
244
|
if _is_404(e):
|
|
@@ -233,9 +249,9 @@ class EzClient:
|
|
|
233
249
|
else:
|
|
234
250
|
_raise_http_error(e)
|
|
235
251
|
|
|
236
|
-
def put(self, url: str, json: dict):
|
|
252
|
+
def put(self, url: str, json: dict = None, text=None, headers: dict = None):
|
|
237
253
|
try:
|
|
238
|
-
resp = self._client().put(url, json=json)
|
|
254
|
+
resp = self._client().put(url, json=json, content=text, headers=headers)
|
|
239
255
|
return _parse_resp(resp)
|
|
240
256
|
except httpx.HTTPStatusError as e:
|
|
241
257
|
_raise_http_error(e)
|
hcs_core/sglib/hcs_client.py
CHANGED
|
@@ -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:
|
|
23
|
-
|
|
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/sglib/login_support.py
CHANGED
|
@@ -17,19 +17,16 @@ limitations under the License.
|
|
|
17
17
|
# PKCE with authlib: https://docs.authlib.org/en/latest/specs/rfc7636.html
|
|
18
18
|
|
|
19
19
|
import logging
|
|
20
|
+
import platform
|
|
20
21
|
import secrets
|
|
21
22
|
import threading
|
|
22
|
-
import time
|
|
23
23
|
import webbrowser
|
|
24
24
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
25
|
-
from typing import Callable
|
|
26
25
|
from urllib.parse import parse_qs, urlparse
|
|
27
26
|
|
|
28
27
|
from authlib.integrations.httpx_client import OAuth2Client
|
|
29
28
|
from authlib.oauth2.rfc7636 import create_s256_code_challenge
|
|
30
29
|
|
|
31
|
-
from hcs_core.util.scheduler import ThreadPoolScheduler
|
|
32
|
-
|
|
33
30
|
log = logging.getLogger(__name__)
|
|
34
31
|
|
|
35
32
|
_server_address = ("127.0.0.1", 10762)
|
|
@@ -37,10 +34,12 @@ _callback_url = "/hcs-cli/oauth/callback"
|
|
|
37
34
|
|
|
38
35
|
_public_client_ids = {
|
|
39
36
|
"production": "ldjmWbBAUcSB1w3HSbzoYcdKoloYFqT2dWK",
|
|
37
|
+
# "production": "WbvEYXK4abljPJUuY2zODLpOaX5ddH2uhMX", #collie mobile
|
|
40
38
|
"staging": "BbwHlgWt0vFwPUZMJTb5IKltynXjDmb46IO",
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
_auth_code_event = threading.Event()
|
|
42
|
+
_auth_code_event.value = None
|
|
44
43
|
_state = None
|
|
45
44
|
|
|
46
45
|
_auth_success_html = """
|
|
@@ -64,10 +63,10 @@ _auth_success_html = """
|
|
|
64
63
|
</style>
|
|
65
64
|
</head>
|
|
66
65
|
<body>
|
|
67
|
-
<h3>You have successfully logged into
|
|
66
|
+
<h3>You have successfully logged into Omnissa Horizon Cloud Service.</h3>
|
|
68
67
|
<p>You can close this window, and return to the terminal.</p>
|
|
69
68
|
<br/>
|
|
70
|
-
<p><a href="https://github.com/euc-eng/hcs-cli">HCS CLI</a> is in
|
|
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>
|
|
71
70
|
<code>
|
|
72
71
|
# To get the login details: <br/>
|
|
73
72
|
hcs login -d <br/><br/>
|
|
@@ -169,9 +168,18 @@ def do_oauth2_pkce(csp_url: str, client_id: str, org_id: str):
|
|
|
169
168
|
webbrowser.open(authorization_url, new=0, autoraise=True)
|
|
170
169
|
|
|
171
170
|
try:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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}")
|
|
175
183
|
return
|
|
176
184
|
# Once the user is redirected back to your app with an authorization code, exchange it for an access token
|
|
177
185
|
authorization_code = _auth_code_event.value
|
hcs_core/sglib/utils.py
CHANGED
|
@@ -65,7 +65,10 @@ def run_govc_guest_script(script, args):
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
def waitForVmPowerOff(iPath):
|
|
68
|
-
|
|
68
|
+
|
|
69
|
+
def current_millis():
|
|
70
|
+
return int(round(time.time() * 1000))
|
|
71
|
+
|
|
69
72
|
start_time = current_millis()
|
|
70
73
|
while current_millis() - start_time < 5 * 60 * 1000:
|
|
71
74
|
p = subprocess.run(
|
hcs_core/util/check_license.py
CHANGED
hcs_core/util/job_view.py
CHANGED
|
@@ -19,7 +19,7 @@ import threading
|
|
|
19
19
|
from time import monotonic, sleep
|
|
20
20
|
from typing import Optional
|
|
21
21
|
|
|
22
|
-
from rich.console import RenderableType
|
|
22
|
+
from rich.console import Console, RenderableType
|
|
23
23
|
from rich.live import Live
|
|
24
24
|
from rich.progress import Progress, ProgressBar, ProgressColumn, SpinnerColumn, Task, TextColumn, TimeRemainingColumn
|
|
25
25
|
from rich.table import Column
|
|
@@ -27,6 +27,9 @@ from rich.text import Text
|
|
|
27
27
|
|
|
28
28
|
import hcs_core.util.duration as duration
|
|
29
29
|
|
|
30
|
+
# Detect if we're running on Windows
|
|
31
|
+
_IS_WINDOWS = os.name == "nt"
|
|
32
|
+
|
|
30
33
|
|
|
31
34
|
def _shorten_package_name(package_name, max_length):
|
|
32
35
|
if len(package_name) <= max_length:
|
|
@@ -117,6 +120,13 @@ class _MyTimeRemainingColumn(ProgressColumn):
|
|
|
117
120
|
|
|
118
121
|
|
|
119
122
|
class _MySpinnerColumn(SpinnerColumn):
|
|
123
|
+
def __init__(self, *args, **kwargs):
|
|
124
|
+
# Use ASCII spinner for Windows terminals
|
|
125
|
+
if _IS_WINDOWS and "spinner_name" not in kwargs:
|
|
126
|
+
# Use a simple ASCII spinner: | / - \
|
|
127
|
+
kwargs["spinner_name"] = "line"
|
|
128
|
+
super().__init__(*args, **kwargs)
|
|
129
|
+
|
|
120
130
|
def render(self, task: "Task") -> RenderableType:
|
|
121
131
|
if task.finished:
|
|
122
132
|
text = self.finished_text
|
|
@@ -134,6 +144,9 @@ def _timeout_to_seconds(timeout: str, default: int = 60) -> int:
|
|
|
134
144
|
return t
|
|
135
145
|
|
|
136
146
|
|
|
147
|
+
_NAME_WIDTH = 40
|
|
148
|
+
|
|
149
|
+
|
|
137
150
|
class JobView:
|
|
138
151
|
def __init__(self, auto_close: bool = True):
|
|
139
152
|
self._map = {}
|
|
@@ -144,24 +157,28 @@ class JobView:
|
|
|
144
157
|
self._view_exited = threading.Event()
|
|
145
158
|
|
|
146
159
|
w, h = os.get_terminal_size()
|
|
147
|
-
msg_width = w -
|
|
160
|
+
msg_width = w - _NAME_WIDTH - 1 - 1 - 1 - 10 - 1 - 5 - 1 - 1
|
|
161
|
+
|
|
162
|
+
# Create a console with ASCII-only mode for Windows to avoid rendering issues
|
|
163
|
+
# We need to store it and pass it to Live as well
|
|
164
|
+
self._console = Console(legacy_windows=True) if _IS_WINDOWS else None
|
|
165
|
+
|
|
148
166
|
self._job_ctl = Progress(
|
|
149
|
-
TextColumn("{task.description}", table_column=Column(max_width=
|
|
167
|
+
TextColumn("{task.description}", table_column=Column(max_width=_NAME_WIDTH, min_width=10)),
|
|
150
168
|
# SpinnerColumn(table_column=Column(min_width=1)),
|
|
151
169
|
_MySpinnerColumn(table_column=Column(min_width=1)),
|
|
152
170
|
# BarColumn(pulse_style='white'),
|
|
153
171
|
_MyPlainBarColumn(),
|
|
154
172
|
# TextColumn("[white][progress.percentage][white]{task.percentage:>3.0f}%"),
|
|
155
173
|
# TaskProgressColumn(show_speed=True),
|
|
156
|
-
TimeRemainingColumn(
|
|
157
|
-
compact=True, elapsed_when_finished=True, table_column=Column(style="white", min_width=5)
|
|
158
|
-
),
|
|
174
|
+
TimeRemainingColumn(compact=True, elapsed_when_finished=True, table_column=Column(style="white", min_width=5)),
|
|
159
175
|
TextColumn("{task.fields[details]}", table_column=Column(max_width=msg_width, no_wrap=True)),
|
|
176
|
+
console=self._console,
|
|
160
177
|
get_time=monotonic,
|
|
161
178
|
)
|
|
162
179
|
|
|
163
180
|
def add(self, id: str, name: str):
|
|
164
|
-
name = _shorten_package_name(name,
|
|
181
|
+
name = _shorten_package_name(name, _NAME_WIDTH)
|
|
165
182
|
self._map[id] = self._job_ctl.add_task(name, start=False, details="")
|
|
166
183
|
self._todo.add(id)
|
|
167
184
|
|
|
@@ -206,9 +223,10 @@ class JobView:
|
|
|
206
223
|
def skip(self, id: str, reason: str) -> None:
|
|
207
224
|
self._ensure_started(id)
|
|
208
225
|
task_id = self._map[id]
|
|
209
|
-
details = "skipped"
|
|
226
|
+
details = "<skipped>"
|
|
210
227
|
if reason:
|
|
211
|
-
details
|
|
228
|
+
details = reason + " " + details
|
|
229
|
+
|
|
212
230
|
self._job_ctl.update(task_id, completed=sys.float_info.max, details=details)
|
|
213
231
|
self._todo.discard(id)
|
|
214
232
|
|
|
@@ -230,7 +248,7 @@ class JobView:
|
|
|
230
248
|
|
|
231
249
|
def show(self) -> None:
|
|
232
250
|
try:
|
|
233
|
-
with Live(self._job_ctl, refresh_per_second=10):
|
|
251
|
+
with Live(self._job_ctl, refresh_per_second=10, console=self._console):
|
|
234
252
|
self._view_started.set()
|
|
235
253
|
while True:
|
|
236
254
|
self.refresh()
|
hcs_core/util/query_util.py
CHANGED
|
@@ -40,7 +40,11 @@ def with_query(url: str, **kwargs: Any) -> str:
|
|
|
40
40
|
class PageRequest:
|
|
41
41
|
def __init__(self, fn_get_page: Callable, fn_filter: Callable = None, **kwargs):
|
|
42
42
|
limit = kwargs.get("limit")
|
|
43
|
-
|
|
43
|
+
if limit is None or limit == "":
|
|
44
|
+
limit = 10
|
|
45
|
+
else:
|
|
46
|
+
limit = int(limit)
|
|
47
|
+
self.limit = limit
|
|
44
48
|
self.fn_get_page = fn_get_page
|
|
45
49
|
self.fn_filter = fn_filter
|
|
46
50
|
self.query = _remove_none(dict(kwargs))
|
|
@@ -57,12 +61,14 @@ class PageRequest:
|
|
|
57
61
|
params["size"] = size
|
|
58
62
|
|
|
59
63
|
query_string = urlencode(params)
|
|
60
|
-
|
|
61
|
-
if not
|
|
64
|
+
page_data = self.fn_get_page(query_string)
|
|
65
|
+
if not page_data or not page_data.content:
|
|
62
66
|
return [], False
|
|
67
|
+
content = page_data.content
|
|
63
68
|
if self.fn_filter:
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
content = list(filter(self.fn_filter, content))
|
|
70
|
+
has_next = page_data.get("totalPages", 0) > page + 1
|
|
71
|
+
return content, has_next
|
|
66
72
|
|
|
67
73
|
def get(self) -> list:
|
|
68
74
|
ret = []
|
|
@@ -83,12 +89,15 @@ class PageRequest:
|
|
|
83
89
|
else:
|
|
84
90
|
content = page.content
|
|
85
91
|
ret += content
|
|
86
|
-
if len(ret) > self.limit:
|
|
92
|
+
if self.limit > 0 and len(ret) > self.limit:
|
|
87
93
|
ret = ret[: self.limit]
|
|
88
94
|
break
|
|
89
|
-
if len(page.content) < self.query["size"]:
|
|
90
|
-
|
|
95
|
+
# if len(page.content) < self.query["size"]:
|
|
96
|
+
# break # no more items
|
|
97
|
+
total_pages = page.get("totalPages", 0)
|
|
91
98
|
page_index += 1
|
|
99
|
+
if page_index >= total_pages:
|
|
100
|
+
break
|
|
92
101
|
|
|
93
102
|
return ret
|
|
94
103
|
|
hcs_core/util/versions.py
CHANGED
|
@@ -15,9 +15,9 @@ limitations under the License.
|
|
|
15
15
|
|
|
16
16
|
import logging
|
|
17
17
|
import time
|
|
18
|
+
from importlib.metadata import version
|
|
18
19
|
|
|
19
20
|
import httpx
|
|
20
|
-
import pkg_resources
|
|
21
21
|
from packaging.version import Version
|
|
22
22
|
|
|
23
23
|
import hcs_core
|
|
@@ -30,20 +30,22 @@ def check_upgrade():
|
|
|
30
30
|
checked_at = hcs_core.ctxp.state.get(last_upgrade_check_at, 0)
|
|
31
31
|
|
|
32
32
|
now = time.time()
|
|
33
|
-
if now - checked_at
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
if now - checked_at < 24 * 60 * 60:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
latest = get_latest_version()
|
|
38
|
+
current = Version(get_version())
|
|
39
|
+
if current < latest:
|
|
40
|
+
log.warning(f"New version available: {latest}. Execute 'hcs upgrade' to upgrade.")
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logging.debug(e)
|
|
41
43
|
|
|
42
44
|
hcs_core.ctxp.state.set(last_upgrade_check_at, now)
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
def get_version():
|
|
46
|
-
return
|
|
48
|
+
return version("hcs-cli")
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
def get_latest_version() -> Version:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hcs-core
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.316
|
|
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
|
|
@@ -9,37 +9,39 @@ Project-URL: repository, https://github.com/euc-eng/hcs-cli
|
|
|
9
9
|
Project-URL: changelog, https://github.com/euc-eng/hcs-cli/blob/main/CHANGELOG.md
|
|
10
10
|
Author-email: Nanw1103 <nanw1103@gmail.com>
|
|
11
11
|
Keywords: CLI,Horizon,Horizon Cloud,Horizon Cloud Service
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Requires-Python: >=3.9
|
|
17
|
-
Requires-Dist: authlib>=1.
|
|
18
|
-
Requires-Dist: click>=8.1.
|
|
17
|
+
Requires-Dist: authlib>=1.6.0
|
|
18
|
+
Requires-Dist: click>=8.1.8
|
|
19
19
|
Requires-Dist: coloredlogs>=15.0.1
|
|
20
|
-
Requires-Dist: cryptography>=
|
|
21
|
-
Requires-Dist: graphviz>=0.
|
|
22
|
-
Requires-Dist: httpx>=0.
|
|
23
|
-
Requires-Dist: packaging>=
|
|
24
|
-
Requires-Dist: portalocker>=
|
|
25
|
-
Requires-Dist: psutil>=
|
|
26
|
-
Requires-Dist: pydantic>=2.
|
|
27
|
-
Requires-Dist: pyjwt>=2.
|
|
28
|
-
Requires-Dist: pyopenssl>=
|
|
20
|
+
Requires-Dist: cryptography>=45.0.5
|
|
21
|
+
Requires-Dist: graphviz>=0.21
|
|
22
|
+
Requires-Dist: httpx>=0.28.0
|
|
23
|
+
Requires-Dist: packaging>=25.0
|
|
24
|
+
Requires-Dist: portalocker>=3.0.0
|
|
25
|
+
Requires-Dist: psutil>=7.0.0
|
|
26
|
+
Requires-Dist: pydantic>=2.11.7
|
|
27
|
+
Requires-Dist: pyjwt>=2.10.1
|
|
28
|
+
Requires-Dist: pyopenssl>=25.1.0
|
|
29
|
+
Requires-Dist: python-dotenv>=1.1.1
|
|
29
30
|
Requires-Dist: pyyaml>=6.0.1
|
|
30
31
|
Requires-Dist: questionary>=2.0.1
|
|
31
|
-
Requires-Dist: rel>=0.4.
|
|
32
|
+
Requires-Dist: rel>=0.4.9.20
|
|
32
33
|
Requires-Dist: retry>=0.9.2
|
|
33
|
-
Requires-Dist: rich>=
|
|
34
|
+
Requires-Dist: rich>=14.0.0
|
|
34
35
|
Requires-Dist: schedule>=1.1.0
|
|
35
|
-
Requires-Dist: setuptools>=70.0.0
|
|
36
36
|
Requires-Dist: tabulate>=0.9.0
|
|
37
37
|
Requires-Dist: websocket-client>=1.2.3
|
|
38
|
-
Requires-Dist: yumako>=0.1.
|
|
38
|
+
Requires-Dist: yumako>=0.1.36
|
|
39
39
|
Provides-Extra: dev
|
|
40
40
|
Requires-Dist: bandit; extra == 'dev'
|
|
41
41
|
Requires-Dist: black; extra == 'dev'
|
|
42
42
|
Requires-Dist: build; extra == 'dev'
|
|
43
|
+
Requires-Dist: flake8; extra == 'dev'
|
|
44
|
+
Requires-Dist: hatch; extra == 'dev'
|
|
43
45
|
Requires-Dist: mypy; extra == 'dev'
|
|
44
46
|
Requires-Dist: pylint; extra == 'dev'
|
|
45
47
|
Requires-Dist: pytest; extra == 'dev'
|