DataExcept 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataexcept/__init__.py +81 -0
- dataexcept/__main__.py +74 -0
- dataexcept/database_exceptions.py +71 -0
- dataexcept/dataengineering_exceptions.py +125 -0
- dataexcept/datascience_exceptions/__init__.py +85 -0
- dataexcept/datascience_exceptions/base.py +17 -0
- dataexcept/datascience_exceptions/ingestion.py +312 -0
- dataexcept/datascience_exceptions/operations.py +130 -0
- dataexcept/datascience_exceptions/training.py +508 -0
- dataexcept/exceptions/__init__.py +36 -0
- dataexcept/exceptions/authentication.py +21 -0
- dataexcept/exceptions/base.py +4 -0
- dataexcept/exceptions/configuration.py +10 -0
- dataexcept/exceptions/external.py +42 -0
- dataexcept/exceptions/lifecycle.py +13 -0
- dataexcept/exceptions/notification.py +49 -0
- dataexcept/exceptions/parsing.py +31 -0
- dataexcept/exceptions/scheduling.py +21 -0
- dataexcept/exceptions/validation.py +12 -0
- dataexcept/io_exceptions.py +66 -0
- dataexcept/job_exceptions.py +59 -0
- dataexcept/logging_helpers.py +76 -0
- dataexcept/network_exceptions.py +102 -0
- dataexcept/pandas_exceptions.py +143 -0
- dataexcept/pipeline_exceptions.py +205 -0
- dataexcept/py.typed +1 -0
- dataexcept/security_exceptions.py +68 -0
- dataexcept-0.1.0.dist-info/METADATA +397 -0
- dataexcept-0.1.0.dist-info/RECORD +32 -0
- dataexcept-0.1.0.dist-info/WHEEL +4 -0
- dataexcept-0.1.0.dist-info/entry_points.txt +3 -0
- dataexcept-0.1.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# parsing.py
|
|
2
|
+
from .base import JobError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ParsingError(JobError):
|
|
6
|
+
"""Raised when parsing of input data fails."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, text: str, message: str | None = None):
|
|
9
|
+
self.text = text
|
|
10
|
+
self.message = message or f"Failed to parse text: {text!r}"
|
|
11
|
+
super().__init__(self.message)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SerializationError(JobError):
|
|
15
|
+
"""Raised when serialization of an object fails."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, obj, format: str, message: str | None = None):
|
|
18
|
+
self.obj = obj
|
|
19
|
+
self.format = format
|
|
20
|
+
self.message = message or f"Failed to serialize object to {format}"
|
|
21
|
+
super().__init__(self.message)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DeserializationError(JobError):
|
|
25
|
+
"""Raised when deserialization of data fails."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, data: bytes, format: str, message: str | None = None):
|
|
28
|
+
self.data = data
|
|
29
|
+
self.format = format
|
|
30
|
+
self.message = message or f"Failed to deserialize data from {format}"
|
|
31
|
+
super().__init__(self.message)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# scheduling.py
|
|
2
|
+
from .base import JobError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ScheduleConflictError(JobError):
|
|
6
|
+
"""Raised when two jobs have conflicting schedules."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, job_name: str, schedule: str):
|
|
9
|
+
self.job_name = job_name
|
|
10
|
+
self.schedule = schedule
|
|
11
|
+
msg = f"Schedule conflict for job '{job_name}' on schedule '{schedule}'"
|
|
12
|
+
super().__init__(msg)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CronExpressionError(JobError):
|
|
16
|
+
"""Raised when a cron expression is invalid."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, expression: str, message: str | None = None):
|
|
19
|
+
self.expression = expression
|
|
20
|
+
self.message = message or f"Invalid cron expression: '{expression}'"
|
|
21
|
+
super().__init__(self.message)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# validation.py
|
|
2
|
+
from .base import JobError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ValidationError(JobError):
|
|
6
|
+
"""Raised when input data fails validation."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, field: str, value, message: str | None = None):
|
|
9
|
+
self.field = field
|
|
10
|
+
self.value = value
|
|
11
|
+
self.message = message or f"Validation failed for field '{field}': {value!r}"
|
|
12
|
+
super().__init__(self.message)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Custom exceptions for file and I/O operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CustomIOError(Exception):
|
|
7
|
+
"""Base exception for I/O errors."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FileReadError(CustomIOError):
|
|
13
|
+
"""Raised when reading a file fails."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, path: str, original: Exception | None = None) -> None:
|
|
16
|
+
"""Initialize FileReadError.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
path: File path that could not be read.
|
|
20
|
+
original: Optional underlying exception.
|
|
21
|
+
"""
|
|
22
|
+
self.path = path
|
|
23
|
+
self.original = original
|
|
24
|
+
msg = f"Failed to read file '{path}'"
|
|
25
|
+
if original:
|
|
26
|
+
msg += f": {original}"
|
|
27
|
+
super().__init__(msg)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FileWriteError(CustomIOError):
|
|
31
|
+
"""Raised when writing to a file fails."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, path: str, original: Exception | None = None) -> None:
|
|
34
|
+
"""Initialize FileWriteError.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
path: File path that could not be written to.
|
|
38
|
+
original: Optional underlying exception.
|
|
39
|
+
"""
|
|
40
|
+
self.path = path
|
|
41
|
+
self.original = original
|
|
42
|
+
msg = f"Failed to write file '{path}'"
|
|
43
|
+
if original:
|
|
44
|
+
msg += f": {original}"
|
|
45
|
+
super().__init__(msg)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FileLockError(CustomIOError):
|
|
49
|
+
"""Raised when a file lock cannot be acquired."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, path: str) -> None:
|
|
52
|
+
"""Initialize FileLockError.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
path: Path of the lock file.
|
|
56
|
+
"""
|
|
57
|
+
self.path = path
|
|
58
|
+
super().__init__(f"Unable to obtain lock for '{path}'")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"CustomIOError",
|
|
63
|
+
"FileReadError",
|
|
64
|
+
"FileWriteError",
|
|
65
|
+
"FileLockError",
|
|
66
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Backward compatible job-related exceptions.
|
|
2
|
+
|
|
3
|
+
This module re-exports the core job exceptions defined in
|
|
4
|
+
:mod:`dataexcept.exceptions`. Applications should import from that
|
|
5
|
+
package directly going forward. Importing from :mod:`dataexcept.job_exceptions`
|
|
6
|
+
will raise a :class:`DeprecationWarning`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import warnings
|
|
12
|
+
|
|
13
|
+
from .exceptions import (
|
|
14
|
+
AuthenticationError,
|
|
15
|
+
AuthorizationError,
|
|
16
|
+
ConfigurationError,
|
|
17
|
+
ConnectionError,
|
|
18
|
+
CronExpressionError,
|
|
19
|
+
DependencyError,
|
|
20
|
+
DeserializationError,
|
|
21
|
+
EmailError,
|
|
22
|
+
JobCancellationError,
|
|
23
|
+
JobError,
|
|
24
|
+
NotificationError,
|
|
25
|
+
ParsingError,
|
|
26
|
+
ResourceNotFoundError,
|
|
27
|
+
ScheduleConflictError,
|
|
28
|
+
SerializationError,
|
|
29
|
+
TimeoutError,
|
|
30
|
+
ValidationError,
|
|
31
|
+
WebhookError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"JobError",
|
|
36
|
+
"ValidationError",
|
|
37
|
+
"ConfigurationError",
|
|
38
|
+
"ConnectionError",
|
|
39
|
+
"TimeoutError",
|
|
40
|
+
"ResourceNotFoundError",
|
|
41
|
+
"DependencyError",
|
|
42
|
+
"AuthenticationError",
|
|
43
|
+
"AuthorizationError",
|
|
44
|
+
"ParsingError",
|
|
45
|
+
"SerializationError",
|
|
46
|
+
"DeserializationError",
|
|
47
|
+
"ScheduleConflictError",
|
|
48
|
+
"CronExpressionError",
|
|
49
|
+
"NotificationError",
|
|
50
|
+
"EmailError",
|
|
51
|
+
"WebhookError",
|
|
52
|
+
"JobCancellationError",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
warnings.warn(
|
|
56
|
+
"dataexcept.job_exceptions is deprecated; use dataexcept.exceptions instead",
|
|
57
|
+
DeprecationWarning,
|
|
58
|
+
stacklevel=2,
|
|
59
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Helper functions for logging exceptions consistently."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Iterator, Mapping, Optional
|
|
9
|
+
|
|
10
|
+
Context = Mapping[str, Any]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _normalize_context_value(value: Any) -> Any:
|
|
14
|
+
try:
|
|
15
|
+
json.dumps(value)
|
|
16
|
+
return value
|
|
17
|
+
except TypeError:
|
|
18
|
+
try:
|
|
19
|
+
return json.loads(json.dumps(value, default=str))
|
|
20
|
+
except Exception: # pragma: no cover - extremely defensive
|
|
21
|
+
return repr(value)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _build_extra(context: Context | None) -> dict[str, Any] | None:
|
|
25
|
+
if not context:
|
|
26
|
+
return None
|
|
27
|
+
serialized = {
|
|
28
|
+
key: _normalize_context_value(value) for key, value in context.items()
|
|
29
|
+
}
|
|
30
|
+
return {"dataexcept_context": serialized}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def log_exception(
|
|
34
|
+
exc: Exception,
|
|
35
|
+
logger: Optional[logging.Logger] = None,
|
|
36
|
+
level: int = logging.ERROR,
|
|
37
|
+
context: Context | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Log *exc* at the given log *level* using *logger*.
|
|
40
|
+
|
|
41
|
+
If *logger* is ``None`` a module level logger is used.
|
|
42
|
+
"""
|
|
43
|
+
if logger is None:
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
exc_info = (type(exc), exc, exc.__traceback__)
|
|
46
|
+
logger.log(level, "%s", exc, exc_info=exc_info, extra=_build_extra(context))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@contextlib.contextmanager
|
|
50
|
+
def log_and_raise(
|
|
51
|
+
logger: Optional[logging.Logger] = None,
|
|
52
|
+
level: int = logging.ERROR,
|
|
53
|
+
context: Context | None = None,
|
|
54
|
+
) -> Iterator[None]:
|
|
55
|
+
"""Context manager that logs and re-raises exceptions preserving traceback."""
|
|
56
|
+
try:
|
|
57
|
+
yield
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
log_exception(exc, logger=logger, level=level, context=context)
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def log_then_raise(
|
|
64
|
+
exc: Exception,
|
|
65
|
+
logger: Optional[logging.Logger] = None,
|
|
66
|
+
level: int = logging.ERROR,
|
|
67
|
+
context: Context | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Log *exc* and immediately raise it.
|
|
70
|
+
|
|
71
|
+
This helper mirrors the pre-context-manager API for scenarios where adding a
|
|
72
|
+
``with`` block would be too intrusive. Prefer :func:`log_and_raise` whenever
|
|
73
|
+
possible so tracebacks remain untouched.
|
|
74
|
+
"""
|
|
75
|
+
log_exception(exc, logger=logger, level=level, context=context)
|
|
76
|
+
raise exc
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Custom exceptions for network operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NetworkError(Exception):
|
|
7
|
+
"""Base exception for network-related errors.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
>>> from dataexcept.network_exceptions import NetworkError
|
|
11
|
+
>>> try:
|
|
12
|
+
... raise NetworkError("Something went wrong")
|
|
13
|
+
... except NetworkError:
|
|
14
|
+
... print("Caught network error")
|
|
15
|
+
Caught network error
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HostUnreachableError(NetworkError):
|
|
22
|
+
"""Raised when a remote host cannot be reached.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> from dataexcept.network_exceptions import HostUnreachableError
|
|
26
|
+
>>> try:
|
|
27
|
+
... raise HostUnreachableError("api.example.com")
|
|
28
|
+
... except HostUnreachableError as exc:
|
|
29
|
+
... print(exc)
|
|
30
|
+
Host 'api.example.com' is unreachable
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, host: str, message: str | None = None) -> None:
|
|
34
|
+
"""Initialize HostUnreachableError.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
host: Host address that could not be reached.
|
|
38
|
+
message: Optional custom error message.
|
|
39
|
+
"""
|
|
40
|
+
self.host = host
|
|
41
|
+
default = f"Host '{host}' is unreachable"
|
|
42
|
+
super().__init__(message or default)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConnectionTimeoutError(NetworkError):
|
|
46
|
+
"""Raised when a network connection attempt times out.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> from dataexcept.network_exceptions import ConnectionTimeoutError
|
|
50
|
+
>>> try:
|
|
51
|
+
... raise ConnectionTimeoutError("api.example.com", 30)
|
|
52
|
+
... except ConnectionTimeoutError as exc:
|
|
53
|
+
... print(exc)
|
|
54
|
+
Connection to 'api.example.com' timed out after 30 seconds
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, host: str, timeout: float) -> None:
|
|
58
|
+
"""Initialize ConnectionTimeoutError.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
host: Host address.
|
|
62
|
+
timeout: Timeout in seconds.
|
|
63
|
+
"""
|
|
64
|
+
self.host = host
|
|
65
|
+
self.timeout = timeout
|
|
66
|
+
msg = f"Connection to '{host}' timed out after {timeout} seconds"
|
|
67
|
+
super().__init__(msg)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ProtocolError(NetworkError):
|
|
71
|
+
"""Raised when an unexpected protocol error occurs.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> from dataexcept.network_exceptions import ProtocolError
|
|
75
|
+
>>> try:
|
|
76
|
+
... raise ProtocolError("HTTP", "Invalid status line")
|
|
77
|
+
... except ProtocolError as exc:
|
|
78
|
+
... print(exc)
|
|
79
|
+
Protocol error in HTTP: Invalid status line
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, protocol: str, details: str | None = None) -> None:
|
|
83
|
+
"""Initialize ProtocolError.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
protocol: Protocol name (e.g., HTTP).
|
|
87
|
+
details: Optional additional details about the failure.
|
|
88
|
+
"""
|
|
89
|
+
self.protocol = protocol
|
|
90
|
+
self.details = details
|
|
91
|
+
msg = f"Protocol error in {protocol}"
|
|
92
|
+
if details:
|
|
93
|
+
msg += f": {details}"
|
|
94
|
+
super().__init__(msg)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = [
|
|
98
|
+
"NetworkError",
|
|
99
|
+
"HostUnreachableError",
|
|
100
|
+
"ConnectionTimeoutError",
|
|
101
|
+
"ProtocolError",
|
|
102
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Custom exceptions for pandas DataFrame operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Sequence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PandasError(Exception):
|
|
9
|
+
"""Base exception for pandas-related errors."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MissingColumnError(PandasError):
|
|
13
|
+
"""Raised when a required DataFrame column is missing.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
column: Name of the missing column.
|
|
17
|
+
dataframe: Optional name of the DataFrame being inspected.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, column: str, dataframe: Optional[str] = None) -> None:
|
|
21
|
+
if not isinstance(column, str):
|
|
22
|
+
raise TypeError(f"column must be str, got {type(column).__name__}")
|
|
23
|
+
if dataframe is not None and not isinstance(dataframe, str):
|
|
24
|
+
raise TypeError(
|
|
25
|
+
"dataframe must be str or None, " f"got {type(dataframe).__name__}"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
self.column = column
|
|
29
|
+
self.dataframe = dataframe
|
|
30
|
+
name = f" in DataFrame '{dataframe}'" if dataframe else ""
|
|
31
|
+
msg = f"Missing required column '{column}'{name}"
|
|
32
|
+
super().__init__(msg)
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
return f"[MissingColumnError] {self.args[0]}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DtypeMismatchError(PandasError):
|
|
39
|
+
"""Raised when a column has an unexpected dtype.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
column: Name of the column.
|
|
43
|
+
expected: Sequence of allowed dtypes.
|
|
44
|
+
found: Detected dtype for the column.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, column: str, expected: Sequence[str], found: str) -> None:
|
|
48
|
+
if not isinstance(column, str):
|
|
49
|
+
raise TypeError(f"column must be str, got {type(column).__name__}")
|
|
50
|
+
if not isinstance(found, str):
|
|
51
|
+
raise TypeError(f"found must be str, got {type(found).__name__}")
|
|
52
|
+
if not isinstance(expected, Sequence) or isinstance(expected, str):
|
|
53
|
+
raise TypeError("expected must be a sequence of strings")
|
|
54
|
+
if not all(isinstance(dt, str) for dt in expected):
|
|
55
|
+
raise TypeError("expected must contain strings")
|
|
56
|
+
|
|
57
|
+
self.column = column
|
|
58
|
+
self.expected = list(expected)
|
|
59
|
+
self.found = found
|
|
60
|
+
expected_fmt = ", ".join(self.expected)
|
|
61
|
+
msg = f"Column '{column}' has dtype {found}; expected {expected_fmt}"
|
|
62
|
+
super().__init__(msg)
|
|
63
|
+
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
return f"[DtypeMismatchError:{self.column}] {self.args[0]}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class IndexAlignmentError(PandasError):
|
|
69
|
+
"""Raised when DataFrame indices are misaligned for an operation.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
details: Optional details about the misalignment.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, details: Optional[str] = None) -> None:
|
|
76
|
+
if details is not None and not isinstance(details, str):
|
|
77
|
+
raise TypeError(
|
|
78
|
+
f"details must be str or None, got {type(details).__name__}"
|
|
79
|
+
)
|
|
80
|
+
msg = "DataFrame indices are misaligned"
|
|
81
|
+
if details:
|
|
82
|
+
msg += f": {details}"
|
|
83
|
+
self.details = details
|
|
84
|
+
super().__init__(msg)
|
|
85
|
+
|
|
86
|
+
def __str__(self) -> str:
|
|
87
|
+
return f"[IndexAlignmentError] {self.args[0]}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class MergeKeyError(PandasError):
|
|
91
|
+
"""Raised when merging DataFrames fails due to key issues.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
left_keys: Keys from the left DataFrame.
|
|
95
|
+
right_keys: Keys from the right DataFrame.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, left_keys: Sequence[str], right_keys: Sequence[str]) -> None:
|
|
99
|
+
if not all(isinstance(k, str) for k in left_keys):
|
|
100
|
+
raise TypeError("left_keys must be a sequence of strings")
|
|
101
|
+
if not all(isinstance(k, str) for k in right_keys):
|
|
102
|
+
raise TypeError("right_keys must be a sequence of strings")
|
|
103
|
+
self.left_keys = list(left_keys)
|
|
104
|
+
self.right_keys = list(right_keys)
|
|
105
|
+
msg = f"Failed to merge on keys {self.left_keys} and {self.right_keys}"
|
|
106
|
+
super().__init__(msg)
|
|
107
|
+
|
|
108
|
+
def __str__(self) -> str:
|
|
109
|
+
return f"[MergeKeyError] {self.args[0]}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PandasIOError(PandasError):
|
|
113
|
+
"""Raised when reading from or writing to disk with pandas fails.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
path: File path involved in the operation.
|
|
117
|
+
original: The underlying exception that was raised.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, path: str, original: Exception) -> None:
|
|
121
|
+
if not isinstance(path, str):
|
|
122
|
+
raise TypeError(f"path must be str, got {type(path).__name__}")
|
|
123
|
+
if not isinstance(original, Exception):
|
|
124
|
+
raise TypeError(
|
|
125
|
+
f"original must be Exception, got {type(original).__name__}"
|
|
126
|
+
)
|
|
127
|
+
self.path = path
|
|
128
|
+
self.original = original
|
|
129
|
+
msg = f"Pandas I/O operation failed on {path!r}: {original}"
|
|
130
|
+
super().__init__(msg)
|
|
131
|
+
|
|
132
|
+
def __str__(self) -> str:
|
|
133
|
+
return f"[PandasIOError] {self.args[0]}"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
__all__ = [
|
|
137
|
+
"PandasError",
|
|
138
|
+
"MissingColumnError",
|
|
139
|
+
"DtypeMismatchError",
|
|
140
|
+
"IndexAlignmentError",
|
|
141
|
+
"MergeKeyError",
|
|
142
|
+
"PandasIOError",
|
|
143
|
+
]
|