errpath 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.
@@ -0,0 +1,622 @@
1
+ Metadata-Version: 2.5
2
+ Name: errpath
3
+ Version: 0.1.0
4
+ Summary: Result types for Python. See every error path.
5
+ Project-URL: Homepage, https://github.com/jagreehal/errpath-python
6
+ Project-URL: Documentation, https://github.com/jagreehal/errpath-python#readme
7
+ Project-URL: Repository, https://github.com/jagreehal/errpath-python
8
+ Project-URL: Issues, https://github.com/jagreehal/errpath-python/issues
9
+ Author-email: Jag Reehal <jag@jagreehal.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: error-handling,machinejson,railway,result,static-analysis,xstate
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+
26
+ # errpath
27
+
28
+ **Result types for Python. See every error path.**
29
+
30
+ errpath lets you:
31
+
32
+ - return errors as values (`Ok` / `Err`), so your type checker knows every way a function can fail
33
+ - keep the happy path readable: `@safe` stops at the first failure, with no `if` after every call
34
+ - draw every error path of a function as a diagram, straight from your code
35
+ - catch risky changes in pull requests: new crash points, ignored errors, changed error paths
36
+
37
+ No dependencies. Python 3.11+.
38
+
39
+ ```bash
40
+ uv add errpath # or: pip install errpath
41
+ ```
42
+
43
+ ---
44
+
45
+ ## The problem
46
+
47
+ A Python signature can't say what a function raises:
48
+
49
+ ```python
50
+ def get_user(user_id: str) -> User: ... # raises UserNotFound? DbError? Nothing says.
51
+
52
+
53
+ try:
54
+ user = get_user("u-2")
55
+ except Exception as e: # which errors? the signature doesn't say
56
+ ...
57
+ ```
58
+
59
+ Your type checker can't see a `raise`, so callers guess, catch everything, or forget.
60
+
61
+ errpath puts the failure in the signature.
62
+
63
+ ---
64
+
65
+ ## Return errors as values
66
+
67
+ ```python
68
+ from dataclasses import dataclass
69
+
70
+ from errpath import Err, Ok, Result, ok
71
+
72
+
73
+ @dataclass
74
+ class User:
75
+ id: str
76
+ name: str
77
+
78
+
79
+ class UserNotFound(Exception):
80
+ pass
81
+
82
+
83
+ def get_user(user_id: str) -> Result[User, UserNotFound]:
84
+ if user_id == "u-1":
85
+ return Ok(User(user_id, "Alice"))
86
+ return Err(UserNotFound(user_id))
87
+
88
+
89
+ result = get_user("u-2")
90
+ if ok(result):
91
+ print(result.value.name) # your editor knows this is a User
92
+ else:
93
+ print(f"no such user: {result.error}") # ...and this is a UserNotFound
94
+ ```
95
+
96
+ The failure is part of the return type, so you can't use the value without checking first.
97
+
98
+ `match` works too:
99
+
100
+ ```python
101
+ match get_user("u-1"):
102
+ case Ok(user):
103
+ print(user.name)
104
+ case Err(error):
105
+ print(f"no such user: {error}")
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Many steps: an `if` after every call
111
+
112
+ Real work is several steps that can each fail. Checking every result by hand gets noisy:
113
+
114
+ ```python
115
+ def place_order(cart: Cart, card: Card) -> Result[Receipt, PriceError | CardDeclined | DbError]:
116
+ total = price(cart)
117
+ if not ok(total):
118
+ return total
119
+ payment = charge(card, total.value)
120
+ if not ok(payment):
121
+ return payment
122
+ return save_order(cart, payment.value)
123
+ ```
124
+
125
+ Ten steps would mean ten checks. That's what `@safe` is for.
126
+
127
+ ---
128
+
129
+ ## `@safe`: write the happy path, stop at the first failure
130
+
131
+ ```python
132
+ from errpath import safe_with
133
+
134
+
135
+ @safe_with(PriceError, CardDeclined, DbError)
136
+ def place_order(cart: Cart, card: Card) -> Receipt:
137
+ total = price(cart).unwrap() # an Err stops here...
138
+ payment = charge(card, total).unwrap() # ...or here...
139
+ return save_order(cart, payment).unwrap() # ...or here
140
+
141
+
142
+ result = place_order(cart, card) # Result[Receipt, PriceError | CardDeclined | DbError]
143
+ ```
144
+
145
+ `.unwrap()` gives you the value of an `Ok`, and raises the error of an `Err`. The decorator catches it and returns it as `Err`, so the function reads top to bottom and still returns a `Result`.
146
+
147
+ - `@safe_with(A, B)` catches only those errors, and your editor knows the result is `Result[T, A | B]`. Anything else still raises, as a bug should.
148
+ - `@safe` catches any `Exception`, bugs included: a typo's `KeyError` comes back as an `Err` instead of crashing. Use it deliberately, as a catch-all at an outer boundary or around code you don't own. Prefer `@safe_with`.
149
+ - `@safe_async` and `@safe_async_with` do the same for `async def`. They never catch `asyncio.CancelledError`.
150
+
151
+ It also wraps existing code that raises:
152
+
153
+ ```python
154
+ import json
155
+
156
+ from errpath import safe_with
157
+
158
+
159
+ @safe_with(json.JSONDecodeError)
160
+ def parse(text: str) -> dict:
161
+ return json.loads(text)
162
+
163
+
164
+ parse("{oops") # Err(JSONDecodeError(...)), not a crash
165
+ ```
166
+
167
+ ---
168
+
169
+ ## See every way it can fail
170
+
171
+ Here's a fuller checkout ([examples/checkout.py](examples/checkout.py)):
172
+
173
+ ```python
174
+ @safe
175
+ def place_order(cart: Cart, card: Card) -> Receipt:
176
+ total = price(cart).unwrap()
177
+ if cart.has_coupon:
178
+ validate_coupon(cart).unwrap()
179
+ payment = charge(card, total).unwrap()
180
+ receipt = save_order(cart, payment).unwrap()
181
+ send_confirmation(receipt).unwrap()
182
+ return receipt
183
+ ```
184
+
185
+ ```bash
186
+ errpath diagram examples/checkout.py place_order
187
+ ```
188
+
189
+ prints this diagram, which GitHub draws for you in any README, issue or PR:
190
+
191
+ ```mermaid
192
+ flowchart LR
193
+ Start(["place_order"])
194
+ Done((Success))
195
+ S1["send_confirmation"]
196
+ S1 -->|ok| Done
197
+ E2[/"EmailError"/]
198
+ S1 -->|err| E2
199
+ S3["save_order"]
200
+ S3 -->|ok| S1
201
+ E4[/"DbError"/]
202
+ S3 -->|err| E4
203
+ S5["charge"]
204
+ S5 -->|ok| S3
205
+ E6[/"CardDeclined"/]
206
+ S5 -->|err| E6
207
+ D7{"if cart.has_coupon"}
208
+ S8["validate_coupon"]
209
+ S8 -->|ok| S5
210
+ E9[/"CouponError"/]
211
+ S8 -->|err| E9
212
+ D7 -->|yes| S8
213
+ D7 -->|no| S5
214
+ S10["price"]
215
+ S10 -->|ok| D7
216
+ E11[/"PriceError"/]
217
+ S10 -->|err| E11
218
+ Start --> S10
219
+ classDef error fill:#fdecea,stroke:#d93025,color:#a50e0e
220
+ class E2,E4,E6,E9,E11 error
221
+ ```
222
+
223
+ Every red box is a way `place_order` can fail. errpath reads them from the `-> Result[T, E]` annotations of the functions it calls.
224
+
225
+ It follows branches, loops, `try` / `except` / `finally` and `raise` (see [what the analyzer follows](#what-the-analyzer-follows)), and tells you about anything it can't.
226
+
227
+ For an interactive version, `errpath machine examples/checkout.py place_order` prints an [XState](https://stately.ai/docs/xstate) state machine you can open in [autovisualizer](https://auto-visualizer.vercel.app).
228
+
229
+ ---
230
+
231
+ ## Catch risky changes in pull requests
232
+
233
+ Say a PR changes one line:
234
+
235
+ ```diff
236
+ - send_confirmation(receipt).unwrap()
237
+ + _ = send_confirmation(receipt)
238
+ ```
239
+
240
+ The tests still pass. But a failed confirmation email now disappears without a trace. `errpath review` compares the error paths before and after, and posts:
241
+
242
+ > ## Error path review: high risk
243
+ >
244
+ > 1 function(s) changed, 1 new finding(s), risk=high
245
+ >
246
+ > ### New findings
247
+ >
248
+ > - `no-swallow` `_ =` drops the error from `send_confirmation` at `shop/checkout.py:72`
249
+ >
250
+ > ### `shop.checkout.place_order`
251
+ >
252
+ > - added swallow `send_confirmation`
253
+ > - removed step `send_confirmation`
254
+ >
255
+ > ▶ Diagram
256
+
257
+ What it flags:
258
+
259
+ | Finding | Example | Why it matters |
260
+ | --- | --- | --- |
261
+ | New crash point (`no-panic`) | `.unwrap()` outside `@safe` | raises instead of returning `Err` |
262
+ | Ignored error (`no-swallow`) | `_ = charge(card)`, or `charge(card)` on its own line | the failure vanishes |
263
+ | Changed error path | a step added, removed or moved out of a `try` | callers can now see different errors |
264
+
265
+ Risk is `low`, `moderate` or `high`.
266
+
267
+ ### GitHub Action
268
+
269
+ ```yaml
270
+ name: errpath
271
+ on: pull_request
272
+
273
+ permissions:
274
+ contents: read
275
+ pull-requests: write # to post the review comment
276
+
277
+ jobs:
278
+ review:
279
+ runs-on: ubuntu-latest
280
+ steps:
281
+ - uses: actions/checkout@v7
282
+ - uses: jagreehal/errpath-python@v0
283
+ with:
284
+ fail-on: high # never, moderate or high
285
+ ```
286
+
287
+ It keeps one comment up to date per PR, annotates new findings inline, and writes the report to the job summary. Run it locally with `errpath review --base main`.
288
+
289
+ ---
290
+
291
+ ## Turn errors into your own at the boundary
292
+
293
+ Low-level errors (`KeyError`, `DbError`) shouldn't leak out of your module. `map_err` changes the error and leaves a success alone:
294
+
295
+ ```python
296
+ order = find_item(sku).map_err(lambda e: OrderError(f"unknown item {sku}"))
297
+ ```
298
+
299
+ ---
300
+
301
+ ## With Pydantic and pydantic-ai
302
+
303
+ ### Validation
304
+
305
+ Pydantic raises `ValidationError` for bad input. `@safe_with` turns that into a value, with Pydantic's field errors intact:
306
+
307
+ ```python
308
+ from pydantic import BaseModel, ValidationError
309
+
310
+ from errpath import safe_with
311
+
312
+
313
+ class User(BaseModel):
314
+ name: str
315
+
316
+
317
+ @safe_with(ValidationError)
318
+ def parse_user(data: object) -> User:
319
+ return User.model_validate(data)
320
+
321
+
322
+ parse_user({"name": "Ada"}) # Ok(User(name='Ada'))
323
+ parse_user({}) # Err(ValidationError(...)): "name: Field required"
324
+ ```
325
+
326
+ Then chain it with other steps that can fail: `parse_user(data).and_then(save_user)`.
327
+
328
+ ### Results inside models
329
+
330
+ A `Result` can be a field of a Pydantic model. Both sides are validated, and the JSON says which one it is:
331
+
332
+ ```python
333
+ class Response(BaseModel):
334
+ result: Result[User, str]
335
+
336
+
337
+ Response(result=Ok(User(name="Ada"))).model_dump_json()
338
+ # {"result": {"ok": true, "value": {"name": "Ada"}}}
339
+
340
+ Response.model_validate_json('{"result": {"ok": false, "error": "not found"}}')
341
+ # Response(result=Err('not found'))
342
+
343
+ Response(result=Ok("not a User")) # ValidationError
344
+ ```
345
+
346
+ It also appears in `model_json_schema()`, so FastAPI and other API tools document it.
347
+
348
+ For results that travel through APIs, use a model, `str` or enum as the error type. An exception error is written as `{"type": "KeyError", "message": "..."}` but can't be read back, because an exception isn't data.
349
+
350
+ A `str` or model error can't be raised, so `.unwrap()` isn't available on `Result[User, str]`: your type checker flags it. Use `match`, `.unwrap_or(default)`, or turn the error into an exception first with `.map_err(NotFound).unwrap()`.
351
+
352
+ ### pydantic-ai
353
+
354
+ Two patterns, both in [examples/pydantic_ai_agent.py](examples/pydantic_ai_agent.py):
355
+
356
+ ```python
357
+ @agent.tool_plain(retries=2)
358
+ def price_of(sku: str) -> int:
359
+ result = find_price(sku) # your code: Result[int, UnknownItem]
360
+ if not ok(result):
361
+ raise ModelRetry(f"unknown item {sku}") # the model reads this and tries again
362
+ return result.value
363
+
364
+
365
+ @safe_async_with(UsageLimitExceeded, UnexpectedModelBehavior)
366
+ async def ask(prompt: str) -> str: # Result[str, UsageLimitExceeded | UnexpectedModelBehavior]
367
+ return (await agent.run(prompt)).output
368
+ ```
369
+
370
+ 1. **In a tool**, turn an `Err` into `ModelRetry`. Don't return `Ok`/`Err` from a tool: pydantic-ai can't send them to the model.
371
+ 2. **Around `agent.run`**, `@safe_async_with` gives your caller a typed `Result` instead of exceptions to remember.
372
+
373
+ The example runs offline with a scripted model. To try it with a local model:
374
+
375
+ ```bash
376
+ OLLAMA_BASE_URL=http://localhost:11434/v1 ERRPATH_MODEL=ollama:qwen3.5:4b \
377
+ uv run python examples/pydantic_ai_agent.py
378
+ ```
379
+
380
+ ---
381
+
382
+ ## Putting it together
383
+
384
+ One file: validate input, run domain steps, handle every error, see the paths. Copy it, or run it from [examples/signup.py](examples/signup.py):
385
+
386
+ ```python
387
+ """errpath end to end: validate input, run domain steps, handle every error, see the paths.
388
+
389
+ Run it: uv run python examples/signup.py
390
+ See every path: uv run errpath diagram examples/signup.py register
391
+ """
392
+
393
+ from collections.abc import Mapping
394
+ from dataclasses import dataclass
395
+
396
+ from pydantic import BaseModel, Field, ValidationError
397
+
398
+ from errpath import Err, Ok, Result, safe_with
399
+
400
+ # 1. Validate input. Pydantic's ValidationError becomes an Err, with its field errors.
401
+
402
+
403
+ class SignupForm(BaseModel):
404
+ email: str = Field(pattern=r"^[^@\s]+@[^@\s]+$")
405
+ password: str = Field(min_length=8)
406
+
407
+
408
+ @safe_with(ValidationError)
409
+ def parse_form(data: Mapping[str, object]) -> SignupForm:
410
+ return SignupForm.model_validate(data)
411
+
412
+
413
+ # 2. Domain steps return Results: how each one can fail is in its signature.
414
+
415
+
416
+ class EmailTaken(Exception):
417
+ pass
418
+
419
+
420
+ class MailerDown(Exception):
421
+ pass
422
+
423
+
424
+ @dataclass
425
+ class Account:
426
+ id: int
427
+ email: str
428
+
429
+
430
+ registered = {"taken@example.com"}
431
+
432
+
433
+ def reserve_email(email: str) -> Result[str, EmailTaken]:
434
+ if email in registered:
435
+ return Err(EmailTaken(email))
436
+ registered.add(email)
437
+ return Ok(email)
438
+
439
+
440
+ def send_welcome(account: Account) -> Result[None, MailerDown]:
441
+ if account.email.endswith("@offline.example"):
442
+ return Err(MailerDown("mail server unreachable"))
443
+ return Ok(None)
444
+
445
+
446
+ # 3. Compose: the happy path reads top to bottom; the first Err stops it.
447
+
448
+
449
+ @safe_with(ValidationError, EmailTaken, MailerDown)
450
+ def register(data: Mapping[str, object]) -> Account:
451
+ form = parse_form(data).unwrap()
452
+ email = reserve_email(form.email).unwrap()
453
+ account = Account(id=len(registered), email=email)
454
+ send_welcome(account).unwrap()
455
+ return account
456
+
457
+
458
+ # 4. Handle every error at the boundary, e.g. as an HTTP response.
459
+
460
+
461
+ def handle(data: Mapping[str, object]) -> tuple[int, object]:
462
+ match register(data):
463
+ case Ok(account):
464
+ return 201, {"id": account.id}
465
+ case Err(ValidationError() as error):
466
+ return 422, [e["msg"] for e in error.errors()]
467
+ case Err(EmailTaken()):
468
+ return 409, "email already registered"
469
+ case Err(MailerDown()):
470
+ return 503, "try again later"
471
+ case Err(error):
472
+ raise AssertionError(f"unhandled error: {error!r}")
473
+
474
+
475
+ if __name__ == "__main__":
476
+ for data in [
477
+ {"email": "ada@example.com", "password": "correct horse"},
478
+ {"email": "not-an-email", "password": "short"},
479
+ {"email": "taken@example.com", "password": "correct horse"},
480
+ {"email": "bob@offline.example", "password": "correct horse"},
481
+ ]:
482
+ print(data["email"], "->", *handle(data))
483
+ ```
484
+
485
+ ```bash
486
+ uv run python examples/signup.py
487
+ # ada@example.com -> 201 {'id': 2}
488
+ # not-an-email -> 422 [...Pydantic's field errors...]
489
+ # taken@example.com -> 409 email already registered
490
+ # bob@offline.example -> 503 try again later
491
+
492
+ uv run errpath diagram examples/signup.py register
493
+ ```
494
+
495
+ ```mermaid
496
+ flowchart LR
497
+ Start(["register"])
498
+ Done((Success))
499
+ S1["send_welcome"]
500
+ S1 -->|ok| Done
501
+ E2[/"MailerDown"/]
502
+ S1 -->|err| E2
503
+ S3["reserve_email"]
504
+ S3 -->|ok| S1
505
+ E4[/"EmailTaken"/]
506
+ S3 -->|err| E4
507
+ S5["parse_form"]
508
+ S5 -->|ok| S3
509
+ E6[/"ValidationError"/]
510
+ S5 -->|err| E6
511
+ Start --> S5
512
+ classDef error fill:#fdecea,stroke:#d93025,color:#a50e0e
513
+ class E2,E4,E6 error
514
+ ```
515
+
516
+ Every error in `register`'s signature has a box, and `handle` has a `case` for each one.
517
+
518
+ ---
519
+
520
+ ## What the analyzer follows
521
+
522
+ errpath reads your code without running it. It draws:
523
+
524
+ - calls to functions that return `Result`, whether annotated `-> Result[T, E]` or decorated with `@safe` / `@safe_with(...)`
525
+ - `.unwrap()`, `return Ok(...)` / `return Err(...)` (including `x if cond else y`), and `raise`
526
+ - `if` / `elif` / `else`, `match`, `for` and `while` (with `break` / `continue`), `with`, and `await`
527
+ - `try` / `except` / `else` / `finally`, including `except*`: each error goes to the handler that would catch it, through any `finally`
528
+
529
+ When it sees a `Result` it can't place on the diagram, such as one inside a condition, a comprehension or another call's arguments, it says so instead of quietly drawing less:
530
+
531
+ ```text
532
+ errpath: note: shop.py:14: Result used inside an expression; its error path isn't drawn: check(email)
533
+ ```
534
+
535
+ `errpath diagram` and `errpath machine` print these notes, `errpath review` lists them (marking new ones), and `errpath ir` includes them. An `.unwrap()` in such a spot outside `@safe` is still flagged as a crash point, unless it's inside a lambda: errpath can't tell if or when a lambda runs, so that gets a note only.
536
+
537
+ It doesn't see calls made through a variable or callback (`step = check; step(x)`), or a module function called as a method on a local object (`client.charge()`).
538
+
539
+ ---
540
+
541
+ ## Quick reference
542
+
543
+ ### Methods
544
+
545
+ | Method | On `Ok(value)` | On `Err(error)` |
546
+ | --- | --- | --- |
547
+ | `.unwrap()` | returns `value` | raises `error` |
548
+ | `.unwrap_or(default)` | returns `value` | returns `default` |
549
+ | `.map(f)` | `Ok(f(value))` | unchanged |
550
+ | `.map_err(f)` | unchanged | `Err(f(error))` |
551
+ | `.and_then(f)` | `f(value)`, a step that can fail | unchanged |
552
+
553
+ ### Helpers
554
+
555
+ | Helper | Does |
556
+ | --- | --- |
557
+ | `ok(r)` | `True` for `Ok`; your editor then knows the types of `r.value` and `r.error` |
558
+ | `err_type(r, KeyError)` | `True` if `r` failed with a `KeyError` (or a subclass) |
559
+ | `traceback_of(r)` | the error's stack trace as text, for logging |
560
+
561
+ ### Decorators
562
+
563
+ | Decorator | Catches | Returns |
564
+ | --- | --- | --- |
565
+ | `@safe` | any `Exception` | `Result[T, Exception]` |
566
+ | `@safe_with(A, B)` | only `A` and `B` | `Result[T, A \| B]` |
567
+ | `@safe_async`, `@safe_async_with(A, B)` | the same, for `async def` | the same, awaited |
568
+
569
+ ### CLI
570
+
571
+ | Command | Prints |
572
+ | --- | --- |
573
+ | `errpath diagram <path> <function>` | Mermaid diagram |
574
+ | `errpath machine <path> <function>` | XState state machine (JSON) |
575
+ | `errpath review --base <ref>` | PR review (Markdown, or JSON with `--format json`) |
576
+ | `errpath ir [path]` | everything errpath found, as JSON |
577
+
578
+ ---
579
+
580
+ ## Common pitfalls
581
+
582
+ - **`r.is_ok()` doesn't tell your editor the types.** Python's type checkers can't narrow through a method. Use `ok(r)` or `match`.
583
+ - **`.unwrap()` outside `@safe` raises.** That's fine in a script, but `errpath review` flags it as a new crash point.
584
+ - **`@safe` also catches your bugs.** A `KeyError` from a typo becomes an `Err` like any expected failure. Name the errors you expect with `@safe_with(...)`, and keep `@safe` for a deliberate catch-all.
585
+ - **`.unwrap()` is for exception errors.** Python can't raise a `str` or a model, so type checkers reject `.unwrap()` on `Result[int, str]`. (Unchecked code gets a `RuntimeError` saying what to do.) Use `match`, `.unwrap_or(default)`, or `.map_err(SomeError).unwrap()`.
586
+ - **Don't drop a result.** `_ = charge(card)` and a bare `charge(card)` both throw the error away.
587
+ - **Annotate what can fail.** errpath names errors from `-> Result[T, E]` annotations. Without one, a step's error shows as `Error`.
588
+ - **Results can't be changed.** Build a new one instead: `Ok(new_value)`.
589
+
590
+ ---
591
+
592
+ ## When to use errpath
593
+
594
+ **Use it when:**
595
+
596
+ - you want the ways a function can fail to be part of its signature
597
+ - you want to see, and review, how errors flow through your code
598
+
599
+ **Skip it when:**
600
+
601
+ - exceptions are fine for you, as in most small scripts
602
+ - you want a full functional-programming toolkit (`Maybe`, `IO`, pipelines): look at [returns](https://github.com/dry-python/returns)
603
+
604
+ errpath's `Result` is deliberately small, and inspired by [safe-result](https://github.com/overflowy/safe-result). What it adds is the analysis: diagrams and PR reviews of your error paths.
605
+
606
+ ---
607
+
608
+ ## Development
609
+
610
+ Uses [uv](https://docs.astral.sh/uv/), [ruff](https://docs.astral.sh/ruff/) and [ty](https://docs.astral.sh/ty/).
611
+
612
+ ```bash
613
+ uv sync # install
614
+ uv run pytest # test
615
+ uv run ruff check # lint
616
+ uv run ruff format # format
617
+ uv run ty check # type check
618
+ ```
619
+
620
+ ## License
621
+
622
+ MIT
@@ -0,0 +1,17 @@
1
+ errpath/__init__.py,sha256=WvbqGhilCQf00Ej7oSA1D_dhSn27mHggc8Tb6ZVWejM,393
2
+ errpath/__main__.py,sha256=nGlnYwHtgl1xY2CtdaZYgWUQfhIFVkQUeaXfVb91hGY,68
3
+ errpath/_pydantic.py,sha256=hLw3RdvHQzb2Ks6yXefCi4oqn_PsrWO6BwsiWUbUfIk,4125
4
+ errpath/analyse.py,sha256=2WPk11se6AEUYmQFiZqxMJuW1MfSmt5wwKsr2YXnZbc,38164
5
+ errpath/cli.py,sha256=K462V4FLyn2VGcPB9bVNylhjrbyNW9wh_doao0K01yg,4868
6
+ errpath/diagram.py,sha256=N-_KM-vs-8gcDaeDsS7ht2_fJK7yEaofMWws61EIfBQ,4740
7
+ errpath/ir.py,sha256=XyFnpn-27t1EaTm-GnAdVuZHbh1zaeQ6-cHEJ3LEo3k,6337
8
+ errpath/machine.py,sha256=Wxp1UAmvJ0_JZ3KdgA9_pTbdWTYFLjLldE6rH3enX_8,5749
9
+ errpath/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ errpath/result.py,sha256=ScsClrHIUCHaDr8EudTiV4ySeJDp5FxdCl4sCnMchow,9146
11
+ errpath/review.py,sha256=kH10910Ie_KcakQ3AaIR9wTkQoZ0D3UXT6qlfBZuhnU,16972
12
+ errpath/safe.py,sha256=e1BUoDjkKDqYAA0DONujkRN95e47Q-QcL2DlZDduxqs,4065
13
+ errpath-0.1.0.dist-info/METADATA,sha256=2bvmnW_jFWl4MDt8l9mVZdpeNhNFi1YPT8Ov9977Fiw,19256
14
+ errpath-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
15
+ errpath-0.1.0.dist-info/entry_points.txt,sha256=ydvR_UyPIjdHr6KAhkdQ84kB7ABdFlDObTBzWEAMSc0,45
16
+ errpath-0.1.0.dist-info/licenses/LICENSE,sha256=JM6lNjZlRemKaGG6uUIlEuk38DJ2HpF_Je0h-utBxvc,1067
17
+ errpath-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ errpath = errpath.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jag Reehal
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.