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.
@@ -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
+ ]
@@ -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
+ """
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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