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/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
- "/csp/gateway/am/api/auth/authorize",
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
@@ -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, Union
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
- 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
@@ -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)
@@ -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))
@@ -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 VMware Horizon Cloud Service.</h3>
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 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>
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
- _auth_code_event.wait()
173
- except:
174
- 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}")
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
- current_millis = lambda: int(round(time.time() * 1000))
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(
@@ -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 python -W ignore
17
-
18
16
  import os
19
17
  import sys
20
18
 
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 - 40 - 1 - 1 - 1 - 10 - 1 - 5 - 1 - 1
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=40, min_width=10)),
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, 30)
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 += f": {reason}"
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()
@@ -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
- self.limit = int(limit) if limit else 10
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
- page = self.fn_get_page(query_string)
61
- if not page or not page.content:
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
- return list(filter(self.fn_filter, page.content)), True
65
- return page.content, True
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
- break # no more items
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 > 24 * 60 * 60:
34
- try:
35
- latest = get_latest_version()
36
- current = Version(get_version())
37
- if current < latest:
38
- log.warning(f"New version available: {latest}. Execute 'hcs upgrade' to upgrade.")
39
- except Exception as e:
40
- logging.debug(e)
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 pkg_resources.require("hcs-cli")[0].version
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.283
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 :: 4 - Beta
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.2.1
18
- Requires-Dist: click>=8.1.7
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>=42.0.7
21
- Requires-Dist: graphviz>=0.20.3
22
- Requires-Dist: httpx>=0.27.0
23
- Requires-Dist: packaging>=24.0
24
- Requires-Dist: portalocker>=2.8.2
25
- Requires-Dist: psutil>=5.9.4
26
- Requires-Dist: pydantic>=2.0.0
27
- Requires-Dist: pyjwt>=2.8.0
28
- Requires-Dist: pyopenssl>=24.1.0
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.7
32
+ Requires-Dist: rel>=0.4.9.20
32
33
  Requires-Dist: retry>=0.9.2
33
- Requires-Dist: rich>=13.7.1
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.21
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'