htx-cli 0.1.0__tar.gz → 0.1.2__tar.gz

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.
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htx-cli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: htx-cli command-line client for the Disk backend API
5
- Requires-Python: >=3.11
5
+ Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: rich<14,>=13.9
8
8
  Requires-Dist: requests<3,>=2.32
@@ -19,7 +19,12 @@ Requires-Dist: requests<3,>=2.32
19
19
  ## Install
20
20
 
21
21
  ```bash
22
- source /Users/jyonn/Projects/venv/django/bin/activate
22
+ pip install htx-cli
23
+ ```
24
+
25
+ For local development:
26
+
27
+ ```bash
23
28
  pip install -e .
24
29
  ```
25
30
 
@@ -10,7 +10,12 @@
10
10
  ## Install
11
11
 
12
12
  ```bash
13
- source /Users/jyonn/Projects/venv/django/bin/activate
13
+ pip install htx-cli
14
+ ```
15
+
16
+ For local development:
17
+
18
+ ```bash
14
19
  pip install -e .
15
20
  ```
16
21
 
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "htx-cli"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "htx-cli command-line client for the Disk backend API"
9
9
  readme = "README.md"
10
- requires-python = ">=3.11"
10
+ requires-python = ">=3.9"
11
11
  dependencies = [
12
12
  "rich>=13.9,<14",
13
13
  "requests>=2.32,<3",
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from pathlib import Path
5
5
  import time
6
- from typing import Any, Callable
6
+ from typing import Any, Callable, Dict, Optional
7
7
  from urllib.parse import urljoin
8
8
 
9
9
  import requests
@@ -16,9 +16,9 @@ class DiskAPIError(RuntimeError):
16
16
  self,
17
17
  message: str,
18
18
  *,
19
- status_code: int | None = None,
20
- identifier: str | None = None,
21
- debug_msg: str | None = None,
19
+ status_code: Optional[int] = None,
20
+ identifier: Optional[str] = None,
21
+ debug_msg: Optional[str] = None,
22
22
  ) -> None:
23
23
  super().__init__(message)
24
24
  self.status_code = status_code
@@ -32,13 +32,13 @@ class UploadToken:
32
32
  upload_token: str
33
33
 
34
34
 
35
- ProgressCallback = Callable[[int, int | None], None]
35
+ ProgressCallback = Callable[[int, Optional[int]], None]
36
36
  MAX_RETRIES = 5
37
37
  RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
38
38
 
39
39
 
40
40
  class UploadMonitor:
41
- def __init__(self, local_path: Path, callback: ProgressCallback | None = None) -> None:
41
+ def __init__(self, local_path: Path, callback: Optional[ProgressCallback] = None) -> None:
42
42
  self.local_path = local_path
43
43
  self.callback = callback
44
44
  self.handle = local_path.open("rb")
@@ -74,8 +74,8 @@ class DiskClient:
74
74
  self.timeout = timeout
75
75
  self.session = requests.Session()
76
76
 
77
- def _headers(self, *, include_auth: bool = True) -> dict[str, str]:
78
- headers: dict[str, str] = {}
77
+ def _headers(self, *, include_auth: bool = True) -> Dict[str, str]:
78
+ headers: Dict[str, str] = {}
79
79
  if include_auth and self.settings.token:
80
80
  headers["Token"] = self.settings.token
81
81
  return headers
@@ -114,10 +114,10 @@ class DiskClient:
114
114
  method: str,
115
115
  url: str,
116
116
  *,
117
- params: dict[str, Any] | None = None,
118
- json: dict[str, Any] | None = None,
119
- data: dict[str, Any] | None = None,
120
- files: dict[str, Any] | None = None,
117
+ params: Optional[Dict[str, Any]] = None,
118
+ json: Optional[Dict[str, Any]] = None,
119
+ data: Optional[Dict[str, Any]] = None,
120
+ files: Optional[Dict[str, Any]] = None,
121
121
  include_auth: bool = True,
