yeetr 2026.5.21.post15__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.
- yeetr-2026.5.21.post15/LICENSE +21 -0
- yeetr-2026.5.21.post15/PKG-INFO +480 -0
- yeetr-2026.5.21.post15/README.md +456 -0
- yeetr-2026.5.21.post15/pyproject.toml +137 -0
- yeetr-2026.5.21.post15/yeeter/__init__.py +16 -0
- yeetr-2026.5.21.post15/yeeter/_cli.py +58 -0
- yeetr-2026.5.21.post15/yeeter/_metadata.py +76 -0
- yeetr-2026.5.21.post15/yeeter/_runner.py +933 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roger
|
|
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,480 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yeetr
|
|
3
|
+
Version: 2026.5.21.post15
|
|
4
|
+
Summary: A tiny, typed, signature-driven CLI runner.
|
|
5
|
+
Keywords: python
|
|
6
|
+
Author: yeeter
|
|
7
|
+
Author-email: yeeter <noreply@example.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Requires-Dist: rich>=15.0.0
|
|
16
|
+
Requires-Dist: rich-argparse>=1.5.2
|
|
17
|
+
Requires-Dist: uvloop>=0.21.0 ; extra == 'uvloop'
|
|
18
|
+
Requires-Python: >=3.14
|
|
19
|
+
Project-URL: Homepage, https://rogerthomas.github.io/yeetr/
|
|
20
|
+
Project-URL: Repository, https://github.com/RogerThomas/yeetr
|
|
21
|
+
Project-URL: Documentation, https://rogerthomas.github.io/yeetr/
|
|
22
|
+
Provides-Extra: uvloop
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="assets/yeetr.png" alt="yeeter" width="500">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
# yeeter
|
|
30
|
+
|
|
31
|
+
[](https://github.com/RogerThomas/yeetr/releases)
|
|
32
|
+
[](https://github.com/RogerThomas/yeetr/actions/workflows/main.yml?query=branch%3Amain)
|
|
33
|
+
[](https://github.com/RogerThomas/yeetr/blob/main/LICENSE)
|
|
34
|
+
|
|
35
|
+
A tiny, typed, signature-driven CLI runner.
|
|
36
|
+
|
|
37
|
+
PyPI distribution: `yeetr`
|
|
38
|
+
Python import package: `yeeter`
|
|
39
|
+
CLI command: `yeet`
|
|
40
|
+
|
|
41
|
+
> No decorators.
|
|
42
|
+
> No command classes.
|
|
43
|
+
> No ceremony.
|
|
44
|
+
> Just yeet the function.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Minimal example
|
|
49
|
+
|
|
50
|
+
### Zero-boilerplate: the `yeet` script
|
|
51
|
+
|
|
52
|
+
Installing yeeter also installs a `yeet` script that finds and runs a
|
|
53
|
+
function in any Python file.
|
|
54
|
+
|
|
55
|
+
No `if __name__ == "__main__"` block, no `yeeter.run(...)` call — just
|
|
56
|
+
the function:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# app.py
|
|
60
|
+
def main(thing: int, *, n: float = 0.1) -> None:
|
|
61
|
+
print(thing, n)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
yeet file.py 5 --n 0.2
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The default function name is `main`. Pass a different one to pick another
|
|
69
|
+
top-level function in the same file:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# app.py
|
|
73
|
+
def main(...) -> None: ...
|
|
74
|
+
def greet(name: str, *, loud: bool = False) -> None: ...
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
yeet file.py greet world --loud
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`yeet file.py --help` prints the **target function's** help, not yeet's.
|
|
82
|
+
`yeet` itself only has `yeet FILE [FUNC] [args...]`.
|
|
83
|
+
|
|
84
|
+
You can still use the explicit `yeeter.run(main)` form when you prefer —
|
|
85
|
+
the `yeet` script is just sugar on top of it.
|
|
86
|
+
|
|
87
|
+
### Explicit `yeeter.run(main)`
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
def main(thing: int, *, n: float = 0.1) -> None:
|
|
91
|
+
print(thing, n)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
import yeeter
|
|
96
|
+
yeeter.run(main)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
yeet file.py 5 --n 0.2
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Note the bare `*` in the signature: parameters **before** it become
|
|
104
|
+
positional CLI args, parameters **after** it become `--options`. That's
|
|
105
|
+
the whole mapping — no decorators, no per-parameter annotations needed.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Hashbang
|
|
110
|
+
|
|
111
|
+
For tiny scripts, you can make the file itself executable and let `yeet`
|
|
112
|
+
discover `main` directly from the shebang. The short forms are:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
#!yeet
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
or:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
#!uv run yeet
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
For example:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
#!yeet
|
|
128
|
+
|
|
129
|
+
def main(name: str, *, loud: bool = False) -> None:
|
|
130
|
+
print(name.upper() if loud else name)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Then run it directly:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
chmod +x greet.py
|
|
137
|
+
./greet.py world --loud
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If you need a different entry function, keep the shebang simple and call
|
|
141
|
+
`uv run yeet file.py other_func ...` explicitly instead.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Async
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
async def main(name: str, *, loud: bool = False) -> None:
|
|
149
|
+
...
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
yeet file.py world --loud
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
If the function is a coroutine, its result is awaited via `asyncio.run`,
|
|
157
|
+
or via [`uvloop.run`](https://github.com/MagicStack/uvloop) when the
|
|
158
|
+
optional `uvloop` extra is installed:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
uv add "yeeter[uvloop]"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
When `uvloop` is importable, yeeter uses it transparently — no code
|
|
165
|
+
change required. Otherwise it falls back to the stdlib event loop.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Path
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from pathlib import Path
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def main(path: Path, *, output: Path | None = None) -> None:
|
|
176
|
+
...
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
yeet file.py input.pdf --output out.txt
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Literal choices
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from typing import Literal
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def main(*, format: Literal["json", "csv"] = "json") -> None:
|
|
192
|
+
...
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
yeet file.py --format csv
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## `Arg` and `Opt` metadata
|
|
202
|
+
|
|
203
|
+
For aliases and help text, use `Arg` (positional) or `Opt` (keyword-only)
|
|
204
|
+
inside `Annotated`:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from pathlib import Path
|
|
208
|
+
from typing import Annotated
|
|
209
|
+
from yeeter import Arg, Opt
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def main(
|
|
213
|
+
path: Annotated[Path, Arg(help="Input file")],
|
|
214
|
+
*,
|
|
215
|
+
workers: Annotated[int, Opt(alias="-w", help="Worker count")] = 4,
|
|
216
|
+
) -> None:
|
|
217
|
+
...
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
yeet file.py input.pdf -w 8
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`Arg` accepts `help`, `metavar`, `min`, and the path validators below. `Opt`
|
|
225
|
+
accepts `alias`, `aliases`, `help`, `metavar`, `envvar`, `hidden`, and the
|
|
226
|
+
path validators below. Mixing them (e.g. `Opt` on a positional or `Arg` on a
|
|
227
|
+
keyword-only parameter) raises a clear `YeeterError`.
|
|
228
|
+
|
|
229
|
+
You can also define aliases once and reuse them:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from pathlib import Path
|
|
233
|
+
from typing import Annotated
|
|
234
|
+
from yeeter import Arg, Opt
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
type InputPath = Annotated[Path, Arg(help="Input file")]
|
|
238
|
+
type WorkerCount = Annotated[int, Opt(alias="-w", help="Worker count")]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def main(path: InputPath, *, workers: WorkerCount = 4) -> None:
|
|
242
|
+
...
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Environment variable fallback (`Opt(envvar=...)`)
|
|
248
|
+
|
|
249
|
+
`Opt(envvar="NAME")` falls back to an environment variable when the flag is
|
|
250
|
+
not provided on the CLI. Precedence: **explicit CLI > env var > default**.
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from typing import Annotated
|
|
254
|
+
from yeeter import Opt
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def main(*, workers: Annotated[int, Opt(envvar="WORKERS")] = 4) -> None:
|
|
258
|
+
...
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
WORKERS=8 yeet file.py # workers == 8
|
|
263
|
+
yeet file.py --workers 16 # workers == 16 (CLI wins)
|
|
264
|
+
yeet file.py # workers == 4 (default)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Env-var values are type-coerced just like CLI values. `bool` accepts
|
|
268
|
+
`1/0/true/false/yes/no` (case-insensitive). `list[T]` splits on `os.pathsep`
|
|
269
|
+
(`:` on POSIX, `;` on Windows). `Literal` choices are validated.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Hidden options (`Opt(hidden=True)`)
|
|
274
|
+
|
|
275
|
+
Hidden options still parse from the CLI but are absent from `--help` (both
|
|
276
|
+
the usage line and the options table):
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from typing import Annotated
|
|
280
|
+
from yeeter import Opt
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def main(*, debug: Annotated[bool, Opt(hidden=True)] = False) -> None:
|
|
284
|
+
...
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Path validators
|
|
290
|
+
|
|
291
|
+
`Arg` and `Opt` accept `exists`, `file_okay`, `dir_okay`, `readable`, and
|
|
292
|
+
`writable` for `Path` parameters. They run at parse time and fail with a
|
|
293
|
+
clear error:
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
from pathlib import Path
|
|
297
|
+
from typing import Annotated
|
|
298
|
+
from yeeter import Arg
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def main(
|
|
302
|
+
src: Annotated[Path, Arg(exists=True, dir_okay=False, readable=True)],
|
|
303
|
+
dst: Annotated[Path, Arg(writable=True)],
|
|
304
|
+
) -> None:
|
|
305
|
+
...
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Defaults mirror typer: `file_okay=True`, `dir_okay=True`, others off.
|
|
309
|
+
Setting any path-check on a non-`Path` parameter raises `YeeterError` at
|
|
310
|
+
parser-build time. Validators also apply to `list[Path]` and to
|
|
311
|
+
`*paths: Path`.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Variadic positional args (`*args`)
|
|
316
|
+
|
|
317
|
+
`*args` maps to a trailing variadic positional CLI argument. The annotation
|
|
318
|
+
on `*args` is the **element type** (not `list[T]`):
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
from pathlib import Path
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def main(dst: Path, *sources: Path) -> None:
|
|
325
|
+
...
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
yeet file.py dst src1 src2 src3
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
By default `*args` accepts zero or more values (argparse `nargs="*"`). Use
|
|
333
|
+
`Arg(min=1)` to require at least one:
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
from typing import Annotated
|
|
337
|
+
from yeeter import Arg
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def main(*sources: Annotated[Path, Arg(min=1, help="Source paths")]) -> None:
|
|
341
|
+
...
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Keyword-only options remain `--flags` after `*args`. `**kwargs` is not
|
|
345
|
+
supported.
|
|
346
|
+
|
|
347
|
+
**Why `Annotated`?** Python's type system only permits call expressions
|
|
348
|
+
(`Opt(...)`) inside the metadata slot of `Annotated`. No other syntax is
|
|
349
|
+
accepted by Pyright in strict mode. The `Annotated` form is verbose but is
|
|
350
|
+
the only way to attach per-parameter metadata that fully type-checks.
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Rules
|
|
355
|
+
|
|
356
|
+
- **Positional** parameters become positional CLI args.
|
|
357
|
+
- **Keyword-only** parameters (after `*`) become `--options`.
|
|
358
|
+
- Names convert from `snake_case` to `kebab-case` for CLI flags.
|
|
359
|
+
- `flag: bool = False` becomes `--flag`.
|
|
360
|
+
- `flag: bool = True` becomes `--no-flag`.
|
|
361
|
+
- Required `bool` parameters raise a clear error.
|
|
362
|
+
- `T | None` / `Optional[T]` are accepted; treated as their inner type with
|
|
363
|
+
`None` as default.
|
|
364
|
+
- `list[T]` becomes a repeated option (`--tag a --tag b`).
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Supported types
|
|
369
|
+
|
|
370
|
+
`str`, `int`, `float`, `bool`, `pathlib.Path`, `typing.Literal[...]`,
|
|
371
|
+
`T | None`, `list[T]`. Anything else raises a clear `YeeterError`.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Logging
|
|
376
|
+
|
|
377
|
+
By default, `yeeter.run` installs a Rich-based logging handler before
|
|
378
|
+
invoking your function, so you get formatted logs with zero boilerplate:
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
import logging
|
|
382
|
+
|
|
383
|
+
import yeeter
|
|
384
|
+
|
|
385
|
+
logger = logging.getLogger("app")
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def main(thing: int) -> None:
|
|
389
|
+
logger.info("thing = %s", thing)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
If your function has a `log_level` parameter (e.g.
|
|
393
|
+
`log_level: Literal["debug", "info", "warning", "error"] = "info"`), its
|
|
394
|
+
value drives the log level. Otherwise, the default is `INFO`.
|
|
395
|
+
|
|
396
|
+
Setup is idempotent: if the root logger already has handlers, yeeter does
|
|
397
|
+
not touch them. To take full control of logging yourself, opt out:
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
yeeter.run(main, should_setup_logging=False)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Testing
|
|
406
|
+
|
|
407
|
+
`run()` accepts an explicit `argv` for tests:
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
yeeter.run(main, argv=["5", "--n", "0.2"])
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## yeeter vs. typer
|
|
416
|
+
|
|
417
|
+
[Typer](https://github.com/fastapi/typer) is a mature, feature-rich CLI
|
|
418
|
+
framework and a direct inspiration for yeeter — the `Annotated[..., Arg/Opt]`
|
|
419
|
+
metadata pattern, path validators, and envvar fallback all take cues from
|
|
420
|
+
typer. yeeter is a much smaller library aimed at a narrower slice of the
|
|
421
|
+
problem. Quick honest comparison so you can pick the right tool:
|
|
422
|
+
|
|
423
|
+
| Topic | yeeter | typer |
|
|
424
|
+
| -------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
425
|
+
| Style | Plain function signature, no decorators | Decorators (`@app.command()`) or `typer.run` |
|
|
426
|
+
| Zero-boilerplate runner | `yeet main.py [func] [args...]` script — no `if __name__ == "__main__"` / `yeeter.run(...)` block needed | Always need a `typer.run(...)` call or a decorated `@app.command()` entry point |
|
|
427
|
+
| Executable shebang | `#!yeet` or `#!uv run yeet` can make the script itself executable without extra wrapper code | No equivalent single-line signature-driven runner; still need a `typer.run(...)` or app entry point |
|
|
428
|
+
| Arg vs. option mapping | Uses Python's `*` separator: before `*` = positional args, after `*` = `--options` (no per-param annotation needed) | Decide per parameter via `typer.Argument(...)` / `typer.Option(...)` |
|
|
429
|
+
| Per-param metadata | `Annotated[T, Arg(...)]` / `Annotated[T, Opt(...)]` | `Annotated[T, typer.Argument(...)]` / `typer.Option(...)` |
|
|
430
|
+
| Variadic positional args | Native `*args: T` maps to a trailing variadic positional arg | Use `list[T]` with `typer.Argument(...)` |
|
|
431
|
+
| Boolean flags | Default drives the flag: `= False` -> `--flag`, `= True` -> `--no-flag` | Pair of flags declared explicitly: `--flag / --no-flag` |
|
|
432
|
+
| Subcommands | Not supported (single command per script) | First-class subcommands, command groups, nested apps |
|
|
433
|
+
| Async functions | Native: `async def` is run via `asyncio.run` / `uvloop.run` | Not built-in; wrap with `asyncio.run(...)` yourself |
|
|
434
|
+
| Shell completion | Not built-in | Built-in (bash/zsh/fish/PowerShell) |
|
|
435
|
+
| Help rendering | Rich tables for args and options | Rich-formatted help via `rich` |
|
|
436
|
+
| Type-checker friendliness | Designed to be Pyright-strict clean end-to-end | Some patterns require `# type: ignore` under strict settings |
|
|
437
|
+
| Logging | Rich logging set up by default (opt-out) | Not opinionated about logging |
|
|
438
|
+
| Dependencies | `rich`, `rich-argparse` (small footprint) | `click`, `rich`, `shellingham`, `typing-extensions` |
|
|
439
|
+
| Maturity / ecosystem | New and small | Widely adopted, large ecosystem |
|
|
440
|
+
| Best for | Single-purpose scripts and tools where the function *is* the CLI | Multi-command CLIs, distributed apps, anything needing completion |
|
|
441
|
+
|
|
442
|
+
If you need subcommands or shell completion, use typer. If you want one
|
|
443
|
+
function = one CLI with minimal ceremony and strict typing, yeeter is
|
|
444
|
+
designed for that.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Releases
|
|
449
|
+
|
|
450
|
+
`yeeter` uses CalVer based on the release date. Versions are published in
|
|
451
|
+
PEP 440 canonical form as `YYYY.M.D`, so a release on 2026-05-21 is
|
|
452
|
+
`2026.5.21`; multiple releases on the same day use `.postN`, for example
|
|
453
|
+
`2026.5.21.post1`.
|
|
454
|
+
|
|
455
|
+
Run `task release` to create the `release/{TAG}` PR, then merge it.
|
|
456
|
+
Then create and push the matching release tag. GitHub Actions validates
|
|
457
|
+
the tag, creates the GitHub Release, and a separate workflow deploys
|
|
458
|
+
docs.
|
|
459
|
+
|
|
460
|
+
If you need to bypass the PR flow, run `task release-direct`. That bumps
|
|
461
|
+
the version on `main`, runs `task deps-lock`, commits, pushes `main`,
|
|
462
|
+
creates the matching tag, and pushes the tag.
|
|
463
|
+
|
|
464
|
+
To bump a release version manually, run `uv version <version>`.
|
|
465
|
+
|
|
466
|
+
Install from PyPI with:
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
pip install yeetr
|
|
470
|
+
```
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## Development
|
|
474
|
+
|
|
475
|
+
```
|
|
476
|
+
uv sync
|
|
477
|
+
uv run ruff check
|
|
478
|
+
uv run pyright
|
|
479
|
+
uv run pytest
|
|
480
|
+
```
|