vcti-error 1.0.4__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.
@@ -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,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: vcti-error
3
+ Version: 1.0.4
4
+ Summary: Structured exception handling and error classification for Python
5
+ Author: Visual Collaboration Technologies Inc.
6
+ Requires-Python: <3.15,>=3.12
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: test
10
+ Requires-Dist: pytest; extra == "test"
11
+ Requires-Dist: pytest-cov; extra == "test"
12
+ Provides-Extra: lint
13
+ Requires-Dist: ruff; extra == "lint"
14
+ Dynamic: license-file
15
+
16
+ # Error Framework
17
+
18
+ ## Purpose
19
+
20
+ VCollab Python scripts are typically invoked by partner programs or
21
+ external applications. When a script finishes, the invoking application
22
+ needs to know what happened — did it succeed, fail due to a missing
23
+ file, a license issue, or something unexpected?
24
+
25
+ In standard subprocess semantics, the **exit code is the only reliable
26
+ contract** between caller and callee. The `vcti-error` package
27
+ formalizes this contract by ensuring that every known exception type
28
+ maps to a stable, unique integer exit code — consistent across all
29
+ VCollab applications.
30
+
31
+ This package has **zero external dependencies**.
32
+
33
+ ### When to use this package
34
+
35
+ Use `vcti-error` when your Python script is invoked as a **subprocess** by
36
+ a partner application (e.g., VCollab Presenter, an external scheduler, or
37
+ a CI pipeline) and the caller interprets the process exit code to decide
38
+ what happened. If your code runs inside a larger Python process where
39
+ exceptions propagate normally, you likely don't need this package.
40
+
41
+ ---
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ # Latest main branch
47
+ pip install git+https://github.com/vcollab/vcti-python-error.git
48
+
49
+ # Specific version
50
+ pip install git+https://github.com/vcollab/vcti-python-error.git@v1.0.4
51
+ ```
52
+
53
+ ### From a GitHub Release
54
+
55
+ Download the wheel from the
56
+ [Releases](https://github.com/vcollab/vcti-python-error/releases)
57
+ page and install directly:
58
+
59
+ ```bash
60
+ pip install vcti_error-1.0.4-py3-none-any.whl
61
+ ```
62
+
63
+ ### In `requirements.txt`
64
+
65
+ ```
66
+ vcti-error @ git+https://github.com/vcollab/vcti-python-error.git@v1.0.4
67
+ ```
68
+
69
+ ### In `pyproject.toml` dependencies
70
+
71
+ ```toml
72
+ dependencies = [
73
+ "vcti-error @ git+https://github.com/vcollab/vcti-python-error.git@v1.0.4",
74
+ ]
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Quick Start
80
+
81
+ ### The main() pattern
82
+
83
+ All CLI entry points follow this structure:
84
+
85
+ ```python
86
+ import logging
87
+ import traceback
88
+ from vcti.error import error_code
89
+
90
+ logger = logging.getLogger(__name__)
91
+
92
+ def main() -> int:
93
+ try:
94
+ cli_app()
95
+ return 0
96
+ except Exception as error:
97
+ logger.error("Error: %s", error)
98
+ traceback.print_exc()
99
+ return error_code(type(error))
100
+ ```
101
+
102
+ - `0` — success
103
+ - `!= 0` — failure (category encoded in exit code)
104
+ - `error_code(type(error))` — resolves exception class to exit code
105
+ - Unmapped exceptions fall back to `UNSPECIFIED` (exit code 1)
106
+
107
+ ### Look up exit codes
108
+
109
+ ```python
110
+ from vcti.error import error_code
111
+
112
+ error_code(FileNotFoundError) # 2
113
+ error_code(KeyError) # 4
114
+ error_code(RuntimeError) # 1 (UNSPECIFIED fallback)
115
+ ```
116
+
117
+ ### Resolve exception class by name
118
+
119
+ ```python
120
+ from vcti.error import exception_class
121
+
122
+ exception_class("file_not_found") # FileNotFoundError
123
+ exception_class("variable_undefined") # VariableUndefined
124
+ exception_class("unknown_name") # Exception (fallback)
125
+ ```
126
+
127
+ ### Create system errors with errno
128
+
129
+ ```python
130
+ from vcti.error import system_error
131
+
132
+ err = system_error(FileNotFoundError, "path/to/file")
133
+ # FileNotFoundError(errno.ENOENT, "No such file or directory", "path/to/file")
134
+ raise err
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Registered Exception Types
140
+
141
+ | Exception type | Exit code | Python class | Description |
142
+ |----------------|-----------|-------------|-------------|
143
+ | `UNSPECIFIED` | 1 | `Exception` | Fallback for unmapped exceptions |
144
+ | `FILE_NOT_FOUND` | 2 | `FileNotFoundError` | Expected file not found |
145
+ | `LICENSE_ERROR` | 3 | `LicenseError` | License validation failed |
146
+ | `KEY_ERROR` | 4 | `KeyError` | Missing key or identifier |
147
+ | `VARIABLE_UNDEFINED` | 5 | `VariableUndefined` | Undefined workflow variable |
148
+
149
+ **Stability guarantee:** Exit code values are part of the external
150
+ contract with partner applications. Once assigned, a value must never
151
+ be changed or reused.
152
+
153
+ ---
154
+
155
+ ## Public API
156
+
157
+ | Function | Purpose |
158
+ |----------|---------|
159
+ | `error_code(exception_class)` | Returns exit code for an exception class |
160
+ | `exception_class(name)` | Resolves exception type name to Python class (case-insensitive) |
161
+ | `system_error(exception_class, *args)` | Creates exception with appropriate errno |
162
+
163
+ ---
164
+
165
+ ## Documentation
166
+
167
+ - [Changelog](CHANGELOG.md) — Release history and versioning policy
168
+ - [Design](docs/design.md) — Architecture decisions and mapping pipeline
169
+ - [Source Guide](docs/source-guide.md) — File descriptions and execution flow traces
170
+ - [Extension Guide](docs/extending.md) — How to add new exception types
171
+ - [API Reference](docs/api.md) — Autodoc for all modules
@@ -0,0 +1,156 @@
1
+ # Error Framework
2
+
3
+ ## Purpose
4
+
5
+ VCollab Python scripts are typically invoked by partner programs or
6
+ external applications. When a script finishes, the invoking application
7
+ needs to know what happened — did it succeed, fail due to a missing
8
+ file, a license issue, or something unexpected?
9
+
10
+ In standard subprocess semantics, the **exit code is the only reliable
11
+ contract** between caller and callee. The `vcti-error` package
12
+ formalizes this contract by ensuring that every known exception type
13
+ maps to a stable, unique integer exit code — consistent across all
14
+ VCollab applications.
15
+
16
+ This package has **zero external dependencies**.
17
+
18
+ ### When to use this package
19
+
20
+ Use `vcti-error` when your Python script is invoked as a **subprocess** by
21
+ a partner application (e.g., VCollab Presenter, an external scheduler, or
22
+ a CI pipeline) and the caller interprets the process exit code to decide
23
+ what happened. If your code runs inside a larger Python process where
24
+ exceptions propagate normally, you likely don't need this package.
25
+
26
+ ---
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ # Latest main branch
32
+ pip install git+https://github.com/vcollab/vcti-python-error.git
33
+
34
+ # Specific version
35
+ pip install git+https://github.com/vcollab/vcti-python-error.git@v1.0.4
36
+ ```
37
+
38
+ ### From a GitHub Release
39
+
40
+ Download the wheel from the
41
+ [Releases](https://github.com/vcollab/vcti-python-error/releases)
42
+ page and install directly:
43
+
44
+ ```bash
45
+ pip install vcti_error-1.0.4-py3-none-any.whl
46
+ ```
47
+
48
+ ### In `requirements.txt`
49
+
50
+ ```
51
+ vcti-error @ git+https://github.com/vcollab/vcti-python-error.git@v1.0.4
52
+ ```
53
+
54
+ ### In `pyproject.toml` dependencies
55
+
56
+ ```toml
57
+ dependencies = [
58
+ "vcti-error @ git+https://github.com/vcollab/vcti-python-error.git@v1.0.4",
59
+ ]
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Quick Start
65
+
66
+ ### The main() pattern
67
+
68
+ All CLI entry points follow this structure:
69
+
70
+ ```python
71
+ import logging
72
+ import traceback
73
+ from vcti.error import error_code
74
+
75
+ logger = logging.getLogger(__name__)
76
+
77
+ def main() -> int:
78
+ try:
79
+ cli_app()
80
+ return 0
81
+ except Exception as error:
82
+ logger.error("Error: %s", error)
83
+ traceback.print_exc()
84
+ return error_code(type(error))
85
+ ```
86
+
87
+ - `0` — success
88
+ - `!= 0` — failure (category encoded in exit code)
89
+ - `error_code(type(error))` — resolves exception class to exit code
90
+ - Unmapped exceptions fall back to `UNSPECIFIED` (exit code 1)
91
+
92
+ ### Look up exit codes
93
+
94
+ ```python
95
+ from vcti.error import error_code
96
+
97
+ error_code(FileNotFoundError) # 2
98
+ error_code(KeyError) # 4
99
+ error_code(RuntimeError) # 1 (UNSPECIFIED fallback)
100
+ ```
101
+
102
+ ### Resolve exception class by name
103
+
104
+ ```python
105
+ from vcti.error import exception_class
106
+
107
+ exception_class("file_not_found") # FileNotFoundError
108
+ exception_class("variable_undefined") # VariableUndefined
109
+ exception_class("unknown_name") # Exception (fallback)
110
+ ```
111
+
112
+ ### Create system errors with errno
113
+
114
+ ```python
115
+ from vcti.error import system_error
116
+
117
+ err = system_error(FileNotFoundError, "path/to/file")
118
+ # FileNotFoundError(errno.ENOENT, "No such file or directory", "path/to/file")
119
+ raise err
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Registered Exception Types
125
+
126
+ | Exception type | Exit code | Python class | Description |
127
+ |----------------|-----------|-------------|-------------|
128
+ | `UNSPECIFIED` | 1 | `Exception` | Fallback for unmapped exceptions |
129
+ | `FILE_NOT_FOUND` | 2 | `FileNotFoundError` | Expected file not found |
130
+ | `LICENSE_ERROR` | 3 | `LicenseError` | License validation failed |
131
+ | `KEY_ERROR` | 4 | `KeyError` | Missing key or identifier |
132
+ | `VARIABLE_UNDEFINED` | 5 | `VariableUndefined` | Undefined workflow variable |
133
+
134
+ **Stability guarantee:** Exit code values are part of the external
135
+ contract with partner applications. Once assigned, a value must never
136
+ be changed or reused.
137
+
138
+ ---
139
+
140
+ ## Public API
141
+
142
+ | Function | Purpose |
143
+ |----------|---------|
144
+ | `error_code(exception_class)` | Returns exit code for an exception class |
145
+ | `exception_class(name)` | Resolves exception type name to Python class (case-insensitive) |
146
+ | `system_error(exception_class, *args)` | Creates exception with appropriate errno |
147
+
148
+ ---
149
+
150
+ ## Documentation
151
+
152
+ - [Changelog](CHANGELOG.md) — Release history and versioning policy
153
+ - [Design](docs/design.md) — Architecture decisions and mapping pipeline
154
+ - [Source Guide](docs/source-guide.md) — File descriptions and execution flow traces
155
+ - [Extension Guide](docs/extending.md) — How to add new exception types
156
+ - [API Reference](docs/api.md) — Autodoc for all modules
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vcti-error"
7
+ version = "1.0.4"
8
+ description = "Structured exception handling and error classification for Python"
9
+ readme = "README.md"
10
+ authors = [
11
+ {name = "Visual Collaboration Technologies Inc."}
12
+ ]
13
+ requires-python = ">=3.12,<3.15"
14
+ dependencies = []
15
+
16
+ [project.optional-dependencies]
17
+ test = ["pytest", "pytest-cov"]
18
+ lint = ["ruff"]
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
22
+ include = ["vcti.error", "vcti.error.*"]
23
+
24
+ [tool.setuptools]
25
+ zip-safe = true
26
+
27
+ [tool.pytest.ini_options]
28
+ addopts = "--cov=vcti.error --cov-report=term-missing --cov-fail-under=95"
29
+
30
+ [tool.ruff]
31
+ target-version = "py312"
32
+ line-length = 99
33
+
34
+ [tool.ruff.lint]
35
+ select = ["E", "F", "W", "I", "UP"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Error handling framework for custom and system exceptions.
4
+
5
+ This package provides a framework for defining custom exceptions, mapping them to error codes,
6
+ and handling system errors with errno integration.
7
+ """
8
+
9
+ from importlib.metadata import version
10
+
11
+ __version__ = version("vcti-error")
12
+
13
+ from .custom_exceptions import *
14
+ from .system_error import system_error
15
+ from .utils import *
16
+
17
+ __all__ = [
18
+ "__version__",
19
+ "error_code",
20
+ "exception_class",
21
+ "system_error",
22
+ "LicenseError",
23
+ "VariableUndefined",
24
+ ]
@@ -0,0 +1,13 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Custom exception classes for the VCTI error framework."""
4
+
5
+ __all__ = ["LicenseError", "VariableUndefined"]
6
+
7
+
8
+ class LicenseError(Exception):
9
+ """Raised when license validation fails."""
10
+
11
+
12
+ class VariableUndefined(Exception):
13
+ """Raised when an undefined workflow variable is referenced."""
@@ -0,0 +1,41 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Defines mappings between exception types, classes, and system errors."""
4
+
5
+ import errno
6
+ from types import MappingProxyType
7
+
8
+ from .custom_exceptions import LicenseError, VariableUndefined
9
+ from .exception_type import ExceptionType
10
+
11
+ # The single authoritative mapping: enum → Python class.
12
+ # All other mappings are derived from this.
13
+ EXCEPTION_TYPE_TO_CLASS: MappingProxyType = MappingProxyType(
14
+ {
15
+ ExceptionType.UNSPECIFIED: Exception,
16
+ ExceptionType.FILE_NOT_FOUND: FileNotFoundError,
17
+ ExceptionType.LICENSE_ERROR: LicenseError,
18
+ ExceptionType.KEY_ERROR: KeyError,
19
+ ExceptionType.VARIABLE_UNDEFINED: VariableUndefined,
20
+ }
21
+ )
22
+
23
+ # Map lowercase exception names to their corresponding exception types
24
+ EXCEPTION_NAME_TO_TYPE: MappingProxyType = MappingProxyType(
25
+ {et.name.lower(): et for et in EXCEPTION_TYPE_TO_CLASS}
26
+ )
27
+
28
+ # Map exception classes to their corresponding error codes
29
+ EXCEPTION_CLASS_TO_ERROR_CODE: MappingProxyType = MappingProxyType(
30
+ {cls: et.value for et, cls in EXCEPTION_TYPE_TO_CLASS.items()}
31
+ )
32
+
33
+ # Map system exceptions to their corresponding errno values
34
+ EXCEPTION_ERRNO_MAPPING: MappingProxyType = MappingProxyType(
35
+ {
36
+ FileNotFoundError: errno.ENOENT,
37
+ FileExistsError: errno.EEXIST,
38
+ IsADirectoryError: errno.EISDIR,
39
+ NotADirectoryError: errno.ENOTDIR,
40
+ }
41
+ )
@@ -0,0 +1,35 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Defines standard exception types using an enumeration.
4
+
5
+ WARNING: These integer values are part of the external contract with partner
6
+ applications. They are used as process exit codes and must remain stable
7
+ across releases. Never reuse or reassign an existing value. When adding a
8
+ new entry, assign the next available integer. When removing an entry, leave
9
+ a gap — do not renumber remaining entries.
10
+ """
11
+
12
+ from enum import IntEnum
13
+
14
+
15
+ class ExceptionType(IntEnum):
16
+ """
17
+ Enumeration of exception types.
18
+
19
+ Each value is a stable exit code communicated to partner applications.
20
+ Do not change existing values — they are part of the external contract.
21
+ To add a new entry, assign the next available integer after the last one.
22
+
23
+ Attributes:
24
+ UNSPECIFIED: Represents an unspecified error.
25
+ FILE_NOT_FOUND: File not found error.
26
+ LICENSE_ERROR: License-related error.
27
+ KEY_ERROR: Key-related error.
28
+ VARIABLE_UNDEFINED: Undefined variable error.
29
+ """
30
+
31
+ UNSPECIFIED = 1
32
+ FILE_NOT_FOUND = 2
33
+ LICENSE_ERROR = 3
34
+ KEY_ERROR = 4
35
+ VARIABLE_UNDEFINED = 5
File without changes
@@ -0,0 +1,24 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Provides an interface for handling system errors with errno integration."""
4
+
5
+ import errno
6
+ import os
7
+
8
+ from .exception_mappings import EXCEPTION_ERRNO_MAPPING
9
+
10
+
11
+ def system_error(exception_class: type[Exception], *args, **kwargs) -> Exception:
12
+ """
13
+ Creates an instance of the specified exception class with an appropriate errno value.
14
+
15
+ Args:
16
+ exception_class (type): The exception class to instantiate.
17
+ *args: Additional positional arguments.
18
+ **kwargs: Additional keyword arguments.
19
+
20
+ Returns:
21
+ Exception: An instance of the specified exception class.
22
+ """
23
+ errno_value = EXCEPTION_ERRNO_MAPPING.get(exception_class, errno.EINVAL)
24
+ return exception_class(errno_value, os.strerror(errno_value), *args, **kwargs)
@@ -0,0 +1,41 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Utility functions for retrieving exception details."""
4
+
5
+ __all__ = ["error_code", "exception_class"]
6
+
7
+ from .exception_mappings import (
8
+ EXCEPTION_CLASS_TO_ERROR_CODE,
9
+ EXCEPTION_NAME_TO_TYPE,
10
+ EXCEPTION_TYPE_TO_CLASS,
11
+ )
12
+ from .exception_type import ExceptionType
13
+
14
+
15
+ def exception_class(exception_type_name: str) -> type:
16
+ """
17
+ Retrieves the exception class corresponding to a given exception type name.
18
+
19
+ Args:
20
+ exception_type_name (str): Name of the exception type (case-insensitive).
21
+
22
+ Returns:
23
+ type: Corresponding exception class, or `Exception` if not found.
24
+ """
25
+ exception_type = EXCEPTION_NAME_TO_TYPE.get(exception_type_name.lower())
26
+ if exception_type is None:
27
+ return Exception
28
+ return EXCEPTION_TYPE_TO_CLASS.get(exception_type, Exception)
29
+
30
+
31
+ def error_code(exception_cls: type) -> int:
32
+ """
33
+ Retrieves the error code corresponding to a given exception class.
34
+
35
+ Args:
36
+ exception_cls (type): The exception class.
37
+
38
+ Returns:
39
+ int: Corresponding error code, or `ExceptionType.UNSPECIFIED.value` if not found.
40
+ """
41
+ return EXCEPTION_CLASS_TO_ERROR_CODE.get(exception_cls, ExceptionType.UNSPECIFIED.value)
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: vcti-error
3
+ Version: 1.0.4
4
+ Summary: Structured exception handling and error classification for Python
5
+ Author: Visual Collaboration Technologies Inc.
6
+ Requires-Python: <3.15,>=3.12
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: test
10
+ Requires-Dist: pytest; extra == "test"
11
+ Requires-Dist: pytest-cov; extra == "test"
12
+ Provides-Extra: lint
13
+ Requires-Dist: ruff; extra == "lint"
14
+ Dynamic: license-file
15
+
16
+ # Error Framework
17
+
18
+ ## Purpose
19
+
20
+ VCollab Python scripts are typically invoked by partner programs or
21
+ external applications. When a script finishes, the invoking application
22
+ needs to know what happened — did it succeed, fail due to a missing
23
+ file, a license issue, or something unexpected?
24
+
25
+ In standard subprocess semantics, the **exit code is the only reliable
26
+ contract** between caller and callee. The `vcti-error` package
27
+ formalizes this contract by ensuring that every known exception type
28
+ maps to a stable, unique integer exit code — consistent across all
29
+ VCollab applications.
30
+
31
+ This package has **zero external dependencies**.
32
+
33
+ ### When to use this package
34
+
35
+ Use `vcti-error` when your Python script is invoked as a **subprocess** by
36
+ a partner application (e.g., VCollab Presenter, an external scheduler, or
37
+ a CI pipeline) and the caller interprets the process exit code to decide
38
+ what happened. If your code runs inside a larger Python process where
39
+ exceptions propagate normally, you likely don't need this package.
40
+
41
+ ---
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ # Latest main branch
47
+ pip install git+https://github.com/vcollab/vcti-python-error.git
48
+
49
+ # Specific version
50
+ pip install git+https://github.com/vcollab/vcti-python-error.git@v1.0.4
51
+ ```
52
+
53
+ ### From a GitHub Release
54
+
55
+ Download the wheel from the
56
+ [Releases](https://github.com/vcollab/vcti-python-error/releases)
57
+ page and install directly:
58
+
59
+ ```bash
60
+ pip install vcti_error-1.0.4-py3-none-any.whl
61
+ ```
62
+
63
+ ### In `requirements.txt`
64
+
65
+ ```
66
+ vcti-error @ git+https://github.com/vcollab/vcti-python-error.git@v1.0.4
67
+ ```
68
+
69
+ ### In `pyproject.toml` dependencies
70
+
71
+ ```toml
72
+ dependencies = [
73
+ "vcti-error @ git+https://github.com/vcollab/vcti-python-error.git@v1.0.4",
74
+ ]
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Quick Start
80
+
81
+ ### The main() pattern
82
+
83
+ All CLI entry points follow this structure:
84
+
85
+ ```python
86
+ import logging
87
+ import traceback
88
+ from vcti.error import error_code
89
+
90
+ logger = logging.getLogger(__name__)
91
+
92
+ def main() -> int:
93
+ try:
94
+ cli_app()
95
+ return 0
96
+ except Exception as error:
97
+ logger.error("Error: %s", error)
98
+ traceback.print_exc()
99
+ return error_code(type(error))
100
+ ```
101
+
102
+ - `0` — success
103
+ - `!= 0` — failure (category encoded in exit code)
104
+ - `error_code(type(error))` — resolves exception class to exit code
105
+ - Unmapped exceptions fall back to `UNSPECIFIED` (exit code 1)
106
+
107
+ ### Look up exit codes
108
+
109
+ ```python
110
+ from vcti.error import error_code
111
+
112
+ error_code(FileNotFoundError) # 2
113
+ error_code(KeyError) # 4
114
+ error_code(RuntimeError) # 1 (UNSPECIFIED fallback)
115
+ ```
116
+
117
+ ### Resolve exception class by name
118
+
119
+ ```python
120
+ from vcti.error import exception_class
121
+
122
+ exception_class("file_not_found") # FileNotFoundError
123
+ exception_class("variable_undefined") # VariableUndefined
124
+ exception_class("unknown_name") # Exception (fallback)
125
+ ```
126
+
127
+ ### Create system errors with errno
128
+
129
+ ```python
130
+ from vcti.error import system_error
131
+
132
+ err = system_error(FileNotFoundError, "path/to/file")
133
+ # FileNotFoundError(errno.ENOENT, "No such file or directory", "path/to/file")
134
+ raise err
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Registered Exception Types
140
+
141
+ | Exception type | Exit code | Python class | Description |
142
+ |----------------|-----------|-------------|-------------|
143
+ | `UNSPECIFIED` | 1 | `Exception` | Fallback for unmapped exceptions |
144
+ | `FILE_NOT_FOUND` | 2 | `FileNotFoundError` | Expected file not found |
145
+ | `LICENSE_ERROR` | 3 | `LicenseError` | License validation failed |
146
+ | `KEY_ERROR` | 4 | `KeyError` | Missing key or identifier |
147
+ | `VARIABLE_UNDEFINED` | 5 | `VariableUndefined` | Undefined workflow variable |
148
+
149
+ **Stability guarantee:** Exit code values are part of the external
150
+ contract with partner applications. Once assigned, a value must never
151
+ be changed or reused.
152
+
153
+ ---
154
+
155
+ ## Public API
156
+
157
+ | Function | Purpose |
158
+ |----------|---------|
159
+ | `error_code(exception_class)` | Returns exit code for an exception class |
160
+ | `exception_class(name)` | Resolves exception type name to Python class (case-insensitive) |
161
+ | `system_error(exception_class, *args)` | Creates exception with appropriate errno |
162
+
163
+ ---
164
+
165
+ ## Documentation
166
+
167
+ - [Changelog](CHANGELOG.md) — Release history and versioning policy
168
+ - [Design](docs/design.md) — Architecture decisions and mapping pipeline
169
+ - [Source Guide](docs/source-guide.md) — File descriptions and execution flow traces
170
+ - [Extension Guide](docs/extending.md) — How to add new exception types
171
+ - [API Reference](docs/api.md) — Autodoc for all modules
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/vcti/error/__init__.py
5
+ src/vcti/error/custom_exceptions.py
6
+ src/vcti/error/exception_mappings.py
7
+ src/vcti/error/exception_type.py
8
+ src/vcti/error/py.typed
9
+ src/vcti/error/system_error.py
10
+ src/vcti/error/utils.py
11
+ src/vcti_error.egg-info/PKG-INFO
12
+ src/vcti_error.egg-info/SOURCES.txt
13
+ src/vcti_error.egg-info/dependency_links.txt
14
+ src/vcti_error.egg-info/requires.txt
15
+ src/vcti_error.egg-info/top_level.txt
16
+ src/vcti_error.egg-info/zip-safe
17
+ tests/test_exceptions.py
@@ -0,0 +1,7 @@
1
+
2
+ [lint]
3
+ ruff
4
+
5
+ [test]
6
+ pytest
7
+ pytest-cov
@@ -0,0 +1,160 @@
1
+ import errno
2
+ import os
3
+ import re
4
+
5
+ import pytest
6
+
7
+ import vcti.error
8
+ from vcti.error.exception_mappings import (
9
+ EXCEPTION_CLASS_TO_ERROR_CODE,
10
+ EXCEPTION_ERRNO_MAPPING,
11
+ EXCEPTION_NAME_TO_TYPE,
12
+ EXCEPTION_TYPE_TO_CLASS,
13
+ )
14
+ from vcti.error.exception_type import ExceptionType
15
+ from vcti.error.system_error import system_error
16
+ from vcti.error.utils import error_code, exception_class
17
+
18
+
19
+ class TestPublicAPI:
20
+ """Tests for package public API surface."""
21
+
22
+ def test_version_exists(self):
23
+ """Ensure __version__ is defined."""
24
+ assert hasattr(vcti.error, "__version__")
25
+
26
+ def test_version_is_valid_semver(self):
27
+ """Ensure __version__ matches a valid version pattern."""
28
+ assert re.match(r"^\d+\.\d+\.\d+", vcti.error.__version__)
29
+
30
+ def test_all_exports(self):
31
+ """Ensure __all__ lists exactly the documented public API."""
32
+ expected = {
33
+ "__version__",
34
+ "error_code",
35
+ "exception_class",
36
+ "system_error",
37
+ "LicenseError",
38
+ "VariableUndefined",
39
+ }
40
+ assert set(vcti.error.__all__) == expected
41
+
42
+
43
+ class TestMappingImmutability:
44
+ """Tests that mapping dictionaries are read-only at runtime."""
45
+
46
+ @pytest.mark.parametrize(
47
+ "mapping",
48
+ [
49
+ EXCEPTION_TYPE_TO_CLASS,
50
+ EXCEPTION_NAME_TO_TYPE,
51
+ EXCEPTION_CLASS_TO_ERROR_CODE,
52
+ EXCEPTION_ERRNO_MAPPING,
53
+ ],
54
+ )
55
+ def test_mapping_is_immutable(self, mapping):
56
+ """Ensure mappings raise TypeError on mutation attempts."""
57
+ with pytest.raises(TypeError):
58
+ mapping["__test__"] = None
59
+
60
+
61
+ class TestCustomExceptions:
62
+ """Tests for custom exception classes."""
63
+
64
+ @pytest.mark.parametrize("exception_cls", EXCEPTION_TYPE_TO_CLASS.values())
65
+ def test_exception_creation(self, exception_cls):
66
+ """Ensure custom exceptions exist and inherit from `Exception`."""
67
+ assert issubclass(exception_cls, Exception)
68
+ assert isinstance(exception_cls(), exception_cls)
69
+
70
+ def test_custom_exceptions_importable_from_package(self):
71
+ """Ensure custom exceptions are importable from the public package path."""
72
+ from vcti.error import LicenseError, VariableUndefined
73
+
74
+ assert issubclass(LicenseError, Exception)
75
+ assert issubclass(VariableUndefined, Exception)
76
+
77
+ def test_exception_with_custom_message(self):
78
+ """Ensure custom exceptions accept custom messages."""
79
+ custom_message = "Custom error message"
80
+ exception_cls = EXCEPTION_TYPE_TO_CLASS[ExceptionType.LICENSE_ERROR]
81
+ ex = exception_cls(custom_message)
82
+ assert str(ex) == custom_message
83
+
84
+
85
+ class TestErrorCodeMappings:
86
+ """Tests for exception-to-error-code mappings."""
87
+
88
+ @pytest.mark.parametrize("exception_cls,expected_code", EXCEPTION_CLASS_TO_ERROR_CODE.items())
89
+ def test_error_code_mapping(self, exception_cls, expected_code):
90
+ """Ensure correct error codes are assigned to exception classes."""
91
+ assert error_code(exception_cls) == expected_code
92
+
93
+ @pytest.mark.parametrize(
94
+ "builtin_exception,expected_code",
95
+ [
96
+ (FileNotFoundError, ExceptionType.FILE_NOT_FOUND.value),
97
+ (KeyError, ExceptionType.KEY_ERROR.value),
98
+ ],
99
+ )
100
+ def test_error_code_for_builtin_exceptions(self, builtin_exception, expected_code):
101
+ """Ensure built-in exceptions map to correct predefined error codes."""
102
+ assert error_code(builtin_exception) == expected_code
103
+
104
+ def test_unmapped_exception_returns_unspecified(self):
105
+ """Ensure unmapped exceptions fall back to UNSPECIFIED."""
106
+ assert error_code(RuntimeError) == ExceptionType.UNSPECIFIED.value
107
+
108
+
109
+ class TestExceptionRetrieval:
110
+ """Tests for retrieving exceptions by name."""
111
+
112
+ @pytest.mark.parametrize(
113
+ "exception_name,expected_cls",
114
+ [
115
+ ("license_error", EXCEPTION_TYPE_TO_CLASS[ExceptionType.LICENSE_ERROR]),
116
+ ("variable_undefined", EXCEPTION_TYPE_TO_CLASS[ExceptionType.VARIABLE_UNDEFINED]),
117
+ ("file_not_found", FileNotFoundError),
118
+ ("key_error", KeyError),
119
+ ],
120
+ )
121
+ def test_exception_class_retrieval(self, exception_name, expected_cls):
122
+ """Ensure `exception_class()` correctly retrieves exceptions by name."""
123
+ assert exception_class(exception_name) == expected_cls
124
+
125
+ def test_exception_class_not_found(self):
126
+ """Check that an invalid exception name returns the default Exception class."""
127
+ assert exception_class("non_existent_error") == Exception
128
+
129
+ def test_exception_class_case_insensitive(self):
130
+ """Ensure exception name lookup is case-insensitive."""
131
+ assert exception_class("FILE_NOT_FOUND") == FileNotFoundError
132
+ assert exception_class("File_Not_Found") == FileNotFoundError
133
+
134
+
135
+ class TestSystemErrors:
136
+ """Tests for system error handling with errno integration."""
137
+
138
+ @pytest.mark.parametrize("exception_cls,expected_errno", EXCEPTION_ERRNO_MAPPING.items())
139
+ def test_system_error_creation(self, exception_cls, expected_errno):
140
+ """Ensure `system_error()` correctly assigns errno values."""
141
+ ex = system_error(exception_cls)
142
+ assert ex.args[0] == expected_errno
143
+ assert ex.args[1] == os.strerror(expected_errno)
144
+
145
+ def test_system_error_with_additional_args(self):
146
+ """Ensure additional arguments are passed through to the exception."""
147
+ ex = system_error(FileNotFoundError, "/some/path")
148
+ assert ex.errno == errno.ENOENT
149
+ assert ex.strerror == os.strerror(errno.ENOENT)
150
+ assert ex.filename == "/some/path"
151
+
152
+ def test_system_error_default(self):
153
+ """Ensure unknown exceptions receive `EINVAL` (Invalid argument) as errno."""
154
+
155
+ class UnknownException(Exception):
156
+ pass
157
+
158
+ ex = system_error(UnknownException)
159
+ assert ex.args[0] == errno.EINVAL
160
+ assert ex.args[1] == os.strerror(errno.EINVAL)