122
122
  ) -> Any:
123
123
  response = self.session.request(
@@ -137,15 +137,15 @@ class DiskClient:
137
137
  method: str,
138
138
  path: str,
139
139
  *,
140
- params: dict[str, Any] | None = None,
141
- json: dict[str, Any] | None = None,
142
- absolute_url: str | None = None,
140
+ params: Optional[Dict[str, Any]] = None,
141
+ json: Optional[Dict[str, Any]] = None,
142
+ absolute_url: Optional[str] = None,
143
143
  include_auth: bool = True,
144
- data: dict[str, Any] | None = None,
145
- files: dict[str, Any] | None = None,
144
+ data: Optional[Dict[str, Any]] = None,
145
+ files: Optional[Dict[str, Any]] = None,
146
146
  ) -> Any:
147
147
  url = absolute_url or urljoin(f"{self.settings.backend_url}/", path.lstrip("/"))
148
- last_error: Exception | None = None
148
+ last_error: Optional[Exception] = None
149
149
  for attempt in range(1, MAX_RETRIES + 1):
150
150
  try:
151
151
  return self._request_once(
@@ -173,7 +173,7 @@ class DiskClient:
173
173
  raise last_error
174
174
  raise DiskAPIError(f"Request failed after {MAX_RETRIES} attempts.")
175
175
 
176
- def exchange_code(self, code: str) -> dict[str, Any]:
176
+ def exchange_code(self, code: str) -> Dict[str, Any]:
177
177
  body = self.request(
178
178
  "GET",
179
179
  "/api/oauth/qtb/callback",
@@ -185,26 +185,26 @@ class DiskClient:
185
185
  self.settings.token = body["token"]
186
186
  return body
187
187
 
188
- def get_current_user(self) -> dict[str, Any]:
188
+ def get_current_user(self) -> Dict[str, Any]:
189
189
  body = self.request("GET", "/api/user/")
190
190
  if not isinstance(body, dict):
191
191
  raise DiskAPIError("User endpoint returned an unexpected payload.")
192
192
  return body
193
193
 
194
- def get_resource(self, res_str_id: str, visit_key: str | None = None) -> dict[str, Any]:
194
+ def get_resource(self, res_str_id: str, visit_key: Optional[str] = None) -> Dict[str, Any]:
195
195
  params = {"visit_key": visit_key} if visit_key else None
196
196
  body = self.request("GET", f"/api/res/{res_str_id}", params=params)
197
197
  if not isinstance(body, dict):
198
198
  raise DiskAPIError("Resource endpoint returned an unexpected payload.")
199
199
  return body
200
200
 
201
- def get_selector(self, res_str_id: str) -> dict[str, Any]:
201
+ def get_selector(self, res_str_id: str) -> Dict[str, Any]:
202
202
  body = self.request("GET", f"/api/res/{res_str_id}/selector")
203
203
  if not isinstance(body, dict):
204
204
  raise DiskAPIError("Selector endpoint returned an unexpected payload.")
205
205
  return body
206
206
 
207
- def create_folder(self, parent_id: str, folder_name: str) -> dict[str, Any]:
207
+ def create_folder(self, parent_id: str, folder_name: str) -> Dict[str, Any]:
208
208
  body = self.request(
209
209
  "POST",
210
210
  f"/api/res/{parent_id}/folder",
@@ -214,7 +214,7 @@ class DiskClient:
214
214
  raise DiskAPIError("Create-folder endpoint returned an unexpected payload.")
215
215
  return body
216
216
 
217
- def create_link(self, parent_id: str, link_name: str, link: str) -> dict[str, Any]:
217
+ def create_link(self, parent_id: str, link_name: str, link: str) -> Dict[str, Any]:
218
218
  body = self.request(
219
219
  "POST",
220
220
  f"/api/res/{parent_id}/link",
@@ -228,13 +228,13 @@ class DiskClient:
228
228
  self,
229
229
  res_str_id: str,
230
230
  *,
231
- rname: str | None = None,
232
- status: int | None = None,
233
- description: str | None = None,
234
- visit_key: str | None = None,
235
- right_bubble: bool | None = None,
236
- parent_str_id: str | None = None,
237
- ) -> dict[str, Any]:
231
+ rname: Optional[str] = None,
232
+ status: Optional[int] = None,
233
+ description: Optional[str] = None,
234
+ visit_key: Optional[str] = None,
235
+ right_bubble: Optional[bool] = None,
236
+ parent_str_id: Optional[str] = None,
237
+ ) -> Dict[str, Any]:
238
238
  body = self.request(
239
239
  "PUT",
240
240
  f"/api/res/{res_str_id}",
@@ -259,12 +259,12 @@ class DiskClient:
259
259
  res_str_id: str,
260
260
  destination: Path,
261
261
  *,
262
- progress_callback: ProgressCallback | None = None,
262
+ progress_callback: Optional[ProgressCallback] = None,
263
263
  ) -> str:
264
264
  url = urljoin(f"{self.settings.backend_url}/", f"/api/res/{res_str_id}/dl".lstrip("/"))
265
- last_error: Exception | None = None
265
+ last_error: Optional[Exception] = None
266
266
  for attempt in range(1, MAX_RETRIES + 1):
267
- response: requests.Response | None = None
267
+ response: Optional[requests.Response] = None
268
268
  try:
269
269
  response = self.session.get(
270
270
  url,
@@ -335,9 +335,9 @@ class DiskClient:
335
335
  upload: UploadToken,
336
336
  local_path: Path,
337
337
  *,
338
- progress_callback: ProgressCallback | None = None,
339
- ) -> dict[str, Any]:
340
- last_error: Exception | None = None
338
+ progress_callback: Optional[ProgressCallback] = None,
339
+ ) -> Dict[str, Any]:
340
+ last_error: Optional[Exception] = None
341
341
  for attempt in range(1, MAX_RETRIES + 1):
342
342
  try:
343
343
  with UploadMonitor(local_path, progress_callback) as handle:
@@ -8,7 +8,7 @@ import sys
8
8
  import webbrowser
9
9
  from dataclasses import dataclass
10
10
  from pathlib import Path
11
- from typing import Any
11
+ from typing import Any, Dict, List, Optional, Tuple
12
12
  from urllib.parse import parse_qs, urlparse
13
13
 
14
14
  from rich import box
@@ -76,7 +76,7 @@ class CommandContext:
76
76
  if not self.settings.token:
77
77
  raise CLIUsageError("Please run `htx login` first.")
78
78
 
79
- def sync_user(self) -> dict[str, Any]:
79
+ def sync_user(self) -> Dict[str, Any]:
80
80
  self.require_login()
81
81
  user = self.client.get_current_user()
82
82
  self.settings.user = user
@@ -256,7 +256,7 @@ def status_text(status: int) -> Text:
256
256
  )
