vcti-error-core 1.0.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.
- vcti/error/core/__init__.py +25 -0
- vcti/error/core/codes.py +113 -0
- vcti/error/core/errors.py +33 -0
- vcti/error/core/py.typed +0 -0
- vcti/error/core/system_error.py +49 -0
- vcti_error_core-1.0.0.dist-info/METADATA +132 -0
- vcti_error_core-1.0.0.dist-info/RECORD +10 -0
- vcti_error_core-1.0.0.dist-info/WHEEL +5 -0
- vcti_error_core-1.0.0.dist-info/licenses/LICENSE +8 -0
- vcti_error_core-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""vcti.error.core — exit-code mechanics: validate, combine, and resolve code mappings."""
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
|
|
7
|
+
from .codes import MAX_EXIT_CODE, ErrorCodes, combine, error_code, validate
|
|
8
|
+
from .errors import CodeCollisionError, CodeRangeError, ErrorCodesError
|
|
9
|
+
from .system_error import EXCEPTION_ERRNO_MAPPING, system_error
|
|
10
|
+
|
|
11
|
+
__version__ = version("vcti-error-core")
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"EXCEPTION_ERRNO_MAPPING",
|
|
15
|
+
"MAX_EXIT_CODE",
|
|
16
|
+
"CodeCollisionError",
|
|
17
|
+
"CodeRangeError",
|
|
18
|
+
"ErrorCodes",
|
|
19
|
+
"ErrorCodesError",
|
|
20
|
+
"combine",
|
|
21
|
+
"error_code",
|
|
22
|
+
"system_error",
|
|
23
|
+
"validate",
|
|
24
|
+
"__version__",
|
|
25
|
+
]
|
vcti/error/core/codes.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Pure functions over exception-to-exit-code mappings.
|
|
4
|
+
|
|
5
|
+
A *contract* is nothing but a read-only ``Mapping[type[BaseException],
|
|
6
|
+
int]`` owned by whoever defines it — typically a contract package that
|
|
7
|
+
exports the mapping and calls :func:`validate` at import time. This
|
|
8
|
+
module holds no state and pre-registers nothing; applications compose
|
|
9
|
+
contracts explicitly with :func:`combine` and resolve exceptions with
|
|
10
|
+
:func:`error_code`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
from types import MappingProxyType
|
|
17
|
+
|
|
18
|
+
from .errors import CodeCollisionError, CodeRangeError
|
|
19
|
+
|
|
20
|
+
type ErrorCodes = Mapping[type[BaseException], int]
|
|
21
|
+
"""An exception-to-exit-code contract: class -> code (1..255)."""
|
|
22
|
+
|
|
23
|
+
#: Highest exit code that survives POSIX truncation (codes are taken
|
|
24
|
+
#: modulo 256; 0 is reserved for success).
|
|
25
|
+
MAX_EXIT_CODE = 255
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate(codes: ErrorCodes, *, claim: range | None = None) -> None:
|
|
29
|
+
"""Check that ``codes`` is a well-formed exit-code contract.
|
|
30
|
+
|
|
31
|
+
Contract packages call this at import time so a malformed contract
|
|
32
|
+
fails at startup, not in an error path.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
codes: The contract mapping to check.
|
|
36
|
+
claim: The code range this contract claims (e.g.
|
|
37
|
+
``range(32, 64)``). When given, every code must fall inside
|
|
38
|
+
it — the mechanism that keeps separately-owned contracts
|
|
39
|
+
collision-free by allocation rather than by luck.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
TypeError: If a key is not an exception class or a code is not
|
|
43
|
+
an ``int`` (``bool`` is rejected explicitly — ``True`` is
|
|
44
|
+
almost certainly a bug, not exit code 1).
|
|
45
|
+
CodeRangeError: If a code is outside ``1..255`` or outside
|
|
46
|
+
``claim``.
|
|
47
|
+
"""
|
|
48
|
+
for cls, code in codes.items():
|
|
49
|
+
if not (isinstance(cls, type) and issubclass(cls, BaseException)):
|
|
50
|
+
raise TypeError(f"contract key {cls!r} is not an exception class")
|
|
51
|
+
if isinstance(code, bool) or not isinstance(code, int):
|
|
52
|
+
raise TypeError(f"exit code for {cls.__name__} must be an int, got {code!r}")
|
|
53
|
+
if not 1 <= code <= MAX_EXIT_CODE:
|
|
54
|
+
raise CodeRangeError(
|
|
55
|
+
f"exit code {code} for {cls.__name__} is outside 1..{MAX_EXIT_CODE} "
|
|
56
|
+
f"(0 is reserved for success; POSIX truncates codes modulo 256)"
|
|
57
|
+
)
|
|
58
|
+
if claim is not None and code not in claim:
|
|
59
|
+
raise CodeRangeError(
|
|
60
|
+
f"exit code {code} for {cls.__name__} is outside the claimed "
|
|
61
|
+
f"range {claim.start}..{claim.stop - 1}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def combine(*sets: ErrorCodes) -> ErrorCodes:
|
|
66
|
+
"""Merge contract mappings into one read-only contract.
|
|
67
|
+
|
|
68
|
+
Composition is an application-startup act: an app combines the
|
|
69
|
+
contracts it speaks (its platform's, its own) into the single
|
|
70
|
+
mapping it resolves against. Later sets may repeat earlier entries
|
|
71
|
+
verbatim; they may not disagree.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
A read-only merged mapping.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
CodeCollisionError: If the same exception class is mapped to
|
|
78
|
+
two different codes.
|
|
79
|
+
"""
|
|
80
|
+
merged: dict[type[BaseException], int] = {}
|
|
81
|
+
for codes in sets:
|
|
82
|
+
for cls, code in codes.items():
|
|
83
|
+
existing = merged.get(cls)
|
|
84
|
+
if existing is not None and existing != code:
|
|
85
|
+
raise CodeCollisionError(f"{cls.__name__} is mapped to both {existing} and {code}")
|
|
86
|
+
merged[cls] = code
|
|
87
|
+
return MappingProxyType(merged)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def error_code(
|
|
91
|
+
exc: BaseException | type[BaseException],
|
|
92
|
+
codes: ErrorCodes,
|
|
93
|
+
*,
|
|
94
|
+
default: int,
|
|
95
|
+
) -> int:
|
|
96
|
+
"""Resolve an exception (instance or class) to its exit code.
|
|
97
|
+
|
|
98
|
+
Resolution is MRO-aware: the nearest mapped ancestor wins, so a
|
|
99
|
+
subclass of a mapped exception inherits its category instead of
|
|
100
|
+
falling through to ``default``.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
exc: The exception instance or class to classify.
|
|
104
|
+
codes: The contract to resolve against.
|
|
105
|
+
default: Returned when no ancestor is mapped. Required — what
|
|
106
|
+
"unspecified" means is contract vocabulary, and the
|
|
107
|
+
mechanics have no opinion about it.
|
|
108
|
+
"""
|
|
109
|
+
cls = exc if isinstance(exc, type) else type(exc)
|
|
110
|
+
for ancestor in cls.__mro__:
|
|
111
|
+
if ancestor in codes:
|
|
112
|
+
return codes[ancestor]
|
|
113
|
+
return default
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Exceptions raised by the exit-code mechanics.
|
|
4
|
+
|
|
5
|
+
Catch :class:`ErrorCodesError` to handle any mechanics failure
|
|
6
|
+
generically. Each leaf also inherits the closest stdlib exception so
|
|
7
|
+
existing ``except ValueError`` call sites keep working.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ErrorCodesError(Exception):
|
|
14
|
+
"""Base class for all vcti-error-core errors."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CodeRangeError(ErrorCodesError, ValueError):
|
|
18
|
+
"""An exit code is outside the valid or claimed range.
|
|
19
|
+
|
|
20
|
+
Valid codes are 1..255: 0 is reserved for success, and POSIX
|
|
21
|
+
truncates exit codes modulo 256, so anything above 255 would
|
|
22
|
+
silently alias a smaller code. When a contract declares a claimed
|
|
23
|
+
range, every code must also fall inside it.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CodeCollisionError(ErrorCodesError, ValueError):
|
|
28
|
+
"""The same exception class is mapped to two different exit codes.
|
|
29
|
+
|
|
30
|
+
Raised by ``combine()`` when contract mappings disagree about a
|
|
31
|
+
class. Several classes sharing one code is legal — codes are
|
|
32
|
+
categories — but one class must resolve to exactly one code.
|
|
33
|
+
"""
|
vcti/error/core/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
|
|
2
|
+
# See LICENSE for details.
|
|
3
|
+
"""Errno-correct construction of OS-level exceptions.
|
|
4
|
+
|
|
5
|
+
Python's OS exceptions (``FileNotFoundError``, ``IsADirectoryError``,
|
|
6
|
+
...) carry an ``errno`` when the operating system raises them — but
|
|
7
|
+
``errno`` is ``None`` when they are raised programmatically, which
|
|
8
|
+
breaks callers that inspect ``err.errno``. :func:`system_error` builds
|
|
9
|
+
the instance the way the OS would.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import errno
|
|
15
|
+
import os
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from types import MappingProxyType
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
#: OS exception class -> the errno the operating system would set.
|
|
21
|
+
#: (Keyed wide as ``type[Exception]`` so lookups with arbitrary
|
|
22
|
+
#: exception classes type-check; every entry is an OSError subclass.)
|
|
23
|
+
EXCEPTION_ERRNO_MAPPING: Mapping[type[Exception], int] = MappingProxyType(
|
|
24
|
+
{
|
|
25
|
+
FileNotFoundError: errno.ENOENT,
|
|
26
|
+
FileExistsError: errno.EEXIST,
|
|
27
|
+
IsADirectoryError: errno.EISDIR,
|
|
28
|
+
NotADirectoryError: errno.ENOTDIR,
|
|
29
|
+
PermissionError: errno.EACCES,
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def system_error(exception_class: type[Exception], *args: Any, **kwargs: Any) -> Exception:
|
|
35
|
+
"""Create an exception instance carrying the appropriate errno.
|
|
36
|
+
|
|
37
|
+
Intended for ``OSError`` subclasses, whose two leading constructor
|
|
38
|
+
arguments are ``(errno, strerror)`` — further ``args`` (e.g. a
|
|
39
|
+
filename) are appended. Classes without a known errno fall back to
|
|
40
|
+
``EINVAL``; for non-``OSError`` classes the errno pair simply lands
|
|
41
|
+
in ``args``.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> err = system_error(FileNotFoundError, "config.yaml")
|
|
45
|
+
>>> err.errno == errno.ENOENT and err.filename == "config.yaml"
|
|
46
|
+
True
|
|
47
|
+
"""
|
|
48
|
+
errno_value = EXCEPTION_ERRNO_MAPPING.get(exception_class, errno.EINVAL)
|
|
49
|
+
return exception_class(errno_value, os.strerror(errno_value), *args, **kwargs)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vcti-error-core
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Exit-code mechanics: validation, composition, and MRO-aware resolution of exception-to-exit-code mappings
|
|
5
|
+
Author: Visual Collaboration Technologies Inc.
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Repository, https://github.com/vcollab/vcti-python-error-core
|
|
8
|
+
Project-URL: Changelog, https://github.com/vcollab/vcti-python-error-core/blob/main/CHANGELOG.md
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Python: <3.15,>=3.12
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
19
|
+
Provides-Extra: lint
|
|
20
|
+
Requires-Dist: ruff; extra == "lint"
|
|
21
|
+
Provides-Extra: typecheck
|
|
22
|
+
Requires-Dist: mypy; extra == "typecheck"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# vcti-error-core
|
|
26
|
+
|
|
27
|
+
Exit-code mechanics: validation, composition, and MRO-aware resolution of exception-to-exit-code mappings.
|
|
28
|
+
|
|
29
|
+
## Overview
|
|
30
|
+
|
|
31
|
+
When a Python script runs as a subprocess of a partner application, the
|
|
32
|
+
process **exit code is the only reliable contract** between caller and
|
|
33
|
+
callee. `vcti-error-core` provides the mechanics for that contract — and
|
|
34
|
+
deliberately none of the vocabulary. A contract is plain data: a
|
|
35
|
+
read-only `Mapping[type[BaseException], int]` owned by whoever defines
|
|
36
|
+
it. This package validates such mappings (ranges, claims), composes them
|
|
37
|
+
collision-safely, and resolves an exception to its code MRO-aware, plus
|
|
38
|
+
an errno-correct constructor for OS exceptions. The actual VCollab code
|
|
39
|
+
vocabularies live in [`vcti-error-contract`](https://pypi.org/project/vcti-error-contract/);
|
|
40
|
+
any team can publish its own contract package on these mechanics.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install vcti-error-core
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
> Migrating from the retired `vcti-error` 1.x? Uninstall it first — it
|
|
49
|
+
> owns `vcti/error/__init__.py`, which conflicts with this namespace
|
|
50
|
+
> family. Its API lives on in `vcti.error.contract.legacy`.
|
|
51
|
+
|
|
52
|
+
### In `requirements.txt`
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
vcti-error-core>=1.0.0
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### In `pyproject.toml` dependencies
|
|
59
|
+
|
|
60
|
+
```toml
|
|
61
|
+
dependencies = [
|
|
62
|
+
"vcti-error-core>=1.0.0",
|
|
63
|
+
]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
A contract is a dict; the mechanics do the rest:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from vcti.error.core import combine, error_code, validate
|
|
74
|
+
|
|
75
|
+
class AppError(Exception): ...
|
|
76
|
+
class LicenseExpired(AppError): ...
|
|
77
|
+
|
|
78
|
+
MY_CODES = {AppError: 32, LicenseExpired: 33}
|
|
79
|
+
validate(MY_CODES, claim=range(32, 64)) # fail fast at startup
|
|
80
|
+
|
|
81
|
+
CODES = combine({Exception: 1, FileNotFoundError: 2}, MY_CODES)
|
|
82
|
+
|
|
83
|
+
def main() -> int:
|
|
84
|
+
try:
|
|
85
|
+
run()
|
|
86
|
+
return 0
|
|
87
|
+
except Exception as err:
|
|
88
|
+
return error_code(err, CODES, default=1)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Resolution is MRO-aware — a subclass inherits its nearest mapped
|
|
92
|
+
ancestor's code (`class QuotaExceeded(LicenseExpired)` still exits 33).
|
|
93
|
+
Composition is collision-safe — two contracts disagreeing about a class
|
|
94
|
+
raise `CodeCollisionError` at startup, not in production log analysis.
|
|
95
|
+
|
|
96
|
+
Errno-correct OS errors for programmatic raises:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from vcti.error.core import system_error
|
|
100
|
+
|
|
101
|
+
raise system_error(FileNotFoundError, "config.yaml")
|
|
102
|
+
# FileNotFoundError(ENOENT, "No such file or directory", "config.yaml")
|
|
103
|
+
# -> err.errno / err.strerror / err.filename all set, as if the OS raised it
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## API surface
|
|
109
|
+
|
|
110
|
+
| Name | Kind | Purpose |
|
|
111
|
+
|------|------|---------|
|
|
112
|
+
| `ErrorCodes` | type alias | `Mapping[type[BaseException], int]` — a contract |
|
|
113
|
+
| `validate(codes, *, claim)` | function | Range/type checks; `claim` enforces a contract's allocated range |
|
|
114
|
+
| `combine(*sets)` | function | Read-only merge; disagreement → `CodeCollisionError` |
|
|
115
|
+
| `error_code(exc, codes, *, default)` | function | MRO-aware resolution; `default` is caller-supplied |
|
|
116
|
+
| `system_error(cls, *args)` | function | Errno-correct OS-exception construction |
|
|
117
|
+
| `MAX_EXIT_CODE` | constant | 255 (POSIX truncates exit codes modulo 256) |
|
|
118
|
+
| `ErrorCodesError` | exception | Base; `CodeRangeError`, `CodeCollisionError` |
|
|
119
|
+
|
|
120
|
+
Rules baked into `validate`: codes are 1–255 (0 is success), stay ≤125
|
|
121
|
+
in practice (126+ collides with shell/signal conventions), and `bool`
|
|
122
|
+
codes are rejected.
|
|
123
|
+
|
|
124
|
+
See [docs/design.md](docs/design.md) for the mechanics/contract split
|
|
125
|
+
and [docs/extending.md](docs/extending.md) for writing a contract
|
|
126
|
+
package.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Dependencies
|
|
131
|
+
|
|
132
|
+
None. Standard library only.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
vcti/error/core/__init__.py,sha256=vkEk6FA-cIaPbzzBns-VF_1l1Zs0v3nCKR6UFZMRCg4,731
|
|
2
|
+
vcti/error/core/codes.py,sha256=pynt7LmIHuF6iJ_iN8fiwqn6UQ567e4tZSxmUcARKqI,4407
|
|
3
|
+
vcti/error/core/errors.py,sha256=UPyWmtpOQNH3vto9B1-B1A58ozGWDJTSCnOWkZ97uvM,1174
|
|
4
|
+
vcti/error/core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
vcti/error/core/system_error.py,sha256=sUvrxCNeSE7dzeWsqLnpj_RvM75Paka581TLvRe555I,1914
|
|
6
|
+
vcti_error_core-1.0.0.dist-info/licenses/LICENSE,sha256=gqRj-E4YRsT7mZ52W76LG6aTTFv6iEOK9QR_fV5EdrI,369
|
|
7
|
+
vcti_error_core-1.0.0.dist-info/METADATA,sha256=6hGu84RVwoXskepEBe5mNiIsNTBm19h3cDUjSj5Ma98,4460
|
|
8
|
+
vcti_error_core-1.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
9
|
+
vcti_error_core-1.0.0.dist-info/top_level.txt,sha256=Jl6AIAI3Xhru_BFQAhD_13VeXLmZQd9BqBNUaAKNgKs,5
|
|
10
|
+
vcti_error_core-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright (c) 2018-2026 Visual Collaboration Technologies Inc.
|
|
2
|
+
All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
This software is proprietary and confidential. Unauthorized copying,
|
|
5
|
+
distribution, or use of this software, via any medium, is strictly
|
|
6
|
+
prohibited. Access is granted only to authorized VCollab developers
|
|
7
|
+
and individuals explicitly authorized by Visual Collaboration
|
|
8
|
+
Technologies Inc.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vcti
|