Skip to content

Commit 31f8c60

Browse files
feat: improve cli docstrings (#878)
1 parent 11022a1 commit 31f8c60

9 files changed

Lines changed: 73 additions & 31 deletions

File tree

src/cli/app/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
def main() -> None:
5+
"""Invoke the CLI application."""
56
app()
67

78

src/cli/app/builder/urls.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,30 @@
66

77

88
class UrlBuilder:
9+
"""Builds backend and frontend URLs for the CLI."""
10+
911
def __init__(self, backend_url: str, frontend_url: str) -> None:
12+
"""Create a URL builder from base backend and frontend URLs."""
1013
self._backend_url = self.__ensure_scheme(backend_url)
1114
self._frontend_url = self.__ensure_scheme(frontend_url)
1215

1316
@staticmethod
1417
def __ensure_scheme(url: str) -> str:
18+
"""Ensure the URL includes a scheme."""
1519
if not urlparse(url).scheme:
1620
return "https://" + url.lstrip("/")
1721
return url
1822

1923
@staticmethod
2024
def __ensure_trailing_slash(url: str) -> str:
25+
"""Ensure the URL ends with a trailing slash."""
2126
if not url.endswith("/"):
2227
return url + "/"
2328
return url
2429

2530
@classmethod
2631
def resolve(cls, initial_url: str | None = None) -> "UrlBuilder":
32+
"""Resolve URLs from input, settings, or interactive prompts."""
2733
url_candidate = initial_url or settings.INSTANCE_URL
2834

2935
if url_candidate:
@@ -46,22 +52,28 @@ def resolve(cls, initial_url: str | None = None) -> "UrlBuilder":
4652

4753
@property
4854
def backend_url(self) -> str:
55+
"""Return the normalized backend base URL."""
4956
return self.__ensure_trailing_slash(self._backend_url)
5057

5158
@property
5259
def frontend_url(self) -> str:
60+
"""Return the normalized frontend base URL."""
5361
return self.__ensure_trailing_slash(self._frontend_url)
5462

5563
def upload_url(self) -> str:
64+
"""Return the upload endpoint URL."""
5665
return urljoin(self.backend_url, "upload")
5766

5867
def config_url(self) -> str:
68+
"""Return the config endpoint URL."""
5969
return urljoin(self.backend_url, "config")
6070

6171
def download_url(self) -> str:
72+
"""Return the download endpoint base URL."""
6273
return urljoin(self.backend_url, "download/")
6374

6475
def share_url(self, slug: str, key_secret: str) -> str:
76+
"""Build a shareable frontend URL including the key fragment."""
6577
# urljoin handles the path joining
6678
base_share = urljoin(self.frontend_url, f"download/{slug}")
6779
return f"{base_share}#{key_secret}"

src/cli/app/client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,33 @@ class _ProgressReader:
1818
"""Wraps a file object and updates a tqdm bar on every read."""
1919

2020
def __init__(self, fp: BinaryIO, pbar: tqdm) -> None:
21+
"""Create a progress-aware wrapper around a binary file object."""
2122
self._fp = fp
2223
self._pbar = pbar
2324

2425
def read(self, size: int = -1) -> bytes:
26+
"""Read bytes and advance the progress bar."""
2527
data = self._fp.read(size)
2628
if data:
2729
self._pbar.update(len(data))
2830
return data
2931

3032
def seek(self, offset: int, whence: int = 0) -> int:
33+
"""Seek within the wrapped file object."""
3134
return self._fp.seek(offset, whence)
3235

3336
def tell(self) -> int:
37+
"""Return the current stream position."""
3438
return self._fp.tell()
3539

3640
def __getattr__(self, name: str) -> Any:
41+
"""Delegate missing attributes to the wrapped file object."""
3742
return getattr(self._fp, name)
3843

3944

4045
class Client:
46+
"""Async API client for uploads and downloads."""
47+
4148
def __init__(self, urls: UrlBuilder):
4249
"""
4350
Initialize with a UrlBuilder instance.
@@ -57,6 +64,7 @@ def __init__(self, urls: UrlBuilder):
5764
)
5865

5966
async def __aenter__(self) -> Self:
67+
"""Enter the async context manager."""
6068
return self
6169

6270
async def __aexit__(
@@ -65,9 +73,11 @@ async def __aexit__(
6573
exc_value: BaseException | None,
6674
traceback: TracebackType | None,
6775
) -> None:
76+
"""Exit the async context manager and close the session."""
6877
await self.close()
6978

7079
async def close(self) -> None:
80+
"""Close the underlying HTTP session."""
7181
await self._session.aclose()
7282

7383
@classmethod

src/cli/app/commands/upload.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
from app.helpers.file import cleanup
1414
from app.helpers.print import print_compact_qr
1515

16-
app = typer.AsyncTyper(help="Upload encrypted files via Chithi.")
17-
console = Console()
16+
app: typer.AsyncTyper = typer.AsyncTyper(help="Upload encrypted files via Chithi.")
17+
console: Console = Console()
18+
error_console: Console = Console(stderr=True)
1819

1920

2021
@app.async_command()
@@ -32,7 +33,7 @@ async def upload(
3233
bool, typer.Option("--no-qr", help="Do not print the QR code.")
3334
] = False,
3435
) -> None:
35-
"""Compress, encrypt, and upload a file or folder."""
36+
"""Compress, encrypt, and upload a file or folder, then print the share link."""
3637
try:
3738
# Resolve URLs based on input/prompts
3839
urls = UrlBuilder.resolve(instance_url)
@@ -93,18 +94,18 @@ async def upload(
9394
# UI Output Logic
9495
if minimal:
9596
# Clean output for scripts or pipes
96-
typer.echo(download_url)
97+
console.print(download_url, highlight=False, markup=False)
9798
else:
9899
# Pretty output
99-
typer.echo("\n✓ Upload complete!")
100+
console.print("\n[green]✓ Upload complete![/green]")
100101
if not no_qr:
101102
print_compact_qr(download_url, console)
102-
typer.echo(f"\n Download URL : {download_url}")
103+
console.print(f"\n Download URL : {download_url}")
103104
if password:
104-
typer.echo(
105-
" ⚠ Password-protected. Recipients will need the password to decrypt."
105+
console.print(
106+
" [yellow]⚠ Password-protected. Recipients will need the password to decrypt.[/yellow]"
106107
)
107108

108109
except Exception as exc:
109-
typer.echo(f"✗ Upload failed: {exc}", err=True)
110+
error_console.print(f"[red]✗ Upload failed: {exc}[/red]")
110111
raise typer.Exit(code=1)

src/cli/app/helpers/archive.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88

99
def _get_source_size(source: Path) -> int:
10+
"""Return the size of a file or total size of a directory tree."""
1011
if source.is_file():
1112
return source.stat().st_size
1213
return sum(f.stat().st_size for f in source.rglob("*") if f.is_file())

src/cli/app/helpers/crypto.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ def generate_ikm() -> bytes:
2424

2525

2626
def ikm_to_base64url(ikm: bytes) -> str:
27+
"""Encode IKM as URL-safe base64 without padding."""
2728
return base64.urlsafe_b64encode(ikm).rstrip(b"=").decode("ascii")
2829

2930

3031
def base64url_to_ikm(s: str) -> bytes:
32+
"""Decode URL-safe base64 into IKM bytes."""
3133
s += "=" * (-len(s) % 4)
3234
return base64.urlsafe_b64decode(s)
3335

@@ -137,6 +139,7 @@ def _derive_secrets(ikm: bytes, password: str | None) -> Tuple[bytes, bytes]:
137139

138140

139141
def _argon2(password: bytes, salt: bytes, iterations: int, memory: int) -> bytes:
142+
"""Run Argon2id with provided parameters."""
140143
kdf = Argon2id(
141144
salt=salt,
142145
length=32,
@@ -148,6 +151,7 @@ def _argon2(password: bytes, salt: bytes, iterations: int, memory: int) -> bytes
148151

149152

150153
def _get_chunk_iv(base_iv: bytes, index: int) -> bytes:
154+
"""Derive a per-chunk IV by XORing the index into the base IV."""
151155
iv = bytearray(base_iv)
152156
existing = struct.unpack_from("!I", iv, 8)[0]
153157
struct.pack_into("!I", iv, 8, existing ^ index)

src/cli/app/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33

44
class Settings(BaseSettings):
5+
"""Environment-driven settings for the CLI."""
6+
57
INSTANCE_URL: str | None = None
68
EXPIRE_AFTER_N_DOWNLOAD: int | None = None
79
EXPIRE_AFTER: int | None = None

src/cli/build.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010

1111
class NuitkaBuilder:
12+
"""Builds a standalone CLI binary using Nuitka."""
13+
1214
def __init__(self, target: str = "./app", output_name: str | None = None) -> None:
15+
"""Create a builder for a target module path."""
1316
self.target = target
1417
self.output_name = output_name
1518
self.base_args: list[str] = [
@@ -27,6 +30,7 @@ def __init__(self, target: str = "./app", output_name: str | None = None) -> Non
2730
]
2831

2932
def build_windows(self, debug: bool = False) -> None:
33+
"""Build a Windows binary with optional debug flags."""
3034
args = self.base_args.copy()
3135
args.extend(
3236
[
@@ -42,11 +46,13 @@ def build_windows(self, debug: bool = False) -> None:
4246
self._run(args)
4347

4448
def build_linux(self) -> None:
49+
"""Build a Linux binary using clang."""
4550
args = self.base_args.copy()
4651
args.append("--clang")
4752
self._run(args)
4853

4954
def _run(self, args: list[str]) -> None:
55+
"""Run the Nuitka build with the assembled arguments."""
5056
if self.output_name:
5157
args.append(f"--output-filename={self.output_name}")
5258

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { cn } from '$lib/utils';
3-
import QRCode from 'qrcode';
3+
import { renderSVG } from 'uqr';
44
55
interface Props {
66
value: string;
@@ -18,36 +18,41 @@
1818
class: klass
1919
}: Props = $props();
2020
21-
let canvas = $state<null | HTMLCanvasElement>(null);
21+
let svgMarkup = $state('');
2222
2323
$effect(() => {
24-
if (canvas) {
25-
QRCode.toCanvas(
26-
canvas,
27-
value,
28-
{
29-
width: size,
30-
margin: 1,
31-
color: {
32-
dark: color,
33-
light: backgroundColor
34-
}
35-
},
36-
(error) => {
37-
if (error) console.error(error);
38-
}
39-
);
24+
const svg = renderSVG(value, {
25+
border: 1,
26+
pixelSize: 1,
27+
blackColor: color,
28+
whiteColor: backgroundColor
29+
});
30+
31+
if (klass) {
32+
const safeClass = klass.replace(/"/g, '&quot;');
33+
svgMarkup = svg.replace('<svg', `<svg class="${safeClass}"`);
34+
return;
4035
}
36+
37+
svgMarkup = svg;
4138
});
4239
</script>
4340

4441
<div
4542
class={cn(
46-
`grid h-fit w-fit`,
47-
// I dont like the fact that canvas is a image and people can right click canvas to get the image
48-
// So i am making the canvas unclickable using the same technique facebook/instagram uses
43+
`qr-code grid h-fit w-fit`,
44+
// Prevent right-click save on the rendered SVG by overlaying a transparent layer.
4945
`after:col-start-1 after:row-start-1 after:h-full after:w-full after:content-['']`
5046
)}
47+
style={`width: ${size}px; height: ${size}px;`}
5148
>
52-
<canvas bind:this={canvas} class={cn(klass, 'col-start-1 row-start-1')}></canvas>
49+
{@html svgMarkup}
5350
</div>
51+
52+
<style>
53+
.qr-code :global(svg) {
54+
width: 100%;
55+
height: 100%;
56+
display: block;
57+
}
58+
</style>

0 commit comments

Comments
 (0)