ds-resource-plugin-py-lib 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.
- ds_resource_plugin_py_lib/__init__.py +28 -0
- ds_resource_plugin_py_lib/common/__init__.py +15 -0
- ds_resource_plugin_py_lib/common/resource/__init__.py +17 -0
- ds_resource_plugin_py_lib/common/resource/client.py +265 -0
- ds_resource_plugin_py_lib/common/resource/dataset/__init__.py +25 -0
- ds_resource_plugin_py_lib/common/resource/dataset/base.py +322 -0
- ds_resource_plugin_py_lib/common/resource/dataset/decorators.py +73 -0
- ds_resource_plugin_py_lib/common/resource/dataset/enums.py +36 -0
- ds_resource_plugin_py_lib/common/resource/dataset/errors.py +181 -0
- ds_resource_plugin_py_lib/common/resource/dataset/result.py +64 -0
- ds_resource_plugin_py_lib/common/resource/dataset/storage_format.py +80 -0
- ds_resource_plugin_py_lib/common/resource/errors.py +53 -0
- ds_resource_plugin_py_lib/common/resource/linked_service/__init__.py +18 -0
- ds_resource_plugin_py_lib/common/resource/linked_service/base.py +186 -0
- ds_resource_plugin_py_lib/common/resource/linked_service/enums.py +19 -0
- ds_resource_plugin_py_lib/common/resource/linked_service/errors.py +103 -0
- ds_resource_plugin_py_lib/common/serde/__init__.py +15 -0
- ds_resource_plugin_py_lib/common/serde/binary.py +129 -0
- ds_resource_plugin_py_lib/common/serde/deserialize/__init__.py +18 -0
- ds_resource_plugin_py_lib/common/serde/deserialize/awswrangler.py +142 -0
- ds_resource_plugin_py_lib/common/serde/deserialize/base.py +31 -0
- ds_resource_plugin_py_lib/common/serde/deserialize/pandas.py +107 -0
- ds_resource_plugin_py_lib/common/serde/serialize/__init__.py +18 -0
- ds_resource_plugin_py_lib/common/serde/serialize/awswrangler.py +115 -0
- ds_resource_plugin_py_lib/common/serde/serialize/base.py +27 -0
- ds_resource_plugin_py_lib/common/serde/serialize/pandas.py +102 -0
- ds_resource_plugin_py_lib/libs/__init__.py +14 -0
- ds_resource_plugin_py_lib/libs/utils/__init__.py +14 -0
- ds_resource_plugin_py_lib/libs/utils/import_string.py +50 -0
- ds_resource_plugin_py_lib/libs/utils/json_default.py +66 -0
- ds_resource_plugin_py_lib/libs/utils/sanitize.py +35 -0
- ds_resource_plugin_py_lib/py.typed +0 -0
- ds_resource_plugin_py_lib-0.1.0.dist-info/METADATA +140 -0
- ds_resource_plugin_py_lib-0.1.0.dist-info/RECORD +37 -0
- ds_resource_plugin_py_lib-0.1.0.dist-info/WHEEL +5 -0
- ds_resource_plugin_py_lib-0.1.0.dist-info/licenses/LICENSE-APACHE +201 -0
- ds_resource_plugin_py_lib-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
**File:** ``decorators.py``
|
|
3
|
+
**Region:** ``ds_resource_plugin_py_lib/common/resource/dataset``
|
|
4
|
+
|
|
5
|
+
Description
|
|
6
|
+
-----------
|
|
7
|
+
Decorators applied automatically to dataset method overrides via
|
|
8
|
+
``Dataset.__init_subclass__``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from .enums import DatasetMethod
|
|
17
|
+
from .result import OperationError, OperationInfo
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def track_result(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
21
|
+
"""
|
|
22
|
+
Wrap a dataset method so that ``self.operation`` is initialised before the
|
|
23
|
+
call and enriched afterwards.
|
|
24
|
+
|
|
25
|
+
**Auto-populated fields:**
|
|
26
|
+
|
|
27
|
+
- ``method`` -- set to the function name.
|
|
28
|
+
- ``success`` -- ``True`` when the method returns without raising.
|
|
29
|
+
- ``error`` -- structured ``OperationError`` captured from
|
|
30
|
+
``ResourceException`` on failure.
|
|
31
|
+
- ``started_at`` / ``ended_at`` / ``duration_ms`` -- wall-clock timing.
|
|
32
|
+
- ``row_count`` -- derived from ``len(self.output)`` when the
|
|
33
|
+
provider leaves the default (``0``).
|
|
34
|
+
- ``schema`` -- derived via PyArrow dtype inference on ``self.output``.
|
|
35
|
+
|
|
36
|
+
Providers may override any of these by assigning to ``self.operation``
|
|
37
|
+
inside their method body.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@functools.wraps(fn)
|
|
42
|
+
def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
43
|
+
self.operation = OperationInfo(method=DatasetMethod(fn.__name__))
|
|
44
|
+
self.operation.started_at = datetime.now(tz=UTC)
|
|
45
|
+
try:
|
|
46
|
+
fn(self, *args, **kwargs)
|
|
47
|
+
|
|
48
|
+
self.operation.success = True
|
|
49
|
+
|
|
50
|
+
if self.operation.row_count == 0 and hasattr(self.output, "__len__"):
|
|
51
|
+
self.operation.row_count = len(self.output)
|
|
52
|
+
|
|
53
|
+
if self.operation.schema is None and hasattr(self.output, "convert_dtypes"):
|
|
54
|
+
converted = self.output.convert_dtypes(dtype_backend="pyarrow")
|
|
55
|
+
self.operation.schema = {str(col): str(dtype) for col, dtype in converted.dtypes.to_dict().items()}
|
|
56
|
+
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
self.operation.success = False
|
|
59
|
+
self.operation.error = OperationError(
|
|
60
|
+
message=getattr(exc, "message", str(exc)),
|
|
61
|
+
code=getattr(exc, "code", type(exc).__name__),
|
|
62
|
+
status_code=getattr(exc, "status_code", 500),
|
|
63
|
+
details=getattr(exc, "details", {}),
|
|
64
|
+
)
|
|
65
|
+
raise
|
|
66
|
+
|
|
67
|
+
finally:
|
|
68
|
+
self.operation.ended_at = datetime.now(tz=UTC)
|
|
69
|
+
delta = self.operation.ended_at - self.operation.started_at
|
|
70
|
+
self.operation.duration_ms = round(delta.total_seconds() * 1000, 3)
|
|
71
|
+
|
|
72
|
+
wrapper._tracked = True # type: ignore[attr-defined]
|
|
73
|
+
return wrapper
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
**File:** ``enums.py``
|
|
3
|
+
**Region:** ``ds_resource_plugin_py_lib/common/resource/dataset``
|
|
4
|
+
|
|
5
|
+
Description
|
|
6
|
+
-----------
|
|
7
|
+
Enumerations for dataset operations.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DatasetMethod(StrEnum):
|
|
14
|
+
"""Allowed dataset operation names."""
|
|
15
|
+
|
|
16
|
+
CREATE = "create"
|
|
17
|
+
"""Insert rows into the target. Atomic. Not idempotent."""
|
|
18
|
+
READ = "read"
|
|
19
|
+
"""Read all data from the source into ``self.output``. Idempotent."""
|
|
20
|
+
UPDATE = "update"
|
|
21
|
+
"""Update existing rows matched by identity columns. Atomic. Idempotent."""
|
|
22
|
+
UPSERT = "upsert"
|
|
23
|
+
"""Insert or update rows matched by identity columns. Atomic. Idempotent."""
|
|
24
|
+
DELETE = "delete"
|
|
25
|
+
"""Remove specific rows matched by identity columns. Atomic. Idempotent."""
|
|
26
|
+
PURGE = "purge"
|
|
27
|
+
"""Remove all content from the target. Atomic. Idempotent."""
|
|
28
|
+
LIST = "list"
|
|
29
|
+
"""Discover available resources and populate ``self.output``. Idempotent."""
|
|
30
|
+
RENAME = "rename"
|
|
31
|
+
"""Rename a resource in the backend. Atomic. Not idempotent."""
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def all_values() -> frozenset[str]:
|
|
35
|
+
"""Return all operation method values as a frozen set."""
|
|
36
|
+
return frozenset(m.value for m in DatasetMethod)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
**File:** ``errors.py``
|
|
3
|
+
**Region:** ``ds_resource_plugin_py_lib/common/resource/dataset``
|
|
4
|
+
|
|
5
|
+
Description
|
|
6
|
+
-----------
|
|
7
|
+
Exceptions for datasets.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..errors import ResourceException
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DatasetException(ResourceException):
|
|
16
|
+
"""Base exception for all dataset-related errors."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
message: str = "Dataset operation failed",
|
|
21
|
+
code: str = "DS_DATASET_ERROR",
|
|
22
|
+
status_code: int = 500,
|
|
23
|
+
details: dict[str, Any] | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
super().__init__(message, code, status_code, details)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MismatchedLinkedServiceError(DatasetException):
|
|
29
|
+
"""Raised when a linked service does not match the dataset type."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
message: str = "Mismatched linked service",
|
|
34
|
+
code: str = "DS_DATASET_LINKED_SERVICE_MISMATCHED_ERROR",
|
|
35
|
+
status_code: int = 400,
|
|
36
|
+
details: dict[str, Any] | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
super().__init__(message, code, status_code, details)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UnsupportedDatasetTypeError(DatasetException):
|
|
42
|
+
"""Raised when a dataset type is not supported."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
message: str = "Dataset type is not supported",
|
|
47
|
+
code: str = "DS_DATASET_UNSUPPORTED_TYPE_ERROR",
|
|
48
|
+
status_code: int = 400,
|
|
49
|
+
details: dict[str, Any] | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
super().__init__(message, code, status_code, details)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class InvalidDatasetClassError(DatasetException):
|
|
55
|
+
"""Raised when a dataset type is invalid."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
message: str = "Invalid dataset type",
|
|
60
|
+
code: str = "DS_DATASET_INVALID_CLASS_ERROR",
|
|
61
|
+
status_code: int = 400,
|
|
62
|
+
details: dict[str, Any] | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
super().__init__(message, code, status_code, details)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class NotFoundError(DatasetException):
|
|
68
|
+
"""Raised when a resource is not found."""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
message: str = "Resource not found",
|
|
73
|
+
code: str = "DS_DATASET_NOT_FOUND_ERROR",
|
|
74
|
+
status_code: int = 404,
|
|
75
|
+
details: dict[str, Any] | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
super().__init__(message, code, status_code, details)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ReadError(DatasetException):
|
|
81
|
+
"""Raised when a read operation fails."""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
message: str = "Read operation failed",
|
|
86
|
+
code: str = "DS_DATASET_READ_ERROR",
|
|
87
|
+
status_code: int = 500,
|
|
88
|
+
details: dict[str, Any] | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
super().__init__(message, code, status_code, details)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CreateError(DatasetException):
|
|
94
|
+
"""Raised when a create operation fails."""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
message: str = "Create operation failed",
|
|
99
|
+
code: str = "DS_DATASET_CREATE_ERROR",
|
|
100
|
+
status_code: int = 500,
|
|
101
|
+
details: dict[str, Any] | None = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
super().__init__(message, code, status_code, details)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class UpdateError(DatasetException):
|
|
107
|
+
"""Raised when a update operation fails."""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
message: str = "Update operation failed",
|
|
112
|
+
code: str = "DS_DATASET_UPDATE_ERROR",
|
|
113
|
+
status_code: int = 500,
|
|
114
|
+
details: dict[str, Any] | None = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
super().__init__(message, code, status_code, details)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class DeleteError(DatasetException):
|
|
120
|
+
"""Raised when a delete operation fails."""
|
|
121
|
+
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
message: str = "Delete operation failed",
|
|
125
|
+
code: str = "DS_DATASET_DELETE_ERROR",
|
|
126
|
+
status_code: int = 500,
|
|
127
|
+
details: dict[str, Any] | None = None,
|
|
128
|
+
) -> None:
|
|
129
|
+
super().__init__(message, code, status_code, details)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class RenameError(DatasetException):
|
|
133
|
+
"""Raised when a rename operation fails."""
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
message: str = "Rename operation failed",
|
|
138
|
+
code: str = "DS_DATASET_RENAME_ERROR",
|
|
139
|
+
status_code: int = 500,
|
|
140
|
+
details: dict[str, Any] | None = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
super().__init__(message, code, status_code, details)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class UpsertError(DatasetException):
|
|
146
|
+
"""Raised when an upsert operation fails."""
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
message: str = "Upsert operation failed",
|
|
151
|
+
code: str = "DS_DATASET_UPSERT_ERROR",
|
|
152
|
+
status_code: int = 500,
|
|
153
|
+
details: dict[str, Any] | None = None,
|
|
154
|
+
) -> None:
|
|
155
|
+
super().__init__(message, code, status_code, details)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class PurgeError(DatasetException):
|
|
159
|
+
"""Raised when a purge operation fails."""
|
|
160
|
+
|
|
161
|
+
def __init__(
|
|
162
|
+
self,
|
|
163
|
+
message: str = "Purge operation failed",
|
|
164
|
+
code: str = "DS_DATASET_PURGE_ERROR",
|
|
165
|
+
status_code: int = 500,
|
|
166
|
+
details: dict[str, Any] | None = None,
|
|
167
|
+
) -> None:
|
|
168
|
+
super().__init__(message, code, status_code, details)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class ListError(DatasetException):
|
|
172
|
+
"""Raised when a list operation fails."""
|
|
173
|
+
|
|
174
|
+
def __init__(
|
|
175
|
+
self,
|
|
176
|
+
message: str = "List operation failed",
|
|
177
|
+
code: str = "DS_DATASET_LIST_ERROR",
|
|
178
|
+
status_code: int = 500,
|
|
179
|
+
details: dict[str, Any] | None = None,
|
|
180
|
+
) -> None:
|
|
181
|
+
super().__init__(message, code, status_code, details)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
**File:** ``result.py``
|
|
3
|
+
**Region:** ``ds_resource_plugin_py_lib/common/resource/dataset``
|
|
4
|
+
|
|
5
|
+
Description
|
|
6
|
+
-----------
|
|
7
|
+
Dataclass capturing operation details for every dataset method call.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ds_common_serde_py_lib import Serializable
|
|
15
|
+
|
|
16
|
+
from .enums import DatasetMethod
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(kw_only=True)
|
|
20
|
+
class OperationError(Serializable):
|
|
21
|
+
"""Structured error captured from a ``ResourceException``."""
|
|
22
|
+
|
|
23
|
+
message: str
|
|
24
|
+
"""The error message."""
|
|
25
|
+
code: str
|
|
26
|
+
"""The error code."""
|
|
27
|
+
status_code: int
|
|
28
|
+
"""The HTTP status code."""
|
|
29
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
30
|
+
"""The error details."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(kw_only=True)
|
|
34
|
+
class OperationInfo(Serializable):
|
|
35
|
+
"""
|
|
36
|
+
Report produced by every dataset operation.
|
|
37
|
+
|
|
38
|
+
Timing fields (``started_at``, ``ended_at``, ``duration_ms``) are
|
|
39
|
+
populated automatically by the ``track_result`` decorator. Providers
|
|
40
|
+
may set ``row_count``, ``schema``, or ``metadata`` inside their
|
|
41
|
+
method; any value left at its default will be auto-derived from
|
|
42
|
+
``self.output`` after the method returns.
|
|
43
|
+
|
|
44
|
+
Accessible on the dataset instance as ``self.operation``.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
method: DatasetMethod | None = None
|
|
48
|
+
"""The method that was called."""
|
|
49
|
+
success: bool = False
|
|
50
|
+
"""Whether the method call was successful."""
|
|
51
|
+
error: OperationError | None = None
|
|
52
|
+
"""The error captured from a ``ResourceException``."""
|
|
53
|
+
row_count: int = 0
|
|
54
|
+
"""The number of rows read, written, or discovered."""
|
|
55
|
+
started_at: datetime | None = None
|
|
56
|
+
"""The timestamp when the method started."""
|
|
57
|
+
ended_at: datetime | None = None
|
|
58
|
+
"""The timestamp when the method ended."""
|
|
59
|
+
duration_ms: float = 0.0
|
|
60
|
+
"""The duration of the method in milliseconds."""
|
|
61
|
+
schema: dict[str, Any] | None = None
|
|
62
|
+
"""The schema of the data."""
|
|
63
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
"""The metadata of the data."""
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
**File:** ``storage_format.py``
|
|
3
|
+
**Region:** ``ds_resource_plugin_py_lib/common/resource/dataset``
|
|
4
|
+
|
|
5
|
+
Description
|
|
6
|
+
-----------
|
|
7
|
+
Storage format models for datasets.
|
|
8
|
+
|
|
9
|
+
Example
|
|
10
|
+
-------
|
|
11
|
+
.. code-block:: python
|
|
12
|
+
|
|
13
|
+
from ds_resource_plugin_py_lib.common.resource.dataset.storage_format import (
|
|
14
|
+
CsvFormat,
|
|
15
|
+
DatasetStorageFormatType,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
fmt = CsvFormat(args={"delimiter": ";"})
|
|
19
|
+
assert fmt.type == DatasetStorageFormatType.CSV
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from enum import StrEnum
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from ds_common_serde_py_lib import Serializable
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DatasetStorageFormatType(StrEnum):
|
|
30
|
+
"""
|
|
31
|
+
Enum to define the storage format types.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
PARQUET = "parquet"
|
|
35
|
+
CSV = "csv"
|
|
36
|
+
JSON = "json"
|
|
37
|
+
EXCEL = "excel"
|
|
38
|
+
SEMI_STRUCTURED_JSON = "semi-structured-json"
|
|
39
|
+
XML = "xml"
|
|
40
|
+
BINARY = "binary"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(kw_only=True)
|
|
44
|
+
class DatasetStorageFormat(Serializable):
|
|
45
|
+
"""
|
|
46
|
+
The object containing the storage format of the dataset.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
type: DatasetStorageFormatType
|
|
50
|
+
args: dict[str, Any] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(kw_only=True)
|
|
54
|
+
class ParquetFormat(DatasetStorageFormat):
|
|
55
|
+
type: DatasetStorageFormatType = DatasetStorageFormatType.PARQUET
|
|
56
|
+
args: dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(kw_only=True)
|
|
60
|
+
class CsvFormat(DatasetStorageFormat):
|
|
61
|
+
type: DatasetStorageFormatType = DatasetStorageFormatType.CSV
|
|
62
|
+
args: dict[str, Any] = field(default_factory=lambda: {"delimiter": ","})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(kw_only=True)
|
|
66
|
+
class SemiStructuredJsonFormat(DatasetStorageFormat):
|
|
67
|
+
type: DatasetStorageFormatType = DatasetStorageFormatType.SEMI_STRUCTURED_JSON
|
|
68
|
+
args: dict[str, Any] = field(default_factory=lambda: {"record_path": None})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(kw_only=True)
|
|
72
|
+
class XMLFormat(DatasetStorageFormat):
|
|
73
|
+
type: DatasetStorageFormatType = DatasetStorageFormatType.XML
|
|
74
|
+
args: dict[str, Any] = field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(kw_only=True)
|
|
78
|
+
class BinaryFormat(DatasetStorageFormat):
|
|
79
|
+
type: DatasetStorageFormatType = DatasetStorageFormatType.BINARY
|
|
80
|
+
args: dict[str, Any] = field(default_factory=lambda: {"column": "binary", "row": 0})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
**File:** ``errors.py``
|
|
3
|
+
**Region:** ``ds_resource_plugin_py_lib/common/resource``
|
|
4
|
+
|
|
5
|
+
Description
|
|
6
|
+
-----------
|
|
7
|
+
Exceptions for resources.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ResourceException(Exception):
|
|
14
|
+
"""Base exception for all resource-related errors."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
message: str = "Resource operation failed",
|
|
19
|
+
code: str = "DS_RESOURCE_ERROR",
|
|
20
|
+
status_code: int = 500,
|
|
21
|
+
details: dict[str, Any] | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.code = code
|
|
24
|
+
self.status_code = status_code
|
|
25
|
+
self.message = message
|
|
26
|
+
self.details = details or {}
|
|
27
|
+
super().__init__(self.message)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NotSupportedError(ResourceException):
|
|
31
|
+
"""Raised when a provider does not support an optional method."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
message: str = "Operation not supported",
|
|
36
|
+
code: str = "DS_RESOURCE_NOT_SUPPORTED_ERROR",
|
|
37
|
+
status_code: int = 501,
|
|
38
|
+
details: dict[str, Any] | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
super().__init__(message, code, status_code, details)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ValidationError(ResourceException):
|
|
44
|
+
"""Raised when input fails validation before reaching the backend."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
message: str = "Validation failed",
|
|
49
|
+
code: str = "DS_RESOURCE_VALIDATION_ERROR",
|
|
50
|
+
status_code: int = 400,
|
|
51
|
+
details: dict[str, Any] | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
super().__init__(message, code, status_code, details)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
**File:** ``__init__.py``
|
|
3
|
+
**Region:** ``ds_resource_plugin_py_lib/common/resource/linked_service``
|
|
4
|
+
|
|
5
|
+
Description
|
|
6
|
+
-----------
|
|
7
|
+
Linked service models and typed properties.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .base import LinkedService, LinkedServiceInfo, LinkedServiceSettings
|
|
11
|
+
from .enums import LinkedServiceMethod
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"LinkedService",
|
|
15
|
+
"LinkedServiceInfo",
|
|
16
|
+
"LinkedServiceMethod",
|
|
17
|
+
"LinkedServiceSettings",
|
|
18
|
+
]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
**File:** ``base.py``
|
|
3
|
+
**Region:** ``ds_resource_plugin_py_lib/common/resource/linked_service``
|
|
4
|
+
|
|
5
|
+
Description
|
|
6
|
+
-----------
|
|
7
|
+
Base models for linked services.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import uuid
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
from types import TracebackType
|
|
15
|
+
from typing import Any, Generic, NamedTuple, Self, TypeVar
|
|
16
|
+
|
|
17
|
+
from ds_common_serde_py_lib import Serializable
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LinkedServiceInfo(NamedTuple):
|
|
21
|
+
"""
|
|
22
|
+
NamedTuple that represents the linked service information.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
type: str
|
|
26
|
+
name: str
|
|
27
|
+
class_name: str
|
|
28
|
+
version: str
|
|
29
|
+
description: str | None = None
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
"""
|
|
33
|
+
Return a string representation of the linked service info.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A string representation of the linked service info.
|
|
37
|
+
"""
|
|
38
|
+
return f"{self.type}:v{self.version}"
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def key(self) -> tuple[str, str]:
|
|
42
|
+
"""
|
|
43
|
+
Return the composite key (type, version) for dictionary lookups.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
A tuple containing the type and version.
|
|
47
|
+
"""
|
|
48
|
+
return (self.type, self.version)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(kw_only=True)
|
|
52
|
+
class LinkedServiceSettings(Serializable):
|
|
53
|
+
"""
|
|
54
|
+
The object containing the settings of the linked service.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
LinkedServiceSettingsType = TypeVar("LinkedServiceSettingsType", bound=LinkedServiceSettings)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(kw_only=True)
|
|
64
|
+
class LinkedService(
|
|
65
|
+
ABC,
|
|
66
|
+
Serializable,
|
|
67
|
+
Generic[LinkedServiceSettingsType],
|
|
68
|
+
):
|
|
69
|
+
"""
|
|
70
|
+
The object containing the connection information to connect with related data store.
|
|
71
|
+
|
|
72
|
+
You probably want to use the subclasses and not this class directly.
|
|
73
|
+
Known subclasses are:
|
|
74
|
+
- SftpLinkedService
|
|
75
|
+
- S3LinkedService
|
|
76
|
+
- GraphQlLinkedService
|
|
77
|
+
|
|
78
|
+
All required parameters must be populated in the constructor in order to send to ds-workflow-service.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
id: uuid.UUID
|
|
82
|
+
name: str
|
|
83
|
+
description: str | None = None
|
|
84
|
+
version: str
|
|
85
|
+
|
|
86
|
+
settings: LinkedServiceSettingsType
|
|
87
|
+
|
|
88
|
+
def __enter__(self) -> Self:
|
|
89
|
+
"""
|
|
90
|
+
Context manager enter.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The linked service.
|
|
94
|
+
"""
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def __exit__(
|
|
98
|
+
self,
|
|
99
|
+
exc_type: type[BaseException] | None,
|
|
100
|
+
exc_value: BaseException | None,
|
|
101
|
+
traceback: TracebackType | None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Context manager exit.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
exc_type: The type of the exception.
|
|
108
|
+
exc_value: The value of the exception.
|
|
109
|
+
traceback: The traceback of the exception.
|
|
110
|
+
"""
|
|
111
|
+
self.close()
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
@abstractmethod
|
|
115
|
+
def type(self) -> StrEnum:
|
|
116
|
+
"""
|
|
117
|
+
Get the type of the linked service.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The type of the linked service.
|
|
121
|
+
"""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def connection(self) -> Any:
|
|
127
|
+
"""
|
|
128
|
+
Return the backend client or connection object. Must raise
|
|
129
|
+
``ConnectionError`` if ``connect()`` has not been called.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The backend client, session, or connection object.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ConnectionError: If the connection has not been established.
|
|
136
|
+
|
|
137
|
+
See Also:
|
|
138
|
+
Full contract: ``docs/LINKED_SERVICE_CONTRACT.md`` -- ``connection``
|
|
139
|
+
"""
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
@abstractmethod
|
|
143
|
+
def connect(self) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Establish a connection to the backend data store. The result is
|
|
146
|
+
stored internally and accessible via the ``connection`` property.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ConnectionError: If the connection cannot be established.
|
|
150
|
+
AuthenticationError: If credentials are invalid.
|
|
151
|
+
NotSupportedError: If the provider does not support connect.
|
|
152
|
+
|
|
153
|
+
See Also:
|
|
154
|
+
Full contract: ``docs/LINKED_SERVICE_CONTRACT.md`` -- ``connect()``
|
|
155
|
+
"""
|
|
156
|
+
...
|
|
157
|
+
|
|
158
|
+
@abstractmethod
|
|
159
|
+
def test_connection(self) -> tuple[bool, str]:
|
|
160
|
+
"""
|
|
161
|
+
Verify that the connection to the backend is healthy. Does not
|
|
162
|
+
raise on connection failure -- returns ``(False, "reason")``
|
|
163
|
+
instead.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
A tuple ``(success, message)``. On success: ``(True, "")``.
|
|
167
|
+
On failure: ``(False, "error description")``.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
NotSupportedError: If the provider does not support test_connection.
|
|
171
|
+
|
|
172
|
+
See Also:
|
|
173
|
+
Full contract: ``docs/LINKED_SERVICE_CONTRACT.md`` -- ``test_connection()``
|
|
174
|
+
"""
|
|
175
|
+
...
|
|
176
|
+
|
|
177
|
+
@abstractmethod
|
|
178
|
+
def close(self) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Release connections, sessions, or handles. Must not raise if
|
|
181
|
+
already closed. Idempotent.
|
|
182
|
+
|
|
183
|
+
See Also:
|
|
184
|
+
Full contract: ``docs/LINKED_SERVICE_CONTRACT.md`` -- ``close()``
|
|
185
|
+
"""
|
|
186
|
+
...
|