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.
Files changed (37) hide show
  1. ds_resource_plugin_py_lib/__init__.py +28 -0
  2. ds_resource_plugin_py_lib/common/__init__.py +15 -0
  3. ds_resource_plugin_py_lib/common/resource/__init__.py +17 -0
  4. ds_resource_plugin_py_lib/common/resource/client.py +265 -0
  5. ds_resource_plugin_py_lib/common/resource/dataset/__init__.py +25 -0
  6. ds_resource_plugin_py_lib/common/resource/dataset/base.py +322 -0
  7. ds_resource_plugin_py_lib/common/resource/dataset/decorators.py +73 -0
  8. ds_resource_plugin_py_lib/common/resource/dataset/enums.py +36 -0
  9. ds_resource_plugin_py_lib/common/resource/dataset/errors.py +181 -0
  10. ds_resource_plugin_py_lib/common/resource/dataset/result.py +64 -0
  11. ds_resource_plugin_py_lib/common/resource/dataset/storage_format.py +80 -0
  12. ds_resource_plugin_py_lib/common/resource/errors.py +53 -0
  13. ds_resource_plugin_py_lib/common/resource/linked_service/__init__.py +18 -0
  14. ds_resource_plugin_py_lib/common/resource/linked_service/base.py +186 -0
  15. ds_resource_plugin_py_lib/common/resource/linked_service/enums.py +19 -0
  16. ds_resource_plugin_py_lib/common/resource/linked_service/errors.py +103 -0
  17. ds_resource_plugin_py_lib/common/serde/__init__.py +15 -0
  18. ds_resource_plugin_py_lib/common/serde/binary.py +129 -0
  19. ds_resource_plugin_py_lib/common/serde/deserialize/__init__.py +18 -0
  20. ds_resource_plugin_py_lib/common/serde/deserialize/awswrangler.py +142 -0
  21. ds_resource_plugin_py_lib/common/serde/deserialize/base.py +31 -0
  22. ds_resource_plugin_py_lib/common/serde/deserialize/pandas.py +107 -0
  23. ds_resource_plugin_py_lib/common/serde/serialize/__init__.py +18 -0
  24. ds_resource_plugin_py_lib/common/serde/serialize/awswrangler.py +115 -0
  25. ds_resource_plugin_py_lib/common/serde/serialize/base.py +27 -0
  26. ds_resource_plugin_py_lib/common/serde/serialize/pandas.py +102 -0
  27. ds_resource_plugin_py_lib/libs/__init__.py +14 -0
  28. ds_resource_plugin_py_lib/libs/utils/__init__.py +14 -0
  29. ds_resource_plugin_py_lib/libs/utils/import_string.py +50 -0
  30. ds_resource_plugin_py_lib/libs/utils/json_default.py +66 -0
  31. ds_resource_plugin_py_lib/libs/utils/sanitize.py +35 -0
  32. ds_resource_plugin_py_lib/py.typed +0 -0
  33. ds_resource_plugin_py_lib-0.1.0.dist-info/METADATA +140 -0
  34. ds_resource_plugin_py_lib-0.1.0.dist-info/RECORD +37 -0
  35. ds_resource_plugin_py_lib-0.1.0.dist-info/WHEEL +5 -0
  36. ds_resource_plugin_py_lib-0.1.0.dist-info/licenses/LICENSE-APACHE +201 -0
  37. 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
+ ...