datamasque-python 1.0.3__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/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: Optional[dict] = {"Authorization": self.token} if requires_authorization else None
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 requests.request(
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 requests.request(
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 = requests.post(
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 = requests.post(
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 requests.request(
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
@@ -46,6 +46,7 @@ class DatabaseType(Enum):
46
46
  mongodb = "mongodb"
47
47
  databricks_lakebase = "databricks_lakebase"
48
48
  databricks = "databricks"
49
+ informix = "informix"
49
50
 
50
51
 
51
52
  class SnowflakeStageLocation(str, Enum):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datamasque-python
3
- Version: 1.0.3
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
1
  datamasque/client/__init__.py,sha256=rH7hQyA_nFVkv8eV_C1Ds_40v9xzCK__uXIJtWMLfuQ,5472
2
- datamasque/client/base.py,sha256=he-ObABs_rpXV_KSMmrtJ73D-9lb0giAK029lN5qB3Y,11817
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=K4n9q-8f2wsmxnKlE-jkn9zl40iC9RGAw2tRB0mAtyM,11737
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=NCKlA6w_Lu-G-v3C5dUC8GsOxIck4waW0PtXGoz4FZQ,15752
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.3.dist-info/METADATA,sha256=BLZ0xOeNh8fHFQqIu0CGJi0iFzWgVwq9hVHHMlH8A14,4187
31
- datamasque_python-1.0.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
32
- datamasque_python-1.0.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
33
- datamasque_python-1.0.3.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.30.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any