datamasque-python 1.0.2__py3-none-any.whl → 1.0.4__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.
- datamasque/client/__init__.py +2 -2
- datamasque/client/base.py +62 -7
- datamasque/client/ifm.py +7 -7
- datamasque/client/models/connection.py +28 -6
- {datamasque_python-1.0.2.dist-info → datamasque_python-1.0.4.dist-info}/METADATA +1 -1
- {datamasque_python-1.0.2.dist-info → datamasque_python-1.0.4.dist-info}/RECORD +8 -8
- {datamasque_python-1.0.2.dist-info → datamasque_python-1.0.4.dist-info}/WHEEL +1 -1
- {datamasque_python-1.0.2.dist-info → datamasque_python-1.0.4.dist-info}/licenses/LICENSE +0 -0
datamasque/client/__init__.py
CHANGED
|
@@ -30,7 +30,7 @@ from datamasque.client.models.connection import (
|
|
|
30
30
|
ConnectionId,
|
|
31
31
|
DatabaseConnectionConfig,
|
|
32
32
|
DatabaseType,
|
|
33
|
-
|
|
33
|
+
DatabricksConnectionConfig,
|
|
34
34
|
DynamoConnectionConfig,
|
|
35
35
|
FileConnectionConfig,
|
|
36
36
|
MongoConnectionConfig,
|
|
@@ -129,7 +129,7 @@ __all__ = [
|
|
|
129
129
|
"DataMasqueUserError",
|
|
130
130
|
"DatabaseConnectionConfig",
|
|
131
131
|
"DatabaseType",
|
|
132
|
-
"
|
|
132
|
+
"DatabricksConnectionConfig",
|
|
133
133
|
"DiscoveryMatch",
|
|
134
134
|
"DynamoConnectionConfig",
|
|
135
135
|
"FailedToStartError",
|
datamasque/client/base.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import platform
|
|
3
|
+
import sys
|
|
2
4
|
import warnings
|
|
3
5
|
from contextlib import contextmanager
|
|
4
6
|
from dataclasses import dataclass
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
8
|
from io import BufferedIOBase, BytesIO, TextIOBase
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
from typing import Any, Callable, Iterator, Optional, Type, TypeVar, Union
|
|
@@ -24,6 +27,44 @@ logger = logging.getLogger(__name__)
|
|
|
24
27
|
FileOrContent = Union[str, bytes, TextIOBase, BufferedIOBase, Path]
|
|
25
28
|
_T = TypeVar("_T", bound=BaseModel)
|
|
26
29
|
|
|
30
|
+
|
|
31
|
+
def _build_user_agent() -> str:
|
|
32
|
+
"""
|
|
33
|
+
Identify ourselves to the DataMasque server in access logs and audit trails.
|
|
34
|
+
|
|
35
|
+
Default `python-requests/x.y.z` is anonymous; this surfaces the SDK name +
|
|
36
|
+
version, Python interpreter, and OS so operators can correlate API traffic
|
|
37
|
+
with a specific SDK release (e.g. when triaging a bug report).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
sdk_version = version("datamasque-python")
|
|
42
|
+
except PackageNotFoundError:
|
|
43
|
+
# Source checkouts without installed metadata.
|
|
44
|
+
sdk_version = "dev"
|
|
45
|
+
py = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
46
|
+
return f"datamasque-python/{sdk_version} (Python/{py}; {platform.system()}/{platform.release()})"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
USER_AGENT = _build_user_agent()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _build_session(verify_ssl: bool) -> requests.Session:
|
|
53
|
+
"""
|
|
54
|
+
Build a configured `requests.Session` for one client's lifetime.
|
|
55
|
+
|
|
56
|
+
Centralises the `User-Agent` and `verify` defaults so every call site
|
|
57
|
+
inherits them automatically — keeping the per-call code free of
|
|
58
|
+
boilerplate and removing the risk of forgetting either flag on a new
|
|
59
|
+
endpoint.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
session = requests.Session()
|
|
63
|
+
session.headers["User-Agent"] = USER_AGENT
|
|
64
|
+
session.verify = verify_ssl
|
|
65
|
+
return session
|
|
66
|
+
|
|
67
|
+
|
|
27
68
|
# Substrings (case-insensitive) that mark a key whose value should be redacted
|
|
28
69
|
# before logging on an error path, so that passwords, API tokens, and similar secrets don't
|
|
29
70
|
# end up in user-visible logs when a request fails.
|
|
@@ -71,6 +112,15 @@ class BaseClient:
|
|
|
71
112
|
|
|
72
113
|
Holds the connection config, cached auth token, and the core `make_request` dispatcher
|
|
73
114
|
used by all per-feature mixins that compose `DataMasqueClient`.
|
|
115
|
+
|
|
116
|
+
Uses a single `requests.Session` for the lifetime of the client so that
|
|
117
|
+
per-host TCP / TLS connections are pooled across calls (paginated list
|
|
118
|
+
endpoints and tight polling loops benefit most). Session-wide defaults
|
|
119
|
+
(`User-Agent`, `verify`) are set once on construction; per-call headers
|
|
120
|
+
like `Authorization` are merged at request time.
|
|
121
|
+
|
|
122
|
+
`requests.Session` is not thread-safe; do not share a client between
|
|
123
|
+
threads. Construct one per worker.
|
|
74
124
|
"""
|
|
75
125
|
|
|
76
126
|
token: str = ""
|
|
@@ -86,6 +136,7 @@ class BaseClient:
|
|
|
86
136
|
self.password = connection_config.password
|
|
87
137
|
self.verify_ssl = connection_config.verify_ssl
|
|
88
138
|
self.token_source = connection_config.token_source
|
|
139
|
+
self._session = _build_session(self.verify_ssl)
|
|
89
140
|
|
|
90
141
|
@contextmanager
|
|
91
142
|
def _maybe_suppress_insecure_warning(self) -> Iterator[None]:
|
|
@@ -186,28 +237,32 @@ class BaseClient:
|
|
|
186
237
|
url = urljoin(self.base_url, path)
|
|
187
238
|
|
|
188
239
|
def send() -> Response:
|
|
189
|
-
headers
|
|
240
|
+
headers = {"Authorization": self.token} if requires_authorization else None
|
|
190
241
|
try:
|
|
191
242
|
with self._maybe_suppress_insecure_warning():
|
|
192
243
|
if files:
|
|
193
244
|
files_payload = {f.field_name: (f.filename, f.content, f.content_type or "") for f in files}
|
|
194
|
-
return
|
|
245
|
+
return self._session.request(
|
|
195
246
|
method,
|
|
196
247
|
url,
|
|
197
248
|
data=data,
|
|
198
249
|
params=params,
|
|
199
250
|
headers=headers,
|
|
200
251
|
files=files_payload,
|
|
201
|
-
verify=self.verify_ssl,
|
|
202
252
|
)
|
|
203
|
-
return
|
|
204
|
-
method, url, json=data, params=params, headers=headers, verify=self.verify_ssl
|
|
205
|
-
)
|
|
253
|
+
return self._session.request(method, url, json=data, params=params, headers=headers)
|
|
206
254
|
except requests.RequestException as e:
|
|
207
255
|
raise DataMasqueTransportError(f"Failed to reach DataMasque server at {url}: {e}") from e
|
|
208
256
|
|
|
209
257
|
response = send()
|
|
210
|
-
if response.status_code == 401:
|
|
258
|
+
if response.status_code == 401 and requires_authorization:
|
|
259
|
+
# Token-expiry recovery: re-auth and replay. Only meaningful when the
|
|
260
|
+
# caller actually sent a token; on `requires_authorization=False`
|
|
261
|
+
# calls a 401 means the server itself is rejecting anonymous access
|
|
262
|
+
# (e.g. admin-install on an already-configured instance), and
|
|
263
|
+
# re-authing with whatever creds the client happens to hold would
|
|
264
|
+
# both misdiagnose the failure and emit a misleading
|
|
265
|
+
# "credentials are incorrect" error to the user.
|
|
211
266
|
logger.debug("Re-authenticating")
|
|
212
267
|
self.authenticate()
|
|
213
268
|
# Reset file pointers so the retry doesn't send empty files
|
datamasque/client/ifm.py
CHANGED
|
@@ -17,7 +17,7 @@ import requests
|
|
|
17
17
|
from pydantic import BaseModel
|
|
18
18
|
from requests import Response
|
|
19
19
|
|
|
20
|
-
from datamasque.client.base import suppress_insecure_warning_if_needed
|
|
20
|
+
from datamasque.client.base import _build_session, suppress_insecure_warning_if_needed
|
|
21
21
|
from datamasque.client.exceptions import (
|
|
22
22
|
DataMasqueApiError,
|
|
23
23
|
DataMasqueNotReadyError,
|
|
@@ -82,6 +82,9 @@ class DataMasqueIfmClient:
|
|
|
82
82
|
self.password = connection_config.password
|
|
83
83
|
self.verify_ssl = connection_config.verify_ssl
|
|
84
84
|
self.token_source = connection_config.token_source
|
|
85
|
+
# One session for both admin-server (JWT login/refresh) and IFM (data plane)
|
|
86
|
+
# traffic -- different hosts, but a single session handles per-host pooling.
|
|
87
|
+
self._session = _build_session(self.verify_ssl)
|
|
85
88
|
|
|
86
89
|
def authenticate(self) -> None:
|
|
87
90
|
"""Obtain an access (and refresh) token from the admin server, or via `token_source`."""
|
|
@@ -95,10 +98,9 @@ class DataMasqueIfmClient:
|
|
|
95
98
|
login_url = urljoin(self.admin_server_base_url, "/api/auth/jwt/login/")
|
|
96
99
|
try:
|
|
97
100
|
with self._maybe_suppress_insecure_warning():
|
|
98
|
-
response =
|
|
101
|
+
response = self._session.post(
|
|
99
102
|
login_url,
|
|
100
103
|
json={"username": self.username, "password": self.password},
|
|
101
|
-
verify=self.verify_ssl,
|
|
102
104
|
)
|
|
103
105
|
except requests.RequestException as e:
|
|
104
106
|
raise DataMasqueTransportError(f"Failed to reach admin server at {login_url}: {e}") from e
|
|
@@ -122,10 +124,9 @@ class DataMasqueIfmClient:
|
|
|
122
124
|
refresh_url = urljoin(self.admin_server_base_url, "/api/auth/jwt/refresh/")
|
|
123
125
|
try:
|
|
124
126
|
with self._maybe_suppress_insecure_warning():
|
|
125
|
-
response =
|
|
127
|
+
response = self._session.post(
|
|
126
128
|
refresh_url,
|
|
127
129
|
json={"refresh": self.refresh_token},
|
|
128
|
-
verify=self.verify_ssl,
|
|
129
130
|
)
|
|
130
131
|
except requests.RequestException as e:
|
|
131
132
|
raise DataMasqueTransportError(f"Failed to reach admin server at {refresh_url}: {e}") from e
|
|
@@ -187,13 +188,12 @@ class DataMasqueIfmClient:
|
|
|
187
188
|
def send() -> Response:
|
|
188
189
|
try:
|
|
189
190
|
with self._maybe_suppress_insecure_warning():
|
|
190
|
-
return
|
|
191
|
+
return self._session.request(
|
|
191
192
|
method,
|
|
192
193
|
url,
|
|
193
194
|
json=json_body,
|
|
194
195
|
params=params,
|
|
195
196
|
headers={"Authorization": f"Bearer {self.access_token}"},
|
|
196
|
-
verify=self.verify_ssl,
|
|
197
197
|
)
|
|
198
198
|
except requests.RequestException as e:
|
|
199
199
|
raise DataMasqueTransportError(f"Failed to reach IFM server at {url}: {e}") from e
|
|
@@ -45,6 +45,8 @@ class DatabaseType(Enum):
|
|
|
45
45
|
snowflake = "snowflake"
|
|
46
46
|
mongodb = "mongodb"
|
|
47
47
|
databricks_lakebase = "databricks_lakebase"
|
|
48
|
+
databricks = "databricks"
|
|
49
|
+
informix = "informix"
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
class SnowflakeStageLocation(str, Enum):
|
|
@@ -280,6 +282,8 @@ class DatabaseConnectionConfig(ConnectionConfig):
|
|
|
280
282
|
raise ValueError("For Snowflake, use the SnowflakeConnectionConfig class instead")
|
|
281
283
|
if self.database_type is DatabaseType.mongodb:
|
|
282
284
|
raise ValueError("For MongoDB, use the MongoConnectionConfig class instead")
|
|
285
|
+
if self.database_type is DatabaseType.databricks:
|
|
286
|
+
raise ValueError("For Databricks SQL Warehouse, use the DatabricksConnectionConfig class instead")
|
|
283
287
|
return self
|
|
284
288
|
|
|
285
289
|
mask_type: Literal["database"] = "database"
|
|
@@ -391,19 +395,36 @@ class MountedShareConnectionConfig(FileConnectionConfig):
|
|
|
391
395
|
type: Literal["mounted_share_connection"] = "mounted_share_connection"
|
|
392
396
|
|
|
393
397
|
|
|
394
|
-
class
|
|
395
|
-
"""Connection configuration for Databricks
|
|
398
|
+
class DatabricksConnectionConfig(ConnectionConfig):
|
|
399
|
+
"""Connection configuration for a Databricks SQL Warehouse."""
|
|
396
400
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
401
|
+
server_hostname: str
|
|
402
|
+
http_path: str
|
|
403
|
+
access_token: Optional[str] = None
|
|
404
|
+
catalog: str
|
|
405
|
+
db_schema: Optional[str] = Field(default=None, alias="schema")
|
|
406
|
+
is_read_only: bool = False
|
|
407
|
+
version: str = "1.0"
|
|
408
|
+
|
|
409
|
+
mask_type: Literal["database"] = "database"
|
|
410
|
+
db_type: Literal["databricks"] = "databricks"
|
|
411
|
+
|
|
412
|
+
@property
|
|
413
|
+
def database_type(self) -> DatabaseType:
|
|
414
|
+
return DatabaseType.databricks
|
|
415
|
+
|
|
416
|
+
@model_validator(mode="before")
|
|
417
|
+
@classmethod
|
|
418
|
+
def _strip_encrypted_token(cls, data: dict) -> dict:
|
|
419
|
+
if isinstance(data, dict):
|
|
420
|
+
data.pop("access_token_encrypted", None)
|
|
421
|
+
return data
|
|
400
422
|
|
|
401
423
|
|
|
402
424
|
FILE_TYPE_MAP: dict[str, type[FileConnectionConfig]] = {
|
|
403
425
|
"s3_connection": S3ConnectionConfig,
|
|
404
426
|
"azure_blob_connection": AzureConnectionConfig,
|
|
405
427
|
"mounted_share_connection": MountedShareConnectionConfig,
|
|
406
|
-
"databricks_delta_s3_connection": DatabricksDeltaS3ConnectionConfig,
|
|
407
428
|
}
|
|
408
429
|
|
|
409
430
|
DB_TYPE_MAP: dict[str, type[ConnectionConfig]] = {
|
|
@@ -411,6 +432,7 @@ DB_TYPE_MAP: dict[str, type[ConnectionConfig]] = {
|
|
|
411
432
|
DatabaseType.mongodb.value: MongoConnectionConfig,
|
|
412
433
|
DatabaseType.snowflake.value: SnowflakeConnectionConfig,
|
|
413
434
|
DatabaseType.mssql_linked.value: MssqlLinkedServerConnectionConfig,
|
|
435
|
+
DatabaseType.databricks.value: DatabricksConnectionConfig,
|
|
414
436
|
# others use the default `DatabaseConnectionConfig`
|
|
415
437
|
}
|
|
416
438
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datamasque-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Official Python client for the DataMasque data-masking API.
|
|
5
5
|
Project-URL: Homepage, https://datamasque.com/
|
|
6
6
|
Project-URL: Documentation, https://datamasque-python.readthedocs.io/
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
datamasque/client/__init__.py,sha256=
|
|
2
|
-
datamasque/client/base.py,sha256=
|
|
1
|
+
datamasque/client/__init__.py,sha256=rH7hQyA_nFVkv8eV_C1Ds_40v9xzCK__uXIJtWMLfuQ,5472
|
|
2
|
+
datamasque/client/base.py,sha256=vP1LuYbqq6U8NAEJWnwNrdt_Fa2wjQTJXHf0gSFO79c,14115
|
|
3
3
|
datamasque/client/connections.py,sha256=EFinx8fJRme0mTxuWY3d29UnmUFbsQhMaUQT0Ma2PK4,2885
|
|
4
4
|
datamasque/client/discovery.py,sha256=uA8h6vRqsxSAzSAV9bebJwU47XINlaVy7V1nTYDiaCM,12634
|
|
5
5
|
datamasque/client/dmclient.py,sha256=OPYMzc57gUHPs6iL_J2DYp06MfOaabpTlISmnNCpqS4,1553
|
|
6
6
|
datamasque/client/exceptions.py,sha256=F9FYCxP-ERXkVD1L3yh_rWqcW3IsPL-c-Ic4qMYoGnw,2542
|
|
7
7
|
datamasque/client/files.py,sha256=5Gzel4aLby8T7ncOIV6wtgVbFEk0eaEy7wxohh9u0zE,3468
|
|
8
|
-
datamasque/client/ifm.py,sha256=
|
|
8
|
+
datamasque/client/ifm.py,sha256=uIMxpLIPvDiDO1m4bxezixNIUFIFY0MXWRMQNvTbpTA,11858
|
|
9
9
|
datamasque/client/license.py,sha256=pluYaSU168OC6_laB9bM9H3Vuuxs8wa9gujCJvtoJCk,1392
|
|
10
10
|
datamasque/client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
datamasque/client/ruleset_libraries.py,sha256=tyN--cndzG0gwFnHY9fDtObTLzGsK0wrV5lxxjRP72g,6868
|
|
@@ -14,7 +14,7 @@ datamasque/client/runs.py,sha256=ZPSkkuyqMiwy7dLbWZ1PAEKaesKLeNRX7xEJGfzieVg,742
|
|
|
14
14
|
datamasque/client/settings.py,sha256=Ui8AyR2XdoW8MZ9FIrGn2jm8DLzUGWIL8i2DOTvu7hc,2898
|
|
15
15
|
datamasque/client/users.py,sha256=VCUo2CJyOw4-aO_3mp_w_0BcSoa6-h1HcReMcAMyumw,3701
|
|
16
16
|
datamasque/client/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
datamasque/client/models/connection.py,sha256=
|
|
17
|
+
datamasque/client/models/connection.py,sha256=DQkn1kVTu3ODNNBjd-nR1JZhck5kalWhHJBuDWGNzU0,15778
|
|
18
18
|
datamasque/client/models/data_selection.py,sha256=406yyUZ5NmLBSql2lYM1gsTWY5GWmnutmF-DjznAoLc,1904
|
|
19
19
|
datamasque/client/models/discovery.py,sha256=BawKusPuhyt0gRRnWKYf-ZKF7V54GxkhgYIrEQ6XkOs,7283
|
|
20
20
|
datamasque/client/models/dm_instance.py,sha256=yjjpHZJTFhJp3lAinTEffgKrxnadrJys1EuhO77wQcA,1561
|
|
@@ -27,7 +27,7 @@ datamasque/client/models/ruleset_library.py,sha256=gIqb6yn4-f9i2OydOLk1cd0zId9nG
|
|
|
27
27
|
datamasque/client/models/runs.py,sha256=oVYo9jp9s5LjWts0LKsYpX3HUBmrVwuuzDqFtQmTjvo,5673
|
|
28
28
|
datamasque/client/models/status.py,sha256=rjH6YSwAHoOUaUCADwvMhuKd0ygT0c2w2Ek5SV8PWD8,1984
|
|
29
29
|
datamasque/client/models/user.py,sha256=UGAUzgJkf78m24_zFXXoA99zdut48BXkX_ivV8yq1Vc,2043
|
|
30
|
-
datamasque_python-1.0.
|
|
31
|
-
datamasque_python-1.0.
|
|
32
|
-
datamasque_python-1.0.
|
|
33
|
-
datamasque_python-1.0.
|
|
30
|
+
datamasque_python-1.0.4.dist-info/METADATA,sha256=eIH7wSpr6f9J6twjwzCbrKHZ7Sn3jFs-DLXo_F5Lzus,4187
|
|
31
|
+
datamasque_python-1.0.4.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
32
|
+
datamasque_python-1.0.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
33
|
+
datamasque_python-1.0.4.dist-info/RECORD,,
|
|
File without changes
|