257
257
 
258
258
 
259
- def resource_name_text(resource: dict[str, Any]) -> Text:
259
+ def resource_name_text(resource: Dict[str, Any]) -> Text:
260
260
  text = Text()
261
261
  text.append(format_resource_ref(resource["res_str_id"]), style="bold yellow")
262
262
  text.append(" ")
@@ -264,13 +264,13 @@ def resource_name_text(resource: dict[str, Any]) -> Text:
264
264
  return text
265
265
 
266
266
 
267
- def display_size(resource: dict[str, Any]) -> str:
267
+ def display_size(resource: Dict[str, Any]) -> str:
268
268
  if resource.get("rtype") != RTYPE_FILE:
269
269
  return "-"
270
270
  return format_size(resource.get("rsize"))
271
271
 
272
272
 
273
- def progress_columns() -> list[Any]:
273
+ def progress_columns() -> List[Any]:
274
274
  return [
275
275
  SpinnerColumn(style="bold cyan"),
276
276
  TextColumn("[progress.description]{task.description}"),
@@ -294,7 +294,7 @@ def print_warning(message: str) -> None:
294
294
  error_console.print(f"[bold yellow]WARN[/bold yellow] {message}")
295
295
 
296
296
 
297
- def sort_children(children: list[dict[str, Any]]) -> list[dict[str, Any]]:
297
+ def sort_children(children: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
298
298
  return sorted(
299
299
  children,
300
300
  key=lambda item: (
@@ -304,13 +304,13 @@ def sort_children(children: list[dict[str, Any]]) -> list[dict[str, Any]]:
304
304
  )
305
305
 
306
306
 
307
- def ensure_folder_resource(resource: dict[str, Any], *, label: str) -> None:
307
+ def ensure_folder_resource(resource: Dict[str, Any], *, label: str) -> None:
308
308
  info = resource.get("info", resource)
309
309
  if info.get("rtype") != RTYPE_FOLDER:
310
310
  raise CLIUsageError(f"{label} must refer to a remote directory.")
311
311
 
312
312
 
313
- def parse_resource_reference(reference: str) -> tuple[str | None, str]:
313
+ def parse_resource_reference(reference: str) -> Tuple[Optional[str], str]:
314
314
  if reference.startswith("@"):
315
315
  return reference[1:], "resource id"
316
316
  if reference.startswith("id:"):
@@ -318,7 +318,7 @@ def parse_resource_reference(reference: str) -> tuple[str | None, str]:
318
318
  return None, "path"
319
319
 
320
320
 
321
- def resolve_remote_reference(ctx: CommandContext, reference: str) -> dict[str, Any]:
321
+ def resolve_remote_reference(ctx: CommandContext, reference: str) -> Dict[str, Any]:
322
322
  ctx.require_login()
323
323
  resource_id, _ = parse_resource_reference(reference)
324
324
  if resource_id:
@@ -345,7 +345,7 @@ def resolve_remote_reference(ctx: CommandContext, reference: str) -> dict[str, A
345
345
  return current_resource
346
346
 
347
347
 
348
- def find_child_folder(ctx: CommandContext, parent_id: str, folder_name: str) -> dict[str, Any] | None:
348
+ def find_child_folder(ctx: CommandContext, parent_id: str, folder_name: str) -> Optional[Dict[str, Any]]:
349
349
  selector = ctx.client.get_selector(parent_id)
350
350
  for child in selector.get("child_list", []):
351
351
  if child.get("rname") == folder_name:
@@ -357,7 +357,7 @@ def find_child_folder(ctx: CommandContext, parent_id: str, folder_name: str) ->
357
357
  return None
358
358
 
359
359
 
360
- def find_child_resource(ctx: CommandContext, parent_id: str, resource_name: str) -> dict[str, Any] | None:
360
+ def find_child_resource(ctx: CommandContext, parent_id: str, resource_name: str) -> Optional[Dict[str, Any]]:
361
361
  selector = ctx.client.get_selector(parent_id)
362
362
  for child in selector.get("child_list", []):
363
363
  if child.get("rname") == resource_name:
@@ -374,7 +374,7 @@ def ensure_remote_folder(ctx: CommandContext, parent_id: str, folder_name: str)
374
374
  return created["res_str_id"]
375
375
 
376
376
 
377
- def upload_file_to_remote(ctx: CommandContext, local_path: Path, parent_id: str) -> dict[str, Any]:
377
+ def upload_file_to_remote(ctx: CommandContext, local_path: Path, parent_id: str) -> Dict[str, Any]:
378
378
  with console.status(f"[bold cyan]Preparing upload for[/bold cyan] {local_path.name}", spinner="dots"):
379
379
  upload_token = ctx.client.get_upload_token(parent_id, local_path.name)
380
380
 
@@ -382,7 +382,7 @@ def upload_file_to_remote(ctx: CommandContext, local_path: Path, parent_id: str)
382
382
  with Progress(*progress_columns(), console=console, transient=True) as progress:
383
383
  task_id = progress.add_task(f"Uploading {local_path.name}", total=total)
384
384
 
385
- def on_progress(transferred: int, callback_total: int | None) -> None:
385
+ def on_progress(transferred: int, callback_total: Optional[int]) -> None:
386
386
  progress.update(task_id, completed=transferred, total=callback_total or total)
387
387
 
388
388
  uploaded = ctx.client.upload_file(upload_token, local_path, progress_callback=on_progress)
@@ -394,7 +394,7 @@ def upload_file_to_remote(ctx: CommandContext, local_path: Path, parent_id: str)
394
394
 
395
395
  def upload_directory_to_remote(ctx: CommandContext, local_dir: Path, parent_id: str) -> None:
396
396
  root_remote_id = ensure_remote_folder(ctx, parent_id, local_dir.name)
397
- remote_folder_map: dict[Path, str] = {Path("."): root_remote_id}
397
+ remote_folder_map: Dict[Path, str] = {Path("."): root_remote_id}
398
398
 
399
399
  for current_root, dirnames, filenames in os.walk(local_dir):
400
400
  dirnames.sort()
@@ -411,7 +411,7 @@ def upload_directory_to_remote(ctx: CommandContext, local_dir: Path, parent_id:
411
411
  upload_file_to_remote(ctx, current_root_path / filename, remote_parent_id)
412
412
 
413
413
 
414
- def resolve_download_destination(resource: dict[str, Any], local_path: Path | None) -> Path:
414
+ def resolve_download_destination(resource: Dict[str, Any], local_path: Optional[Path]) -> Path:
415
415
  resource_name = resource["info"]["rname"]
416
416
  if local_path is None:
417
417
  return Path.cwd() / resource_name
@@ -422,11 +422,11 @@ def resolve_download_destination(resource: dict[str, Any], local_path: Path | No
422
422
  return resolved
423
423
 
424
424
 
425
- def download_file_to_local(ctx: CommandContext, resource: dict[str, Any], destination: Path) -> str:
425
+ def download_file_to_local(ctx: CommandContext, resource: Dict[str, Any], destination: Path) -> str:
426
426
  with Progress(*progress_columns(), console=console, transient=True) as progress:
427
427
  task_id = progress.add_task(f"Downloading {resource['info']['rname']}", total=None)
428
428
 
429
- def on_progress(transferred: int, total: int | None) -> None:
429
+ def on_progress(transferred: int, total: Optional[int]) -> None:
430
430
  progress.update(task_id, completed=transferred, total=total)
431
431
 
432
432
  return ctx.client.download_resource(
@@ -438,9 +438,9 @@ def download_file_to_local(ctx: CommandContext, resource: dict[str, Any], destin
438
438
 
439
439
  def download_directory_to_local(
440
440
  ctx: CommandContext,
441
- resource: dict[str, Any],
441
+ resource: Dict[str, Any],
442
442
  destination: Path,
443
- ) -> tuple[int, int]:
443
+ ) -> Tuple[int, int]:
444
444
  destination.mkdir(parents=True, exist_ok=True)
445
445
  file_count = 0
446
446
  directory_count = 1
@@ -466,7 +466,7 @@ def download_directory_to_local(
466
466
  return file_count, directory_count
467
467
 
468
468
 
469
- def delete_resource_recursive(ctx: CommandContext, resource: dict[str, Any]) -> None:
469
+ def delete_resource_recursive(ctx: CommandContext, resource: Dict[str, Any]) -> None:
470
470
  info = resource["info"]
471
471
  if info["rtype"] == RTYPE_FOLDER:
472
472
  for child in sort_children(resource.get("child_list", [])):
@@ -476,7 +476,7 @@ def delete_resource_recursive(ctx: CommandContext, resource: dict[str, Any]) ->
476
476
  print_success(f"deleted {info['rname']} ({format_resource_ref(info['res_str_id'])})")
477
477
 
478
478
 
479
- def print_resource_listing(resource: dict[str, Any]) -> None:
479
+ def print_resource_listing(resource: Dict[str, Any]) -> None:
480
480
  info = resource["info"]
481
481
  child_list = sort_children(resource.get("child_list", []))
482
482
 
@@ -529,7 +529,7 @@ def print_resource_listing(resource: dict[str, Any]) -> None:
529
529
  console.print(table)
530
530
 
531
531
 
532
- def print_user_summary(user: dict[str, Any]) -> None:
532
+ def print_user_summary(user: Dict[str, Any]) -> None:
533
533
  summary = Table.grid(padding=(0, 2))
534
534
  summary.add_column(style="bold cyan", justify="right")
535
535
  summary.add_column()
@@ -753,7 +753,7 @@ def dispatch(args: argparse.Namespace) -> int:
753
753
  return handlers[args.command](ctx, args)
754
754
 
755
755
 
756
- def main(argv: list[str] | None = None) -> int:
756
+ def main(argv: Optional[List[str]] = None) -> int:
757
757
  parser = build_parser()
758
758
  args = parser.parse_args(argv)
759
759
  try:
@@ -4,7 +4,7 @@ import json
4
4
  import os
5
5
  from dataclasses import asdict, dataclass, field
6
6
  from pathlib import Path
7
- from typing import Any
7
+ from typing import Any, Dict, Optional
8
8
  from urllib.parse import urlencode
9
9
 
10
10
  DEFAULT_APP_ID = "R41nXZEwpfl6tw6KYyoEZ4FYST4BXPlF"
@@ -21,12 +21,12 @@ class DiskSettings:
21
21
  sso_url: str = field(default_factory=lambda: os.getenv("DISK_SSO_URL", DEFAULT_SSO_URL))
22
22
  sso_app_id: str = field(default_factory=lambda: os.getenv("DISK_SSO_APP_ID", DEFAULT_APP_ID))
23
23
  qiniu_upload_url: str = field(default_factory=lambda: os.getenv("DISK_QINIU_UPLOAD_URL", DEFAULT_QINIU_UPLOAD_URL))
24
- token: str | None = None
25
- user: dict[str, Any] | None = None
24
+ token: Optional[str] = None
25
+ user: Optional[Dict[str, Any]] = None
26
26
  state_path: Path = DEFAULT_STATE_PATH
27
27
 
28
28
  @classmethod
29
- def load(cls, state_path: Path | None = None) -> "DiskSettings":
29
+ def load(cls, state_path: Optional[Path] = None) -> "DiskSettings":
30
30
  resolved_path = (state_path or DEFAULT_STATE_PATH).expanduser()
31
31
  if state_path is None and not resolved_path.exists() and LEGACY_STATE_PATH.exists():
32
32
  resolved_path = LEGACY_STATE_PATH
@@ -60,7 +60,7 @@ class DiskSettings:
60
60
  self.token = None
61
61
  self.user = None
62
62
 
63
- def oauth_authorize_url(self, *, state: str | None = "htx-cli") -> str:
63
+ def oauth_authorize_url(self, *, state: Optional[str] = "htx-cli") -> str:
64
64
  params = {"app_id": self.sso_app_id}
65
65
  if state:
66
66
  params["state"] = state
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htx-cli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: htx-cli command-line client for the Disk backend API
5
- Requires-Python: >=3.11
5
+ Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: rich<14,>=13.9
8
8
  Requires-Dist: requests<3,>=2.32
@@ -19,7 +19,12 @@ Requires-Dist: requests<3,>=2.32
19
19
  ## Install
20
20
 
21
21
  ```bash
22
- source /Users/jyonn/Projects/venv/django/bin/activate
22
+ pip install htx-cli
23
+ ```
24
+
25
+ For local development:
26
+
27
+ ```bash
23
28
  pip install -e .
24
29
  ```
25
30
 
File without changes