cloud-tc 0.1.0__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.
cloud_tc/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
cloud_tc/cli.py ADDED
@@ -0,0 +1,422 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import stat
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import httpx
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+
15
+ from cloud_tc import __version__
16
+
17
+ IS_WINDOWS = sys.platform.startswith("win")
18
+
19
+
20
+ def _enable_windows_ansi() -> None:
21
+ if not IS_WINDOWS:
22
+ return
23
+ try:
24
+ import ctypes
25
+ kernel32 = ctypes.windll.kernel32
26
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
27
+ for handle_id in (-11, -12):
28
+ h = kernel32.GetStdHandle(handle_id)
29
+ mode = ctypes.c_uint32()
30
+ if kernel32.GetConsoleMode(h, ctypes.byref(mode)):
31
+ kernel32.SetConsoleMode(h, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
32
+ except Exception:
33
+ pass
34
+
35
+
36
+ _enable_windows_ansi()
37
+
38
+ app = typer.Typer(
39
+ no_args_is_help=True,
40
+ add_completion=False,
41
+ pretty_exceptions_show_locals=False,
42
+ help="Terminal client for OnlySq Cloud 2.0",
43
+ )
44
+ console = Console()
45
+
46
+
47
+ def _config_dir() -> Path:
48
+ """
49
+ Cross-platform per-user config directory.
50
+
51
+ Windows: %APPDATA%\\cloud-tc
52
+ macOS: ~/Library/Application Support/cloud-tc
53
+ Linux/*: $XDG_CONFIG_HOME/cloud-tc, fallback ~/.config/cloud-tc
54
+
55
+ Legacy ~/.cloud-tc (used by 0.1.0) is honored if it already exists.
56
+ """
57
+ override = os.environ.get("CLOUD_TC_HOME")
58
+ if override:
59
+ return Path(override).expanduser()
60
+
61
+ legacy = Path.home() / ".cloud-tc"
62
+ if legacy.exists():
63
+ return legacy
64
+
65
+ if IS_WINDOWS:
66
+ base = os.environ.get("APPDATA") or str(Path.home() / "AppData" / "Roaming")
67
+ return Path(base) / "cloud-tc"
68
+ if sys.platform == "darwin":
69
+ return Path.home() / "Library" / "Application Support" / "cloud-tc"
70
+ xdg = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
71
+ return Path(xdg) / "cloud-tc"
72
+
73
+
74
+ CONFIG_DIR = _config_dir()
75
+ CONFIG_FILE = CONFIG_DIR / "config.json"
76
+ DEFAULT_BASE = "https://cloud.onlysq.ru"
77
+
78
+
79
+ def _save(data: dict) -> None:
80
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
81
+ CONFIG_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
82
+ if not IS_WINDOWS:
83
+ try:
84
+ CONFIG_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)
85
+ except Exception:
86
+ pass
87
+
88
+
89
+ def _load() -> dict:
90
+ if not CONFIG_FILE.exists():
91
+ return {}
92
+ try:
93
+ return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
94
+ except Exception:
95
+ return {}
96
+
97
+
98
+ def _client(require_auth: bool = True) -> httpx.Client:
99
+ cfg = _load()
100
+ base = cfg.get("base_url") or os.environ.get("TC_BASE_URL") or DEFAULT_BASE
101
+ token = cfg.get("token") or os.environ.get("TC_TOKEN")
102
+ headers = {"User-Agent": f"cloud-tc/{__version__} ({sys.platform})"}
103
+ if token:
104
+ headers["Authorization"] = f"Bearer {token}"
105
+ elif require_auth:
106
+ console.print("[red]no token configured[/] · run [bold]tc login --token tck_…[/]")
107
+ raise typer.Exit(1)
108
+ return httpx.Client(base_url=base, headers=headers, timeout=120.0, follow_redirects=False)
109
+
110
+
111
+ def _hsize(n) -> str:
112
+ n = int(n or 0)
113
+ units = ["B", "KB", "MB", "GB", "TB", "PB"]
114
+ if n >= 1024 ** 6:
115
+ return "∞"
116
+ i = 0
117
+ f = float(n)
118
+ while f >= 1024 and i < len(units) - 1:
119
+ f /= 1024
120
+ i += 1
121
+ if i == 0:
122
+ return f"{int(f)} {units[i]}"
123
+ if i == 1:
124
+ return f"{f:.1f} {units[i]}"
125
+ return f"{f:.2f} {units[i]}"
126
+
127
+
128
+ def _fail_if_bad(r: httpx.Response, ctx: str = "") -> dict:
129
+ try:
130
+ data = r.json()
131
+ except Exception:
132
+ data = {"raw": r.text}
133
+ if r.status_code >= 300 or (isinstance(data, dict) and data.get("ok") is False):
134
+ msg = data.get("error") if isinstance(data, dict) else r.text
135
+ console.print(f"[red]{ctx or 'error'}[/] HTTP {r.status_code} · {msg}")
136
+ raise typer.Exit(1)
137
+ return data
138
+
139
+
140
+ @app.command()
141
+ def version():
142
+ console.print(f"cloud-tc [bold]{__version__}[/] · {sys.platform} · python {sys.version.split()[0]}")
143
+
144
+
145
+ @app.command()
146
+ def info():
147
+ cfg = _load()
148
+ console.print(f"[bold]cloud-tc[/] v{__version__}")
149
+ console.print(f" platform: {sys.platform}")
150
+ console.print(f" config: {CONFIG_FILE}")
151
+ if cfg:
152
+ console.print(f" base_url = {cfg.get('base_url', DEFAULT_BASE)}")
153
+ if cfg.get("token"):
154
+ console.print(f" token = {cfg['token'][:14]}…")
155
+ else:
156
+ console.print(" [dim]not logged in[/]")
157
+
158
+
159
+ @app.command()
160
+ def login(
161
+ token: str = typer.Option(..., "--token", "-t", help="API token (tck_…) — create one at /v2/ui/settings"),
162
+ base_url: str = typer.Option(DEFAULT_BASE, "--base-url", "-u"),
163
+ ):
164
+ """Save an API token locally and verify it works."""
165
+ with httpx.Client(base_url=base_url, headers={"Authorization": f"Bearer {token}"}, timeout=30) as cli:
166
+ r = cli.get("/v2/quota")
167
+ if r.status_code != 200:
168
+ console.print(f"[red]token rejected[/] HTTP {r.status_code} · {r.text}")
169
+ raise typer.Exit(1)
170
+ q = r.json()
171
+ _save({"token": token, "base_url": base_url})
172
+ console.print(
173
+ f"[green]ok[/] logged in · used {_hsize(q['used_bytes'])} / "
174
+ f"{_hsize(q['quota_bytes'])}"
175
+ )
176
+
177
+
178
+ @app.command()
179
+ def logout():
180
+ """Remove the local config."""
181
+ if CONFIG_FILE.exists():
182
+ CONFIG_FILE.unlink()
183
+ console.print("[green]ok[/] config removed")
184
+
185
+
186
+ @app.command()
187
+ def me():
188
+ """Print the current user profile."""
189
+ with _client() as cli:
190
+ r = cli.get("/auth/me")
191
+ data = _fail_if_bad(r, "me")
192
+ u = data.get("user") or {}
193
+ console.print(f"[bold]{u.get('name')}[/] · {u.get('email') or 'no email'}")
194
+ console.print(f" sauth_id: {u.get('sauth_id')} level: {u.get('level')}{' · admin' if u.get('is_admin') else ''}")
195
+ console.print(f" storage: {_hsize(u.get('used_bytes'))} / {_hsize(u.get('quota_bytes'))}")
196
+ if u.get("has_legacy"):
197
+ console.print(f" legacy: {'migrated' if u.get('migrated') else 'linked, not migrated'}")
198
+
199
+
200
+ @app.command()
201
+ def quota():
202
+ """Show used / total quota."""
203
+ with _client() as cli:
204
+ r = cli.get("/v2/quota")
205
+ q = _fail_if_bad(r, "quota")
206
+ used = q["used_bytes"]
207
+ total = q["quota_bytes"]
208
+ pct = (used / max(1, total)) * 100
209
+ if total >= 1024 ** 6:
210
+ console.print(f"used [bold]{_hsize(used)}[/] / ∞ (unlimited tier)")
211
+ else:
212
+ console.print(f"used [bold]{_hsize(used)}[/] / {_hsize(total)} ([yellow]{pct:.1f}%[/])")
213
+
214
+
215
+ @app.command()
216
+ def ls(folder_id: Optional[int] = typer.Argument(None, help="folder id (omit for root)")):
217
+ """List files in a folder."""
218
+ with _client() as cli:
219
+ q = f"?folder_id={folder_id}" if folder_id else "?root=1"
220
+ r = cli.get(f"/v2/files{q}")
221
+ data = _fail_if_bad(r, "list")
222
+ files = data.get("files", [])
223
+ if not files:
224
+ console.print("[dim]empty[/]")
225
+ return
226
+ t = Table(show_lines=False, header_style="bold")
227
+ t.add_column("UID", style="cyan", no_wrap=True)
228
+ t.add_column("Name")
229
+ t.add_column("Size", justify="right", style="dim")
230
+ t.add_column("Mime", style="dim")
231
+ t.add_column("Status", justify="center")
232
+ for f in files:
233
+ status = "ready" if f.get("status") == 1 else f"#{f.get('status')}"
234
+ t.add_row(
235
+ f["uid"], f["name"], _hsize(f.get("size")),
236
+ f.get("mime") or "—", status,
237
+ )
238
+ console.print(t)
239
+
240
+
241
+ @app.command()
242
+ def upload(
243
+ path: Path = typer.Argument(..., exists=True, readable=True),
244
+ folder_id: Optional[int] = typer.Option(None, "--folder", "-f"),
245
+ ):
246
+ """Upload a local file."""
247
+ path = path.resolve()
248
+ if not path.is_file():
249
+ console.print("[red]not a file[/]")
250
+ raise typer.Exit(1)
251
+ with _client() as cli:
252
+ with path.open("rb") as f:
253
+ files = {"file": (path.name, f, "application/octet-stream")}
254
+ data = {"folder_id": str(folder_id)} if folder_id else {}
255
+ r = cli.post("/v2/files/upload", files=files, data=data)
256
+ out = _fail_if_bad(r, "upload")
257
+ f = out["file"]
258
+ console.print(f"[green]ok[/] {f['uid']} · {f['name']} · {_hsize(f['size'])}")
259
+
260
+
261
+ @app.command()
262
+ def download(
263
+ uid: str = typer.Argument(...),
264
+ out: Optional[Path] = typer.Option(None, "--out", "-o"),
265
+ ):
266
+ """Download a file by uid."""
267
+ with _client() as cli:
268
+ meta = _fail_if_bad(cli.get(f"/v2/files/{uid}"), "meta")
269
+ name = meta["file"]["name"]
270
+ target = (out or Path(name)).resolve()
271
+ target.parent.mkdir(parents=True, exist_ok=True)
272
+ with cli.stream("GET", f"/v2/files/{uid}/stream?mode=dl") as r:
273
+ if r.status_code != 200:
274
+ console.print(f"[red]error[/] HTTP {r.status_code}")
275
+ raise typer.Exit(1)
276
+ with target.open("wb") as fp:
277
+ for chunk in r.iter_bytes(chunk_size=65536):
278
+ fp.write(chunk)
279
+ console.print(f"[green]ok[/] saved {target}")
280
+
281
+
282
+ @app.command()
283
+ def rm(uid: str = typer.Argument(...)):
284
+ """Soft-delete a file (moves to trash for 30 days)."""
285
+ with _client() as cli:
286
+ _fail_if_bad(cli.delete(f"/v2/files/{uid}"), "delete")
287
+ console.print(f"[green]ok[/] {uid} → trash")
288
+
289
+
290
+ @app.command()
291
+ def restore(uid: str = typer.Argument(...)):
292
+ """Restore a file from trash."""
293
+ with _client() as cli:
294
+ _fail_if_bad(cli.post(f"/v2/files/{uid}/restore"), "restore")
295
+ console.print(f"[green]ok[/] {uid} restored")
296
+
297
+
298
+ @app.command()
299
+ def mv(
300
+ uid: str = typer.Argument(...),
301
+ folder_id: int = typer.Argument(...),
302
+ ):
303
+ """Move a file to another folder."""
304
+ with _client() as cli:
305
+ _fail_if_bad(cli.patch(f"/v2/files/{uid}", json={"folder_id": folder_id}), "move")
306
+ console.print(f"[green]ok[/] {uid} → folder #{folder_id}")
307
+
308
+
309
+ @app.command()
310
+ def rename(
311
+ uid: str = typer.Argument(...),
312
+ name: str = typer.Argument(...),
313
+ ):
314
+ """Rename a file."""
315
+ with _client() as cli:
316
+ _fail_if_bad(cli.patch(f"/v2/files/{uid}", json={"name": name}), "rename")
317
+ console.print(f"[green]ok[/] renamed to {name}")
318
+
319
+
320
+ @app.command()
321
+ def mkdir(
322
+ name: str = typer.Argument(...),
323
+ parent: Optional[int] = typer.Option(None, "--parent", "-p"),
324
+ ):
325
+ """Create a folder."""
326
+ with _client() as cli:
327
+ body: dict = {"name": name}
328
+ if parent is not None:
329
+ body["parent_id"] = parent
330
+ data = _fail_if_bad(cli.post("/v2/folders", json=body), "mkdir")
331
+ f = data["folder"]
332
+ console.print(f"[green]ok[/] folder #{f['id']} · {f['name']}")
333
+
334
+
335
+ @app.command()
336
+ def share(
337
+ uid: str = typer.Argument(...),
338
+ role: str = typer.Option("viewer", "--role", "-r", help="viewer | commenter | editor"),
339
+ expires: Optional[str] = typer.Option(None, "--expires", help="ISO 8601 expiry"),
340
+ password: Optional[str] = typer.Option(None, "--password"),
341
+ max_uses: Optional[int] = typer.Option(None, "--max-uses"),
342
+ ):
343
+ """Create a public share link."""
344
+ with _client() as cli:
345
+ body: dict = {"role": role}
346
+ if expires:
347
+ body["expires_at"] = expires
348
+ if password:
349
+ body["password"] = password
350
+ if max_uses:
351
+ body["max_uses"] = max_uses
352
+ data = _fail_if_bad(cli.post(f"/v2/files/{uid}/share", json=body), "share")
353
+ cfg = _load()
354
+ base = cfg.get("base_url", DEFAULT_BASE)
355
+ url = f"{base}/v2/ui/s/{data['share']['token']}"
356
+ console.print(f"[green]share[/] {url}")
357
+
358
+
359
+ tokens_app = typer.Typer(no_args_is_help=True, help="Manage API tokens")
360
+ app.add_typer(tokens_app, name="tokens")
361
+
362
+
363
+ @tokens_app.command("list")
364
+ def tokens_list():
365
+ with _client() as cli:
366
+ data = _fail_if_bad(cli.get("/v2/tokens"), "tokens")
367
+ t = Table(header_style="bold")
368
+ t.add_column("Name")
369
+ t.add_column("Prefix", style="cyan")
370
+ t.add_column("Scopes", style="yellow")
371
+ t.add_column("Last used", style="dim")
372
+ for row in data.get("tokens", []):
373
+ scopes = ",".join(row.get("scopes") or []) or "default"
374
+ t.add_row(
375
+ row["name"], row["prefix"] + "…",
376
+ scopes, row.get("last_used_at") or "never",
377
+ )
378
+ console.print(t)
379
+
380
+
381
+ @tokens_app.command("create")
382
+ def tokens_create(
383
+ name: str = typer.Argument(...),
384
+ scopes: Optional[str] = typer.Option(None, "--scopes", "-s", help="comma-separated scopes"),
385
+ ):
386
+ sc = [x.strip() for x in (scopes or "").split(",") if x.strip()]
387
+ with _client() as cli:
388
+ data = _fail_if_bad(cli.post("/v2/tokens", json={"name": name, "scopes": sc}), "create token")
389
+ console.print(f"[green]ok[/] {data['token']['name']}")
390
+ console.print(f"[bold yellow]token (shown once):[/] {data['token']['token']}")
391
+
392
+
393
+ @tokens_app.command("revoke")
394
+ def tokens_revoke(id: int = typer.Argument(...)):
395
+ with _client() as cli:
396
+ _fail_if_bad(cli.delete(f"/v2/tokens/{id}"), "revoke")
397
+ console.print(f"[green]ok[/] revoked #{id}")
398
+
399
+
400
+ @app.command(name="migrate-self")
401
+ def migrate_self(
402
+ legacy_token: str = typer.Argument(...),
403
+ base_url: str = typer.Option(DEFAULT_BASE, "--base-url", "-u"),
404
+ ):
405
+ """Migrate a legacy account into v2 (no auth required)."""
406
+ with httpx.Client(base_url=base_url, timeout=60) as cli:
407
+ r = cli.post("/v2/migrate/legacy/self", json={"legacy_token": legacy_token})
408
+ data = _fail_if_bad(r, "migrate")
409
+ console.print(
410
+ f"[green]ok[/] migrated={data.get('migrated_count', 0)} "
411
+ f"already={data.get('already')} stub={data.get('stub')} "
412
+ f"cloud2_user_id={data.get('cloud2_user_id')}"
413
+ )
414
+ console.print(f"login: {base_url}{data['login_url']}")
415
+
416
+
417
+ def main():
418
+ app()
419
+
420
+
421
+ if __name__ == "__main__":
422
+ main()
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: cloud-tc
3
+ Version: 0.1.0
4
+ Summary: Terminal client for OnlySq Cloud 2.0 — talks to https://cloud.onlysq.ru via REST
5
+ Author-email: OnlySq <contact@onlysq.ru>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/xOnlySq/cloud-tc
8
+ Project-URL: Repository, https://github.com/xOnlySq/cloud-tc
9
+ Project-URL: Issues, https://github.com/xOnlySq/cloud-tc/issues
10
+ Project-URL: Service, https://cloud.onlysq.ru
11
+ Project-URL: Documentation, https://cloud.onlysq.ru/docs
12
+ Keywords: onlysq,cloud,telegram,cli,storage,drive
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: End Users/Desktop
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Operating System :: MacOS
20
+ Classifier: Operating System :: Microsoft :: Windows
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3 :: Only
26
+ Classifier: Topic :: Internet :: WWW/HTTP
27
+ Classifier: Topic :: System :: Archiving
28
+ Classifier: Topic :: Utilities
29
+ Requires-Python: >=3.10
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Requires-Dist: httpx>=0.27
33
+ Requires-Dist: typer>=0.12
34
+ Requires-Dist: rich>=13.7
35
+ Dynamic: license-file
36
+
37
+ # cloud-tc
38
+
39
+ Terminal client for **OnlySq Cloud 2.0** (`https://cloud.onlysq.ru`). Talks to the public v2 REST API — no Telegram, no Postgres on your machine, just `httpx` + `typer` + `rich`.
40
+
41
+ ## Install
42
+
43
+ From PyPI (when published):
44
+
45
+ ```bash
46
+ pip install cloud-tc
47
+ ```
48
+
49
+ Straight from git:
50
+
51
+ ```bash
52
+ pip install git+https://github.com/xOnlySq/cloud-tc.git
53
+ ```
54
+
55
+ Or for a local checkout (with `-e` for editable):
56
+
57
+ ```bash
58
+ git clone https://github.com/xOnlySq/cloud-tc.git
59
+ cd cloud-tc
60
+ pip install -e .
61
+ ```
62
+
63
+ Both `tc` and `cloud-tc` console scripts are installed.
64
+
65
+ ## Quick start
66
+
67
+ 1. Create an API token in the web UI: `https://cloud.onlysq.ru/v2/ui/settings` → "new token".
68
+ 2. Log in locally:
69
+
70
+ ```bash
71
+ tc login --token tck_xxxxxxxxxxxxxxxxxxxx
72
+ ```
73
+
74
+ The token is stored in `~/.cloud-tc/config.json` (0600).
75
+
76
+ 3. Use it:
77
+
78
+ ```bash
79
+ tc me
80
+ tc quota
81
+ tc ls
82
+ tc upload ./photo.jpg
83
+ tc share <uid> --role viewer
84
+ ```
85
+
86
+ ## Commands
87
+
88
+ | Command | Description |
89
+ |---|---|
90
+ | `tc login --token tck_…` | Save and verify an API token |
91
+ | `tc logout` | Remove local config |
92
+ | `tc info` | Show local config |
93
+ | `tc version` | Print package version |
94
+ | `tc me` | Show current user profile |
95
+ | `tc quota` | Show used / total quota |
96
+ | `tc ls [FOLDER_ID]` | List files |
97
+ | `tc upload PATH [-f FOLDER]` | Upload a local file |
98
+ | `tc download UID [-o PATH]` | Download a file |
99
+ | `tc rm UID` | Move to trash |
100
+ | `tc restore UID` | Restore from trash |
101
+ | `tc mv UID FOLDER_ID` | Move file to another folder |
102
+ | `tc rename UID NEW_NAME` | Rename |
103
+ | `tc mkdir NAME [-p PARENT_ID]` | Create folder |
104
+ | `tc share UID [-r ROLE] [--password] [--expires] [--max-uses]` | Create a share link |
105
+ | `tc tokens list / create NAME / revoke ID` | Manage API tokens |
106
+ | `tc migrate-self LEGACY_TOKEN` | Move a legacy account to v2 (no auth required) |
107
+
108
+ ## Configuration
109
+
110
+ You can override the server with environment variables (useful for self-hosted instances):
111
+
112
+ ```bash
113
+ export TC_BASE_URL="https://cloud.example.com"
114
+ export TC_TOKEN="tck_..."
115
+ tc ls
116
+ ```
117
+
118
+ `TC_TOKEN` overrides the saved token if set. `TC_BASE_URL` overrides the configured base URL.
119
+
120
+ Config file: `~/.cloud-tc/config.json` — just `{ "token": "...", "base_url": "..." }`.
121
+
122
+ ## Examples
123
+
124
+ Upload everything from a directory into a folder:
125
+
126
+ ```bash
127
+ for f in *.jpg; do tc upload "$f" -f 42; done
128
+ ```
129
+
130
+ Pipe a file through `tc upload` (Linux):
131
+
132
+ ```bash
133
+ tc upload /dev/stdin <<< "hello" # named pipe trick, see issues for binary
134
+ ```
135
+
136
+ Quick public link for a file:
137
+
138
+ ```bash
139
+ UID=$(tc upload ./report.pdf | awk '{print $2}')
140
+ tc share "$UID" --role viewer
141
+ ```
142
+
143
+ Batch-migrate one legacy token from any script:
144
+
145
+ ```bash
146
+ tc migrate-self USERS_LEGACY_TOKEN
147
+ ```
148
+
149
+ ## License
150
+
151
+ MIT. See [LICENSE](LICENSE).
152
+
153
+ ## Related
154
+
155
+ - Cloud UI: <https://cloud.onlysq.ru>
156
+ - REST API docs: <https://cloud.onlysq.ru/docs> (see "Cloud 2.0" group)
157
+ - Service / issues: <https://github.com/xOnlySq/cloud-tc/issues>
@@ -0,0 +1,8 @@
1
+ cloud_tc/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ cloud_tc/cli.py,sha256=fnp3StiulQpXjkS3bNq0-3vK_flrFQ-NKz9Ion7IQyM,13921
3
+ cloud_tc-0.1.0.dist-info/licenses/LICENSE,sha256=zSAyimKavmIorc8acq_9EuFMQ6pRDb-LzIWcBQ7uAB8,1063
4
+ cloud_tc-0.1.0.dist-info/METADATA,sha256=zC9SKYyIcAvdjFMbArgCT3pThReWcRKjWsMbeuNbmLs,4395
5
+ cloud_tc-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ cloud_tc-0.1.0.dist-info/entry_points.txt,sha256=7H7z8Rib5sCJnPQV0RPKsYlyTVtcLHKVHao4Imwa-OE,68
7
+ cloud_tc-0.1.0.dist-info/top_level.txt,sha256=RB6q8HPa54yUqL7MBo3yCnMEhjuYuSbJivF2-lCR-eQ,9
8
+ cloud_tc-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cloud-tc = cloud_tc.cli:app
3
+ tc = cloud_tc.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OnlySq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ cloud_tc