python-custom-exceptions 0.0.1__tar.gz → 1.0.0__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.
- {python_custom_exceptions-0.0.1 → python_custom_exceptions-1.0.0}/MANIFEST.in +1 -1
- python_custom_exceptions-1.0.0/PKG-INFO +154 -0
- python_custom_exceptions-1.0.0/README.md +133 -0
- python_custom_exceptions-1.0.0/pyproject.toml +47 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions/__init__.py +37 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions/base_custom_exception.py +46 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions/comparison_exception.py +20 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions/connection_exceptions.py +23 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions/execution_exception.py +15 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions/is_exist_exception.py +55 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions/operation_exception.py +40 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions/os_exceptions.py +14 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions/unsupported_exception.py +14 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions/wrong_exception.py +14 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions.egg-info/PKG-INFO +154 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions.egg-info/SOURCES.txt +21 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions.egg-info/requires.txt +1 -0
- python_custom_exceptions-1.0.0/python_custom_exceptions.egg-info/top_level.txt +1 -0
- python_custom_exceptions-1.0.0/tests/test_diagnostic_info.py +206 -0
- python_custom_exceptions-1.0.0/tests/test_exception.py +103 -0
- python_custom_exceptions-0.0.1/PKG-INFO +0 -43
- python_custom_exceptions-0.0.1/README.md +0 -16
- python_custom_exceptions-0.0.1/pyproject.toml +0 -3
- python_custom_exceptions-0.0.1/python_custom_exceptions.egg-info/PKG-INFO +0 -43
- python_custom_exceptions-0.0.1/python_custom_exceptions.egg-info/SOURCES.txt +0 -11
- python_custom_exceptions-0.0.1/python_custom_exceptions.egg-info/requires.txt +0 -6
- python_custom_exceptions-0.0.1/python_custom_exceptions.egg-info/top_level.txt +0 -1
- python_custom_exceptions-0.0.1/requirements.txt +0 -7
- python_custom_exceptions-0.0.1/setup.py +0 -37
- {python_custom_exceptions-0.0.1 → python_custom_exceptions-1.0.0}/LICENSE +0 -0
- {python_custom_exceptions-0.0.1 → python_custom_exceptions-1.0.0}/python_custom_exceptions.egg-info/dependency_links.txt +0 -0
- {python_custom_exceptions-0.0.1 → python_custom_exceptions-1.0.0}/setup.cfg +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-custom-exceptions
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A collection of custom exceptions for Python projects.
|
|
5
|
+
Author: Avi Zaguri
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aviz92/python-custom-exceptions
|
|
8
|
+
Project-URL: Repository, https://github.com/aviz92/python-custom-exceptions
|
|
9
|
+
Project-URL: Issues, https://github.com/aviz92/python-custom-exceptions/issues
|
|
10
|
+
Keywords: development,python,exceptions,error-handling
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: python-base-toolkit>=1.1.3
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Python Custom Exceptions
|
|
23
|
+
A collection of structured, ready-to-use exception classes for Python projects. <br>
|
|
24
|
+
Every exception renders as JSON with its type, a human-readable message, and optional diagnostic info,
|
|
25
|
+
so errors are easy to read in logs and easy to parse by tools. <br>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
- ✅ Specific Exception Classes: Ready-made exceptions for common error scenarios.
|
|
31
|
+
- ✅ Structured Output: `str(exception)` is a JSON document with type, message and diagnostic info.
|
|
32
|
+
- ✅ Diagnostic Info: Attach typed context (`TypedDict`, `dict` or dataclass) to any exception.
|
|
33
|
+
- ✅ Single Base Class: Everything inherits from `BaseCustomException`, so one `except` catches them all.
|
|
34
|
+
- ✅ Easy Extension: Subclass `BaseCustomException` to add your own exceptions.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### Installation
|
|
39
|
+
```bash
|
|
40
|
+
uv add python-custom-exceptions
|
|
41
|
+
```
|
|
42
|
+
or with pip:
|
|
43
|
+
```bash
|
|
44
|
+
pip install python-custom-exceptions
|
|
45
|
+
```
|
|
46
|
+
Requires Python 3.12+.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### Available Exceptions
|
|
51
|
+
| Exception | Arguments | Message |
|
|
52
|
+
|-----------|-----------|---------|
|
|
53
|
+
| `BaseCustomException` | `message_with_marking_dynamic_variables` | `<message>` |
|
|
54
|
+
| `ComparisonException` | `expected_value`, `actual_value`, `subject=None` | `Expected <subject> to be [<expected>], but got [<actual>]` |
|
|
55
|
+
| `ConnectionException` | `host`, `message=None` | `Connection failed: Unable to establish a connection to the remote host ["<host>"]. ...` |
|
|
56
|
+
| `IsExistException` | `subject` | `[<subject>] is exists` |
|
|
57
|
+
| `IsNotExistException` | `subject` | `[<subject>] is not exists` |
|
|
58
|
+
| `IsExistAfterOperationException` | `subject`, `operation` | `<subject> exists after trying to <operation>` |
|
|
59
|
+
| `IsNotExistAfterOperationException` | `subject`, `operation` | `<subject> does not exist after trying to <operation>` |
|
|
60
|
+
| `OperationException` | `operation` | `Operation <operation> failed` |
|
|
61
|
+
| `NotOperationalException` | `subject` | `<subject> is not Operational` |
|
|
62
|
+
| `UnexpectedlySucceededOperationException` | `operation` | `Operation <operation> unexpectedly succeeded` |
|
|
63
|
+
| `TimedExecutionException` | `operation`, `timeout` | `Operation <operation> did not succeed within [<timeout>] seconds` |
|
|
64
|
+
| `UnsupportedExceptions` | `unsupported_subject` | `<unsupported_subject> is unsupported` |
|
|
65
|
+
| `UnsupportedOSExceptions` | `os_name` | `Unsupported OS: ["<os_name>"]` |
|
|
66
|
+
| `WrongUsageException` | `message` | `Wrong usage: <message>` |
|
|
67
|
+
|
|
68
|
+
All exceptions also accept an optional `diagnostic_info` argument.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### Usage Examples
|
|
73
|
+
|
|
74
|
+
#### Raise an exception with diagnostic info
|
|
75
|
+
```python
|
|
76
|
+
from pathlib import Path
|
|
77
|
+
|
|
78
|
+
from python_custom_exceptions import DiagnosticInfo, IsNotExistException
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class PathDiagnosticInfo(DiagnosticInfo):
|
|
82
|
+
path: str
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
path = Path("/tmp/missing")
|
|
86
|
+
if not path.exists():
|
|
87
|
+
raise IsNotExistException(
|
|
88
|
+
subject="Directory",
|
|
89
|
+
diagnostic_info=PathDiagnosticInfo(path=str(path)),
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
Output:
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"exception_type": "IsNotExistException",
|
|
96
|
+
"message": "[Directory] is not exists",
|
|
97
|
+
"diagnostic_info": {
|
|
98
|
+
"path": "/tmp/missing"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Catch any custom exception
|
|
104
|
+
```python
|
|
105
|
+
from python_custom_exceptions import BaseCustomException, ComparisonException
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
raise ComparisonException(expected_value=200, actual_value=500, subject="status code")
|
|
109
|
+
except BaseCustomException as exc:
|
|
110
|
+
print(exc.exception_type) # ComparisonException
|
|
111
|
+
print(exc.message) # Expected status code to be [200], but got [500]
|
|
112
|
+
print(exc.diagnostic_info) # {}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Define your own exception
|
|
116
|
+
```python
|
|
117
|
+
from python_custom_exceptions import BaseCustomException, DiagnosticInfo
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class QuotaExceededException(BaseCustomException):
|
|
121
|
+
def __init__(self, resource: str, limit: int, diagnostic_info: dict | DiagnosticInfo | None = None) -> None:
|
|
122
|
+
super().__init__(
|
|
123
|
+
message_with_marking_dynamic_variables=f"Quota for [{resource}] exceeded the limit of [{limit}]",
|
|
124
|
+
diagnostic_info=diagnostic_info,
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### Development
|
|
131
|
+
```bash
|
|
132
|
+
git clone https://github.com/aviz92/python-custom-exceptions.git
|
|
133
|
+
cd python-custom-exceptions
|
|
134
|
+
uv sync # create .venv and install runtime + dev dependencies
|
|
135
|
+
uv run pre-commit install # enable git hooks
|
|
136
|
+
uv run pytest # run the tests
|
|
137
|
+
uv build # build sdist + wheel into dist/
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### 🤝 Contributing
|
|
143
|
+
If you have a helpful exception, pattern, or improvement to suggest:
|
|
144
|
+
1. Fork the repo
|
|
145
|
+
2. Create a new branch
|
|
146
|
+
3. Submit a pull request
|
|
147
|
+
|
|
148
|
+
I welcome additions that promote clean, productive, and maintainable development. <br>
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### 🙏 Thanks
|
|
153
|
+
Thanks for exploring this repository! <br>
|
|
154
|
+
Happy coding! <br>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Python Custom Exceptions
|
|
2
|
+
A collection of structured, ready-to-use exception classes for Python projects. <br>
|
|
3
|
+
Every exception renders as JSON with its type, a human-readable message, and optional diagnostic info,
|
|
4
|
+
so errors are easy to read in logs and easy to parse by tools. <br>
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- ✅ Specific Exception Classes: Ready-made exceptions for common error scenarios.
|
|
10
|
+
- ✅ Structured Output: `str(exception)` is a JSON document with type, message and diagnostic info.
|
|
11
|
+
- ✅ Diagnostic Info: Attach typed context (`TypedDict`, `dict` or dataclass) to any exception.
|
|
12
|
+
- ✅ Single Base Class: Everything inherits from `BaseCustomException`, so one `except` catches them all.
|
|
13
|
+
- ✅ Easy Extension: Subclass `BaseCustomException` to add your own exceptions.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
### Installation
|
|
18
|
+
```bash
|
|
19
|
+
uv add python-custom-exceptions
|
|
20
|
+
```
|
|
21
|
+
or with pip:
|
|
22
|
+
```bash
|
|
23
|
+
pip install python-custom-exceptions
|
|
24
|
+
```
|
|
25
|
+
Requires Python 3.12+.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
### Available Exceptions
|
|
30
|
+
| Exception | Arguments | Message |
|
|
31
|
+
|-----------|-----------|---------|
|
|
32
|
+
| `BaseCustomException` | `message_with_marking_dynamic_variables` | `<message>` |
|
|
33
|
+
| `ComparisonException` | `expected_value`, `actual_value`, `subject=None` | `Expected <subject> to be [<expected>], but got [<actual>]` |
|
|
34
|
+
| `ConnectionException` | `host`, `message=None` | `Connection failed: Unable to establish a connection to the remote host ["<host>"]. ...` |
|
|
35
|
+
| `IsExistException` | `subject` | `[<subject>] is exists` |
|
|
36
|
+
| `IsNotExistException` | `subject` | `[<subject>] is not exists` |
|
|
37
|
+
| `IsExistAfterOperationException` | `subject`, `operation` | `<subject> exists after trying to <operation>` |
|
|
38
|
+
| `IsNotExistAfterOperationException` | `subject`, `operation` | `<subject> does not exist after trying to <operation>` |
|
|
39
|
+
| `OperationException` | `operation` | `Operation <operation> failed` |
|
|
40
|
+
| `NotOperationalException` | `subject` | `<subject> is not Operational` |
|
|
41
|
+
| `UnexpectedlySucceededOperationException` | `operation` | `Operation <operation> unexpectedly succeeded` |
|
|
42
|
+
| `TimedExecutionException` | `operation`, `timeout` | `Operation <operation> did not succeed within [<timeout>] seconds` |
|
|
43
|
+
| `UnsupportedExceptions` | `unsupported_subject` | `<unsupported_subject> is unsupported` |
|
|
44
|
+
| `UnsupportedOSExceptions` | `os_name` | `Unsupported OS: ["<os_name>"]` |
|
|
45
|
+
| `WrongUsageException` | `message` | `Wrong usage: <message>` |
|
|
46
|
+
|
|
47
|
+
All exceptions also accept an optional `diagnostic_info` argument.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### Usage Examples
|
|
52
|
+
|
|
53
|
+
#### Raise an exception with diagnostic info
|
|
54
|
+
```python
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
|
|
57
|
+
from python_custom_exceptions import DiagnosticInfo, IsNotExistException
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PathDiagnosticInfo(DiagnosticInfo):
|
|
61
|
+
path: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
path = Path("/tmp/missing")
|
|
65
|
+
if not path.exists():
|
|
66
|
+
raise IsNotExistException(
|
|
67
|
+
subject="Directory",
|
|
68
|
+
diagnostic_info=PathDiagnosticInfo(path=str(path)),
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
Output:
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"exception_type": "IsNotExistException",
|
|
75
|
+
"message": "[Directory] is not exists",
|
|
76
|
+
"diagnostic_info": {
|
|
77
|
+
"path": "/tmp/missing"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Catch any custom exception
|
|
83
|
+
```python
|
|
84
|
+
from python_custom_exceptions import BaseCustomException, ComparisonException
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
raise ComparisonException(expected_value=200, actual_value=500, subject="status code")
|
|
88
|
+
except BaseCustomException as exc:
|
|
89
|
+
print(exc.exception_type) # ComparisonException
|
|
90
|
+
print(exc.message) # Expected status code to be [200], but got [500]
|
|
91
|
+
print(exc.diagnostic_info) # {}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### Define your own exception
|
|
95
|
+
```python
|
|
96
|
+
from python_custom_exceptions import BaseCustomException, DiagnosticInfo
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class QuotaExceededException(BaseCustomException):
|
|
100
|
+
def __init__(self, resource: str, limit: int, diagnostic_info: dict | DiagnosticInfo | None = None) -> None:
|
|
101
|
+
super().__init__(
|
|
102
|
+
message_with_marking_dynamic_variables=f"Quota for [{resource}] exceeded the limit of [{limit}]",
|
|
103
|
+
diagnostic_info=diagnostic_info,
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### Development
|
|
110
|
+
```bash
|
|
111
|
+
git clone https://github.com/aviz92/python-custom-exceptions.git
|
|
112
|
+
cd python-custom-exceptions
|
|
113
|
+
uv sync # create .venv and install runtime + dev dependencies
|
|
114
|
+
uv run pre-commit install # enable git hooks
|
|
115
|
+
uv run pytest # run the tests
|
|
116
|
+
uv build # build sdist + wheel into dist/
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### 🤝 Contributing
|
|
122
|
+
If you have a helpful exception, pattern, or improvement to suggest:
|
|
123
|
+
1. Fork the repo
|
|
124
|
+
2. Create a new branch
|
|
125
|
+
3. Submit a pull request
|
|
126
|
+
|
|
127
|
+
I welcome additions that promote clean, productive, and maintainable development. <br>
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### 🙏 Thanks
|
|
132
|
+
Thanks for exploring this repository! <br>
|
|
133
|
+
Happy coding! <br>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "python-custom-exceptions"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A collection of custom exceptions for Python projects."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Avi Zaguri" },]
|
|
13
|
+
keywords = ["development", "python", "exceptions", "error-handling"]
|
|
14
|
+
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
dependencies = [
|
|
24
|
+
"python-base-toolkit>=1.1.3",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest-better-report>=1.0.2",
|
|
30
|
+
"custom-python-logger>=4.1.0",
|
|
31
|
+
"pre-commit>=4.7.0",
|
|
32
|
+
"pytest>=9.2.0",
|
|
33
|
+
"python-dotenv>=1.2.4",
|
|
34
|
+
"setuptools>=84.2.1",
|
|
35
|
+
"wheel>=0.48.0",
|
|
36
|
+
"python-base-command>=0.1.8",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/aviz92/python-custom-exceptions"
|
|
41
|
+
Repository = "https://github.com/aviz92/python-custom-exceptions"
|
|
42
|
+
Issues = "https://github.com/aviz92/python-custom-exceptions/issues"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["."]
|
|
46
|
+
include = ["python_custom_exceptions*"]
|
|
47
|
+
namespaces = false
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from python_custom_exceptions.base_custom_exception import BaseCustomException, DiagnosticInfo, serialize_data
|
|
2
|
+
from python_custom_exceptions.comparison_exception import ComparisonException
|
|
3
|
+
from python_custom_exceptions.connection_exceptions import ConnectionException
|
|
4
|
+
from python_custom_exceptions.execution_exception import TimedExecutionException
|
|
5
|
+
from python_custom_exceptions.is_exist_exception import (
|
|
6
|
+
IsExistAfterOperationException,
|
|
7
|
+
IsExistException,
|
|
8
|
+
IsNotExistAfterOperationException,
|
|
9
|
+
IsNotExistException,
|
|
10
|
+
)
|
|
11
|
+
from python_custom_exceptions.operation_exception import (
|
|
12
|
+
NotOperationalException,
|
|
13
|
+
OperationException,
|
|
14
|
+
UnexpectedlySucceededOperationException,
|
|
15
|
+
)
|
|
16
|
+
from python_custom_exceptions.os_exceptions import UnsupportedOSExceptions
|
|
17
|
+
from python_custom_exceptions.unsupported_exception import UnsupportedExceptions
|
|
18
|
+
from python_custom_exceptions.wrong_exception import WrongUsageException
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"BaseCustomException",
|
|
22
|
+
"DiagnosticInfo",
|
|
23
|
+
"serialize_data",
|
|
24
|
+
"ConnectionException",
|
|
25
|
+
"ComparisonException",
|
|
26
|
+
"IsExistAfterOperationException",
|
|
27
|
+
"IsExistException",
|
|
28
|
+
"IsNotExistException",
|
|
29
|
+
"IsNotExistAfterOperationException",
|
|
30
|
+
"NotOperationalException",
|
|
31
|
+
"OperationException",
|
|
32
|
+
"TimedExecutionException",
|
|
33
|
+
"UnexpectedlySucceededOperationException",
|
|
34
|
+
"UnsupportedExceptions",
|
|
35
|
+
"WrongUsageException",
|
|
36
|
+
"UnsupportedOSExceptions",
|
|
37
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import is_dataclass
|
|
3
|
+
from json import dumps
|
|
4
|
+
from typing import TypedDict
|
|
5
|
+
|
|
6
|
+
from python_base_toolkit.utils.data_serialization import default_serialize
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DiagnosticInfo(TypedDict): ...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def serialize_data(obj: object) -> dict:
|
|
13
|
+
return json.loads(json.dumps(obj, default=default_serialize))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_full_exception_message(content: dict) -> str:
|
|
17
|
+
return (
|
|
18
|
+
f'{content["message"]}\n\nCustom Exception Raised!\n'
|
|
19
|
+
f"{dumps(content, indent=4, sort_keys=False, default=str)}"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BaseCustomException(Exception):
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
message_with_marking_dynamic_variables: str,
|
|
27
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
if is_dataclass(diagnostic_info):
|
|
30
|
+
diagnostic_info = diagnostic_info.__dict__
|
|
31
|
+
|
|
32
|
+
self.exception_type = type(self).__name__
|
|
33
|
+
self.message = message_with_marking_dynamic_variables
|
|
34
|
+
self.diagnostic_info = diagnostic_info or {}
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
exception_content = {
|
|
38
|
+
"exception_type": self.exception_type,
|
|
39
|
+
"message": self.message,
|
|
40
|
+
"diagnostic_info": self.diagnostic_info,
|
|
41
|
+
}
|
|
42
|
+
# return get_full_exception_message(exception_content)
|
|
43
|
+
return dumps(exception_content, indent=4, sort_keys=False, default=str)
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
return self.__str__()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from python_custom_exceptions.base_custom_exception import BaseCustomException, DiagnosticInfo
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ComparisonException(BaseCustomException):
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
expected_value: int,
|
|
8
|
+
actual_value: int,
|
|
9
|
+
subject: str | None = None,
|
|
10
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
11
|
+
) -> None:
|
|
12
|
+
message = (
|
|
13
|
+
f"Expected {subject} to be [{expected_value}], but got [{actual_value}]"
|
|
14
|
+
if subject
|
|
15
|
+
else f"Expected [{expected_value}], but got [{actual_value}]"
|
|
16
|
+
)
|
|
17
|
+
super().__init__(
|
|
18
|
+
message_with_marking_dynamic_variables=message,
|
|
19
|
+
diagnostic_info=diagnostic_info,
|
|
20
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from python_custom_exceptions.base_custom_exception import BaseCustomException, DiagnosticInfo
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ConnectionException(BaseCustomException):
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
host: str | None,
|
|
8
|
+
message: str | None = None,
|
|
9
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
10
|
+
) -> None:
|
|
11
|
+
message = (
|
|
12
|
+
(
|
|
13
|
+
f'Connection failed: Unable to establish a connection to the remote host ["{host}"]. '
|
|
14
|
+
f"Please check your credentials, network status, and host availability."
|
|
15
|
+
)
|
|
16
|
+
if not message
|
|
17
|
+
else message
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
super().__init__(
|
|
21
|
+
message_with_marking_dynamic_variables=message,
|
|
22
|
+
diagnostic_info=diagnostic_info,
|
|
23
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from python_custom_exceptions.base_custom_exception import BaseCustomException, DiagnosticInfo
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TimedExecutionException(BaseCustomException):
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
operation: str,
|
|
8
|
+
timeout: str | int,
|
|
9
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
10
|
+
) -> None:
|
|
11
|
+
message = f"Operation {operation} did not succeed within [{timeout}] seconds"
|
|
12
|
+
super().__init__(
|
|
13
|
+
message_with_marking_dynamic_variables=message,
|
|
14
|
+
diagnostic_info=diagnostic_info,
|
|
15
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from python_custom_exceptions.base_custom_exception import BaseCustomException, DiagnosticInfo
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class IsExistException(BaseCustomException):
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
subject: str,
|
|
8
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
9
|
+
) -> None:
|
|
10
|
+
message = f"[{subject}] is exists"
|
|
11
|
+
super().__init__(
|
|
12
|
+
message_with_marking_dynamic_variables=message,
|
|
13
|
+
diagnostic_info=diagnostic_info,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IsNotExistException(BaseCustomException):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
subject: str,
|
|
21
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
message = f"[{subject}] is not exists"
|
|
24
|
+
super().__init__(
|
|
25
|
+
message_with_marking_dynamic_variables=message,
|
|
26
|
+
diagnostic_info=diagnostic_info,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class IsExistAfterOperationException(BaseCustomException):
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
subject: str,
|
|
34
|
+
operation: str,
|
|
35
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
message = f"{subject} exists after trying to {operation}"
|
|
38
|
+
super().__init__(
|
|
39
|
+
message_with_marking_dynamic_variables=message,
|
|
40
|
+
diagnostic_info=diagnostic_info,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class IsNotExistAfterOperationException(BaseCustomException):
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
subject: str,
|
|
48
|
+
operation: str,
|
|
49
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
message = f"{subject} does not exist after trying to {operation}"
|
|
52
|
+
super().__init__(
|
|
53
|
+
message_with_marking_dynamic_variables=message,
|
|
54
|
+
diagnostic_info=diagnostic_info,
|
|
55
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from python_custom_exceptions.base_custom_exception import BaseCustomException, DiagnosticInfo
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OperationException(BaseCustomException):
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
operation: str,
|
|
8
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
9
|
+
) -> None:
|
|
10
|
+
message = f"Operation {operation} failed"
|
|
11
|
+
super().__init__(
|
|
12
|
+
message_with_marking_dynamic_variables=message,
|
|
13
|
+
diagnostic_info=diagnostic_info,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NotOperationalException(BaseCustomException):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
subject: str,
|
|
21
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
message = f"{subject} is not Operational"
|
|
24
|
+
super().__init__(
|
|
25
|
+
message_with_marking_dynamic_variables=message,
|
|
26
|
+
diagnostic_info=diagnostic_info,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UnexpectedlySucceededOperationException(BaseCustomException):
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
operation: str,
|
|
34
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
message = f"Operation {operation} unexpectedly succeeded"
|
|
37
|
+
super().__init__(
|
|
38
|
+
message_with_marking_dynamic_variables=message,
|
|
39
|
+
diagnostic_info=diagnostic_info,
|
|
40
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from python_custom_exceptions.base_custom_exception import BaseCustomException, DiagnosticInfo
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UnsupportedOSExceptions(BaseCustomException):
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
os_name: str,
|
|
8
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
9
|
+
) -> None:
|
|
10
|
+
message = f'Unsupported OS: ["{os_name}"]'
|
|
11
|
+
super().__init__(
|
|
12
|
+
message_with_marking_dynamic_variables=message,
|
|
13
|
+
diagnostic_info=diagnostic_info,
|
|
14
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from python_custom_exceptions.base_custom_exception import BaseCustomException, DiagnosticInfo
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UnsupportedExceptions(BaseCustomException):
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
unsupported_subject: str,
|
|
8
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
9
|
+
) -> None:
|
|
10
|
+
message = f"{unsupported_subject} is unsupported"
|
|
11
|
+
super().__init__(
|
|
12
|
+
message_with_marking_dynamic_variables=message,
|
|
13
|
+
diagnostic_info=diagnostic_info,
|
|
14
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from python_custom_exceptions.base_custom_exception import BaseCustomException, DiagnosticInfo
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WrongUsageException(BaseCustomException):
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
message: str,
|
|
8
|
+
diagnostic_info: dict | DiagnosticInfo | None = None,
|
|
9
|
+
) -> None:
|
|
10
|
+
message = f"Wrong usage: {message}"
|
|
11
|
+
super().__init__(
|
|
12
|
+
message_with_marking_dynamic_variables=message,
|
|
13
|
+
diagnostic_info=diagnostic_info,
|
|
14
|
+
)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-custom-exceptions
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A collection of custom exceptions for Python projects.
|
|
5
|
+
Author: Avi Zaguri
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aviz92/python-custom-exceptions
|
|
8
|
+
Project-URL: Repository, https://github.com/aviz92/python-custom-exceptions
|
|
9
|
+
Project-URL: Issues, https://github.com/aviz92/python-custom-exceptions/issues
|
|
10
|
+
Keywords: development,python,exceptions,error-handling
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: python-base-toolkit>=1.1.3
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Python Custom Exceptions
|
|
23
|
+
A collection of structured, ready-to-use exception classes for Python projects. <br>
|
|
24
|
+
Every exception renders as JSON with its type, a human-readable message, and optional diagnostic info,
|
|
25
|
+
so errors are easy to read in logs and easy to parse by tools. <br>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
- ✅ Specific Exception Classes: Ready-made exceptions for common error scenarios.
|
|
31
|
+
- ✅ Structured Output: `str(exception)` is a JSON document with type, message and diagnostic info.
|
|
32
|
+
- ✅ Diagnostic Info: Attach typed context (`TypedDict`, `dict` or dataclass) to any exception.
|
|
33
|
+
- ✅ Single Base Class: Everything inherits from `BaseCustomException`, so one `except` catches them all.
|
|
34
|
+
- ✅ Easy Extension: Subclass `BaseCustomException` to add your own exceptions.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### Installation
|
|
39
|
+
```bash
|
|
40
|
+
uv add python-custom-exceptions
|
|
41
|
+
```
|
|
42
|
+
or with pip:
|
|
43
|
+
```bash
|
|
44
|
+
pip install python-custom-exceptions
|
|
45
|
+
```
|
|
46
|
+
Requires Python 3.12+.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### Available Exceptions
|
|
51
|
+
| Exception | Arguments | Message |
|
|
52
|
+
|-----------|-----------|---------|
|
|
53
|
+
| `BaseCustomException` | `message_with_marking_dynamic_variables` | `<message>` |
|
|
54
|
+
| `ComparisonException` | `expected_value`, `actual_value`, `subject=None` | `Expected <subject> to be [<expected>], but got [<actual>]` |
|
|
55
|
+
| `ConnectionException` | `host`, `message=None` | `Connection failed: Unable to establish a connection to the remote host ["<host>"]. ...` |
|
|
56
|
+
| `IsExistException` | `subject` | `[<subject>] is exists` |
|
|
57
|
+
| `IsNotExistException` | `subject` | `[<subject>] is not exists` |
|
|
58
|
+
| `IsExistAfterOperationException` | `subject`, `operation` | `<subject> exists after trying to <operation>` |
|
|
59
|
+
| `IsNotExistAfterOperationException` | `subject`, `operation` | `<subject> does not exist after trying to <operation>` |
|
|
60
|
+
| `OperationException` | `operation` | `Operation <operation> failed` |
|
|
61
|
+
| `NotOperationalException` | `subject` | `<subject> is not Operational` |
|
|
62
|
+
| `UnexpectedlySucceededOperationException` | `operation` | `Operation <operation> unexpectedly succeeded` |
|
|
63
|
+
| `TimedExecutionException` | `operation`, `timeout` | `Operation <operation> did not succeed within [<timeout>] seconds` |
|
|
64
|
+
| `UnsupportedExceptions` | `unsupported_subject` | `<unsupported_subject> is unsupported` |
|
|
65
|
+
| `UnsupportedOSExceptions` | `os_name` | `Unsupported OS: ["<os_name>"]` |
|
|
66
|
+
| `WrongUsageException` | `message` | `Wrong usage: <message>` |
|
|
67
|
+
|
|
68
|
+
All exceptions also accept an optional `diagnostic_info` argument.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### Usage Examples
|
|
73
|
+
|
|
74
|
+
#### Raise an exception with diagnostic info
|
|
75
|
+
```python
|
|
76
|
+
from pathlib import Path
|
|
77
|
+
|
|
78
|
+
from python_custom_exceptions import DiagnosticInfo, IsNotExistException
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class PathDiagnosticInfo(DiagnosticInfo):
|
|
82
|
+
path: str
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
path = Path("/tmp/missing")
|
|
86
|
+
if not path.exists():
|
|
87
|
+
raise IsNotExistException(
|
|
88
|
+
subject="Directory",
|
|
89
|
+
diagnostic_info=PathDiagnosticInfo(path=str(path)),
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
Output:
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"exception_type": "IsNotExistException",
|
|
96
|
+
"message": "[Directory] is not exists",
|
|
97
|
+
"diagnostic_info": {
|
|
98
|
+
"path": "/tmp/missing"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Catch any custom exception
|
|
104
|
+
```python
|
|
105
|
+
from python_custom_exceptions import BaseCustomException, ComparisonException
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
raise ComparisonException(expected_value=200, actual_value=500, subject="status code")
|
|
109
|
+
except BaseCustomException as exc:
|
|
110
|
+
print(exc.exception_type) # ComparisonException
|
|
111
|
+
print(exc.message) # Expected status code to be [200], but got [500]
|
|
112
|
+
print(exc.diagnostic_info) # {}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Define your own exception
|
|
116
|
+
```python
|
|
117
|
+
from python_custom_exceptions import BaseCustomException, DiagnosticInfo
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class QuotaExceededException(BaseCustomException):
|
|
121
|
+
def __init__(self, resource: str, limit: int, diagnostic_info: dict | DiagnosticInfo | None = None) -> None:
|
|
122
|
+
super().__init__(
|
|
123
|
+
message_with_marking_dynamic_variables=f"Quota for [{resource}] exceeded the limit of [{limit}]",
|
|
124
|
+
diagnostic_info=diagnostic_info,
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### Development
|
|
131
|
+
```bash
|
|
132
|
+
git clone https://github.com/aviz92/python-custom-exceptions.git
|
|
133
|
+
cd python-custom-exceptions
|
|
134
|
+
uv sync # create .venv and install runtime + dev dependencies
|
|
135
|
+
uv run pre-commit install # enable git hooks
|
|
136
|
+
uv run pytest # run the tests
|
|
137
|
+
uv build # build sdist + wheel into dist/
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### 🤝 Contributing
|
|
143
|
+
If you have a helpful exception, pattern, or improvement to suggest:
|
|
144
|
+
1. Fork the repo
|
|
145
|
+
2. Create a new branch
|
|
146
|
+
3. Submit a pull request
|
|
147
|
+
|
|
148
|
+
I welcome additions that promote clean, productive, and maintainable development. <br>
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### 🙏 Thanks
|
|
153
|
+
Thanks for exploring this repository! <br>
|
|
154
|
+
Happy coding! <br>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
python_custom_exceptions/__init__.py
|
|
6
|
+
python_custom_exceptions/base_custom_exception.py
|
|
7
|
+
python_custom_exceptions/comparison_exception.py
|
|
8
|
+
python_custom_exceptions/connection_exceptions.py
|
|
9
|
+
python_custom_exceptions/execution_exception.py
|
|
10
|
+
python_custom_exceptions/is_exist_exception.py
|
|
11
|
+
python_custom_exceptions/operation_exception.py
|
|
12
|
+
python_custom_exceptions/os_exceptions.py
|
|
13
|
+
python_custom_exceptions/unsupported_exception.py
|
|
14
|
+
python_custom_exceptions/wrong_exception.py
|
|
15
|
+
python_custom_exceptions.egg-info/PKG-INFO
|
|
16
|
+
python_custom_exceptions.egg-info/SOURCES.txt
|
|
17
|
+
python_custom_exceptions.egg-info/dependency_links.txt
|
|
18
|
+
python_custom_exceptions.egg-info/requires.txt
|
|
19
|
+
python_custom_exceptions.egg-info/top_level.txt
|
|
20
|
+
tests/test_diagnostic_info.py
|
|
21
|
+
tests/test_exception.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
python-base-toolkit>=1.1.3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
python_custom_exceptions
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# test_diagnostic_info.py
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from custom_python_logger import build_logger
|
|
7
|
+
|
|
8
|
+
from python_custom_exceptions import (
|
|
9
|
+
BaseCustomException,
|
|
10
|
+
DiagnosticInfo,
|
|
11
|
+
UnsupportedExceptions,
|
|
12
|
+
WrongUsageException,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = build_logger(project_name="custom_exceptions_test")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DiagnosticInfoTest(DiagnosticInfo):
|
|
19
|
+
error_code: int
|
|
20
|
+
message: str
|
|
21
|
+
timestamp: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DiagnosticInfoNested(DiagnosticInfo):
|
|
25
|
+
error: dict
|
|
26
|
+
context: dict
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_diagnostic_info_basic_usage():
|
|
30
|
+
"""Test basic usage of DiagnosticInfo as a dictionary"""
|
|
31
|
+
diagnostic_info = DiagnosticInfoTest(error_code=404, message="Not found", timestamp="2023-10-01T12:00:00Z")
|
|
32
|
+
|
|
33
|
+
exception = BaseCustomException(
|
|
34
|
+
message_with_marking_dynamic_variables="Test message",
|
|
35
|
+
diagnostic_info=diagnostic_info,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
assert exception.diagnostic_info == diagnostic_info
|
|
39
|
+
assert exception.diagnostic_info["error_code"] == 404
|
|
40
|
+
assert exception.diagnostic_info["message"] == "Not found"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_diagnostic_info_empty_dict():
|
|
44
|
+
"""Test DiagnosticInfo with empty dictionary"""
|
|
45
|
+
exception = BaseCustomException(message_with_marking_dynamic_variables="Test message", diagnostic_info={})
|
|
46
|
+
|
|
47
|
+
assert exception.diagnostic_info == {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_diagnostic_info_none():
|
|
51
|
+
"""Test DiagnosticInfo with None value"""
|
|
52
|
+
exception = BaseCustomException(message_with_marking_dynamic_variables="Test message", diagnostic_info=None)
|
|
53
|
+
|
|
54
|
+
assert exception.diagnostic_info == {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_diagnostic_info_nested_structure():
|
|
58
|
+
"""Test DiagnosticInfo with nested dictionary structure"""
|
|
59
|
+
diagnostic_info = DiagnosticInfoNested(
|
|
60
|
+
error={
|
|
61
|
+
"code": 500,
|
|
62
|
+
"type": "Internal Server Error",
|
|
63
|
+
"details": {"component": "database", "operation": "query"},
|
|
64
|
+
},
|
|
65
|
+
context={"user_id": 12345, "session_id": "abc123"},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
exception = BaseCustomException(
|
|
69
|
+
message_with_marking_dynamic_variables="Complex error",
|
|
70
|
+
diagnostic_info=diagnostic_info,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
assert exception.diagnostic_info["error"]["code"] == 500
|
|
74
|
+
assert exception.diagnostic_info["context"]["user_id"] == 12345
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_diagnostic_info_with_dataclass():
|
|
78
|
+
"""Test DiagnosticInfo with dataclass object"""
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class ErrorDetails:
|
|
82
|
+
code: int
|
|
83
|
+
message: str
|
|
84
|
+
component: str
|
|
85
|
+
|
|
86
|
+
error_details = ErrorDetails(code=400, message="Bad Request", component="api")
|
|
87
|
+
|
|
88
|
+
exception = BaseCustomException(
|
|
89
|
+
message_with_marking_dynamic_variables="Dataclass test",
|
|
90
|
+
diagnostic_info=error_details.__dict__,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# The dataclass should be converted to dict
|
|
94
|
+
assert exception.diagnostic_info == {
|
|
95
|
+
"code": 400,
|
|
96
|
+
"message": "Bad Request",
|
|
97
|
+
"component": "api",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_diagnostic_info_with_mixed_types():
|
|
102
|
+
"""Test DiagnosticInfo with various data types"""
|
|
103
|
+
diagnostic_info = {
|
|
104
|
+
"string": "test",
|
|
105
|
+
"integer": 42,
|
|
106
|
+
"float": 3.14,
|
|
107
|
+
"boolean": True,
|
|
108
|
+
"list": [1, 2, 3],
|
|
109
|
+
"tuple": (1, 2, 3),
|
|
110
|
+
"set": {1, 2, 3},
|
|
111
|
+
"none": None,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
exception = BaseCustomException(
|
|
115
|
+
message_with_marking_dynamic_variables="Mixed types test",
|
|
116
|
+
diagnostic_info=diagnostic_info,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
assert exception.diagnostic_info["string"] == "test"
|
|
120
|
+
assert exception.diagnostic_info["integer"] == 42
|
|
121
|
+
assert exception.diagnostic_info["float"] == 3.14
|
|
122
|
+
assert exception.diagnostic_info["boolean"] is True
|
|
123
|
+
assert exception.diagnostic_info["list"] == [1, 2, 3]
|
|
124
|
+
assert exception.diagnostic_info["tuple"] == (1, 2, 3)
|
|
125
|
+
assert exception.diagnostic_info["set"] == {1, 2, 3}
|
|
126
|
+
assert exception.diagnostic_info["none"] is None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_diagnostic_info_serialization_in_str():
|
|
130
|
+
"""Test that DiagnosticInfo is properly serialized in exception string representation"""
|
|
131
|
+
diagnostic_info = {"error_code": 500, "component": "database"}
|
|
132
|
+
|
|
133
|
+
exception = BaseCustomException(
|
|
134
|
+
message_with_marking_dynamic_variables="Serialization test",
|
|
135
|
+
diagnostic_info=diagnostic_info,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
str_repr = str(exception)
|
|
139
|
+
assert "error_code" in str_repr
|
|
140
|
+
assert "500" in str_repr
|
|
141
|
+
assert "component" in str_repr
|
|
142
|
+
assert "database" in str_repr
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_diagnostic_info_in_existing_exceptions():
|
|
146
|
+
"""Test DiagnosticInfo usage in existing exception classes"""
|
|
147
|
+
# Test with UnsupportedExceptions
|
|
148
|
+
with pytest.raises(UnsupportedExceptions) as exc_info:
|
|
149
|
+
raise UnsupportedExceptions(
|
|
150
|
+
unsupported_subject="FeatureX",
|
|
151
|
+
diagnostic_info={"feature_id": "X123", "version": "2.0"},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
assert exc_info.value.diagnostic_info["feature_id"] == "X123"
|
|
155
|
+
assert exc_info.value.diagnostic_info["version"] == "2.0"
|
|
156
|
+
|
|
157
|
+
# Test with WrongUsageException
|
|
158
|
+
with pytest.raises(WrongUsageException) as exc_info:
|
|
159
|
+
raise WrongUsageException(
|
|
160
|
+
message="Invalid parameter",
|
|
161
|
+
diagnostic_info={"param_name": "user_id", "expected_type": "int"},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
assert exc_info.value.diagnostic_info["param_name"] == "user_id"
|
|
165
|
+
assert exc_info.value.diagnostic_info["expected_type"] == "int"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_diagnostic_info_type_annotation():
|
|
169
|
+
"""Test that DiagnosticInfo can be properly typed"""
|
|
170
|
+
|
|
171
|
+
def create_diagnostic_info() -> dict:
|
|
172
|
+
return {"timestamp": "2023-01-01T00:00:00Z", "request_id": "req-123"}
|
|
173
|
+
|
|
174
|
+
diagnostic_info = create_diagnostic_info()
|
|
175
|
+
exception = BaseCustomException(
|
|
176
|
+
message_with_marking_dynamic_variables="Type annotation test",
|
|
177
|
+
diagnostic_info=diagnostic_info,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
assert exception.diagnostic_info["timestamp"] == "2023-01-01T00:00:00Z"
|
|
181
|
+
assert exception.diagnostic_info["request_id"] == "req-123"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_diagnostic_info_edge_cases():
|
|
185
|
+
"""Test DiagnosticInfo with edge cases"""
|
|
186
|
+
# Empty string keys
|
|
187
|
+
diagnostic_info = {"": "empty key", "normal_key": "normal value"}
|
|
188
|
+
|
|
189
|
+
exception = BaseCustomException(
|
|
190
|
+
message_with_marking_dynamic_variables="Edge case test",
|
|
191
|
+
diagnostic_info=diagnostic_info,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
assert exception.diagnostic_info[""] == "empty key"
|
|
195
|
+
assert exception.diagnostic_info["normal_key"] == "normal value"
|
|
196
|
+
|
|
197
|
+
# Very long values
|
|
198
|
+
long_string = "x" * 1000
|
|
199
|
+
diagnostic_info_long = {"long_value": long_string}
|
|
200
|
+
|
|
201
|
+
exception_long = BaseCustomException(
|
|
202
|
+
message_with_marking_dynamic_variables="Long value test",
|
|
203
|
+
diagnostic_info=diagnostic_info_long,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
assert exception_long.diagnostic_info["long_value"] == long_string
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from custom_python_logger import build_logger
|
|
3
|
+
|
|
4
|
+
from python_custom_exceptions import (
|
|
5
|
+
ComparisonException,
|
|
6
|
+
ConnectionException,
|
|
7
|
+
IsExistAfterOperationException,
|
|
8
|
+
IsNotExistAfterOperationException,
|
|
9
|
+
NotOperationalException,
|
|
10
|
+
OperationException,
|
|
11
|
+
TimedExecutionException,
|
|
12
|
+
UnexpectedlySucceededOperationException,
|
|
13
|
+
UnsupportedExceptions,
|
|
14
|
+
UnsupportedOSExceptions,
|
|
15
|
+
WrongUsageException,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = build_logger(project_name="custom_exceptions_test")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_unsupported_exceptions():
|
|
22
|
+
with pytest.raises(UnsupportedExceptions) as exc_info:
|
|
23
|
+
raise UnsupportedExceptions(unsupported_subject="FeatureX", diagnostic_info={"code": 123})
|
|
24
|
+
logger.info(f"\n{exc_info.value}") # noqa
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_wrong_usage_exception():
|
|
28
|
+
with pytest.raises(WrongUsageException) as exc_info:
|
|
29
|
+
raise WrongUsageException(message="bad call", diagnostic_info={"hint": "use method X"})
|
|
30
|
+
logger.info(f"\n{exc_info.value}") # noqa
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_comparison_exception():
|
|
34
|
+
with pytest.raises(ComparisonException) as exc_info:
|
|
35
|
+
raise ComparisonException(
|
|
36
|
+
subject="value",
|
|
37
|
+
expected_value=42,
|
|
38
|
+
actual_value=43,
|
|
39
|
+
diagnostic_info={"context": "comparison test"},
|
|
40
|
+
)
|
|
41
|
+
logger.info(f"\n{exc_info.value}") # noqa
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_timed_execution_exception():
|
|
45
|
+
with pytest.raises(TimedExecutionException) as exc_info:
|
|
46
|
+
raise TimedExecutionException(operation="fetch_data", timeout=30, diagnostic_info={"timeout_code": "T30"})
|
|
47
|
+
logger.info(f"\n{exc_info.value}") # noqa
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_operation_exception():
|
|
51
|
+
with pytest.raises(OperationException) as exc_info:
|
|
52
|
+
raise OperationException(operation="save file", diagnostic_info={"reason": "disk full"})
|
|
53
|
+
logger.info(f"\n{exc_info.value}") # noqa
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_not_operational_exception():
|
|
57
|
+
with pytest.raises(NotOperationalException) as exc_info:
|
|
58
|
+
raise NotOperationalException(subject="ServiceX", diagnostic_info={"uptime": "0s"})
|
|
59
|
+
logger.info(f"\n{exc_info.value}") # noqa
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_exist_after_operation_exception():
|
|
63
|
+
with pytest.raises(IsExistAfterOperationException) as exc_info:
|
|
64
|
+
raise IsExistAfterOperationException(subject="file.txt", operation="delete", diagnostic_info={"path": "/tmp"})
|
|
65
|
+
logger.info(f"\n{exc_info.value}") # noqa
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_not_exist_after_operation_exception():
|
|
69
|
+
with pytest.raises(IsNotExistAfterOperationException) as exc_info:
|
|
70
|
+
raise IsNotExistAfterOperationException(
|
|
71
|
+
subject="folder",
|
|
72
|
+
operation="create",
|
|
73
|
+
diagnostic_info={"path": "/tmp/folder"},
|
|
74
|
+
)
|
|
75
|
+
logger.info(f"\n{exc_info.value}") # noqa
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_unexpectedly_succeeded_operation_exception():
|
|
79
|
+
with pytest.raises(UnexpectedlySucceededOperationException) as exc_info:
|
|
80
|
+
raise UnexpectedlySucceededOperationException(operation="fail_test", diagnostic_info={"status": "success"})
|
|
81
|
+
logger.info(f"\n{exc_info.value}") # noqa
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_unsupported_os_exceptions():
|
|
85
|
+
with pytest.raises(UnsupportedOSExceptions) as exc_info:
|
|
86
|
+
raise UnsupportedOSExceptions(os_name="Solaris", diagnostic_info={"os_version": "11.4"})
|
|
87
|
+
logger.info(f"\n{exc_info.value}") # noqa
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_connection_exception():
|
|
91
|
+
with pytest.raises(ConnectionException) as exc_info:
|
|
92
|
+
raise ConnectionException(
|
|
93
|
+
host="Database",
|
|
94
|
+
message="Connection failed",
|
|
95
|
+
diagnostic_info={"host": "db.example.com"},
|
|
96
|
+
)
|
|
97
|
+
logger.info(f"\n{exc_info.value}") # noqa
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_connection_exception_without_message():
|
|
101
|
+
with pytest.raises(ConnectionException) as exc_info:
|
|
102
|
+
raise ConnectionException(host="Database", diagnostic_info={"host": "db.example.com"})
|
|
103
|
+
logger.info(f"\n{exc_info.value}") # noqa
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-custom-exceptions
|
|
3
|
-
Version: 0.0.1
|
|
4
|
-
Summary: A collection of custom exceptions for Python projects.
|
|
5
|
-
Home-page: https://github.com/aviz92/python-custom-exceptions
|
|
6
|
-
Author: Avi Zaguri
|
|
7
|
-
Author-email:
|
|
8
|
-
Project-URL: Repository, https://github.com/aviz92/python-custom-exceptions
|
|
9
|
-
Requires-Python: >=3.11
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Requires-Dist: setuptools
|
|
13
|
-
Requires-Dist: wheel
|
|
14
|
-
Requires-Dist: colorlog
|
|
15
|
-
Requires-Dist: pytest
|
|
16
|
-
Requires-Dist: pathlib
|
|
17
|
-
Requires-Dist: custom-python-logger
|
|
18
|
-
Dynamic: author
|
|
19
|
-
Dynamic: description
|
|
20
|
-
Dynamic: description-content-type
|
|
21
|
-
Dynamic: home-page
|
|
22
|
-
Dynamic: license-file
|
|
23
|
-
Dynamic: project-url
|
|
24
|
-
Dynamic: requires-dist
|
|
25
|
-
Dynamic: requires-python
|
|
26
|
-
Dynamic: summary
|
|
27
|
-
|
|
28
|
-
# Python Custom Exceptions
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## 🤝 Contributing
|
|
33
|
-
If you have a helpful tool, pattern, or improvement to suggest:
|
|
34
|
-
Fork the repo <br>
|
|
35
|
-
Create a new branch <br>
|
|
36
|
-
Submit a pull request <br>
|
|
37
|
-
I welcome additions that promote clean, productive, and maintainable development. <br>
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## 🙏 Thanks
|
|
42
|
-
Thanks for exploring this repository! <br>
|
|
43
|
-
Happy coding! <br>
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# Python Custom Exceptions
|
|
2
|
-
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
## 🤝 Contributing
|
|
6
|
-
If you have a helpful tool, pattern, or improvement to suggest:
|
|
7
|
-
Fork the repo <br>
|
|
8
|
-
Create a new branch <br>
|
|
9
|
-
Submit a pull request <br>
|
|
10
|
-
I welcome additions that promote clean, productive, and maintainable development. <br>
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## 🙏 Thanks
|
|
15
|
-
Thanks for exploring this repository! <br>
|
|
16
|
-
Happy coding! <br>
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-custom-exceptions
|
|
3
|
-
Version: 0.0.1
|
|
4
|
-
Summary: A collection of custom exceptions for Python projects.
|
|
5
|
-
Home-page: https://github.com/aviz92/python-custom-exceptions
|
|
6
|
-
Author: Avi Zaguri
|
|
7
|
-
Author-email:
|
|
8
|
-
Project-URL: Repository, https://github.com/aviz92/python-custom-exceptions
|
|
9
|
-
Requires-Python: >=3.11
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Requires-Dist: setuptools
|
|
13
|
-
Requires-Dist: wheel
|
|
14
|
-
Requires-Dist: colorlog
|
|
15
|
-
Requires-Dist: pytest
|
|
16
|
-
Requires-Dist: pathlib
|
|
17
|
-
Requires-Dist: custom-python-logger
|
|
18
|
-
Dynamic: author
|
|
19
|
-
Dynamic: description
|
|
20
|
-
Dynamic: description-content-type
|
|
21
|
-
Dynamic: home-page
|
|
22
|
-
Dynamic: license-file
|
|
23
|
-
Dynamic: project-url
|
|
24
|
-
Dynamic: requires-dist
|
|
25
|
-
Dynamic: requires-python
|
|
26
|
-
Dynamic: summary
|
|
27
|
-
|
|
28
|
-
# Python Custom Exceptions
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## 🤝 Contributing
|
|
33
|
-
If you have a helpful tool, pattern, or improvement to suggest:
|
|
34
|
-
Fork the repo <br>
|
|
35
|
-
Create a new branch <br>
|
|
36
|
-
Submit a pull request <br>
|
|
37
|
-
I welcome additions that promote clean, productive, and maintainable development. <br>
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## 🙏 Thanks
|
|
42
|
-
Thanks for exploring this repository! <br>
|
|
43
|
-
Happy coding! <br>
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
MANIFEST.in
|
|
3
|
-
README.md
|
|
4
|
-
pyproject.toml
|
|
5
|
-
requirements.txt
|
|
6
|
-
setup.py
|
|
7
|
-
python_custom_exceptions.egg-info/PKG-INFO
|
|
8
|
-
python_custom_exceptions.egg-info/SOURCES.txt
|
|
9
|
-
python_custom_exceptions.egg-info/dependency_links.txt
|
|
10
|
-
python_custom_exceptions.egg-info/requires.txt
|
|
11
|
-
python_custom_exceptions.egg-info/top_level.txt
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
from setuptools import setup, find_packages
|
|
2
|
-
|
|
3
|
-
package_version = '0.0.1'
|
|
4
|
-
|
|
5
|
-
package_name = 'python-custom-exceptions'
|
|
6
|
-
package_description = 'A collection of custom exceptions for Python projects.'
|
|
7
|
-
|
|
8
|
-
package_name_ = package_name.replace('-', '_')
|
|
9
|
-
package_long_description_content_type = 'text/markdown'
|
|
10
|
-
package_url = f'https://github.com/aviz92/{package_name}'
|
|
11
|
-
package_python_requires = '>=3.11'
|
|
12
|
-
package_author = 'Avi Zaguri'
|
|
13
|
-
|
|
14
|
-
with open('requirements.txt', 'r') as file:
|
|
15
|
-
package_install_requires = [
|
|
16
|
-
line.strip() for line in file.readlines() if line.strip() and not line.startswith('#')
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
with open('README.md', 'r') as file:
|
|
20
|
-
package_long_description = file.read()
|
|
21
|
-
|
|
22
|
-
setup(
|
|
23
|
-
name=package_name,
|
|
24
|
-
version=package_version,
|
|
25
|
-
packages=find_packages(include=[package_name_, f'{package_name_}.*']),
|
|
26
|
-
install_requires=package_install_requires,
|
|
27
|
-
author=package_author,
|
|
28
|
-
author_email='',
|
|
29
|
-
description=package_description,
|
|
30
|
-
long_description=package_long_description,
|
|
31
|
-
long_description_content_type=package_long_description_content_type,
|
|
32
|
-
url=package_url,
|
|
33
|
-
project_urls={
|
|
34
|
-
'Repository': package_url,
|
|
35
|
-
},
|
|
36
|
-
python_requires=package_python_requires,
|
|
37
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|