datamasque-python 1.1.5__tar.gz → 1.1.7__tar.gz

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 (82) hide show
  1. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/HISTORY.rst +12 -0
  2. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/PKG-INFO +1 -1
  3. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/__init__.py +5 -0
  4. datamasque_python-1.1.7/datamasque/client/discovery_config_libraries.py +172 -0
  5. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/dmclient.py +2 -0
  6. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/connection.py +3 -0
  7. datamasque_python-1.1.7/datamasque/client/models/discovery_config_library.py +33 -0
  8. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/ruleset.py +3 -5
  9. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/ruleset_library.py +3 -3
  10. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/status.py +13 -1
  11. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/ruleset_libraries.py +3 -3
  12. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/rulesets.py +2 -3
  13. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/pyproject.toml +1 -1
  14. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/setup.cfg +1 -1
  15. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_connections.py +23 -0
  16. datamasque_python-1.1.7/tests/test_discovery_config_libraries.py +625 -0
  17. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_ruleset_library.py +14 -11
  18. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_rulesets.py +21 -13
  19. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/uv.lock +90 -90
  20. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/.editorconfig +0 -0
  21. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/.github/workflows/ci.yml +0 -0
  22. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/.github/workflows/release-testpypi.yml +0 -0
  23. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/.github/workflows/release.yml +0 -0
  24. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/.gitignore +0 -0
  25. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/.readthedocs.yaml +0 -0
  26. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/CONTRIBUTING.rst +0 -0
  27. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/LICENSE +0 -0
  28. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/MANIFEST.in +0 -0
  29. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/Makefile +0 -0
  30. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/NOTICE +0 -0
  31. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/README.rst +0 -0
  32. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/base.py +0 -0
  33. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/connections.py +0 -0
  34. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/discovery.py +0 -0
  35. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/discovery_configs.py +0 -0
  36. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/exceptions.py +0 -0
  37. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/files.py +0 -0
  38. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/ifm.py +0 -0
  39. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/license.py +0 -0
  40. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/__init__.py +0 -0
  41. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/data_selection.py +0 -0
  42. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/discovery.py +0 -0
  43. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/discovery_config.py +0 -0
  44. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/dm_instance.py +0 -0
  45. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/files.py +0 -0
  46. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/git.py +0 -0
  47. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/ifm.py +0 -0
  48. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/license.py +0 -0
  49. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/pagination.py +0 -0
  50. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/runs.py +0 -0
  51. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/models/user.py +0 -0
  52. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/py.typed +0 -0
  53. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/runs.py +0 -0
  54. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/settings.py +0 -0
  55. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/spcs.py +0 -0
  56. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/datamasque/client/users.py +0 -0
  57. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/docs/Makefile +0 -0
  58. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/docs/client.models.rst +0 -0
  59. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/docs/client.rst +0 -0
  60. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/docs/conf.py +0 -0
  61. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/docs/contributing.rst +0 -0
  62. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/docs/history.rst +0 -0
  63. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/docs/index.rst +0 -0
  64. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/docs/installation.rst +0 -0
  65. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/docs/make.bat +0 -0
  66. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/docs/modules.rst +0 -0
  67. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/docs/readme.rst +0 -0
  68. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/docs/usage.rst +0 -0
  69. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/__init__.py +0 -0
  70. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/conftest.py +0 -0
  71. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/helpers.py +0 -0
  72. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_base.py +0 -0
  73. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_discovery.py +0 -0
  74. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_discovery_configs.py +0 -0
  75. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_files.py +0 -0
  76. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_ifm.py +0 -0
  77. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_license.py +0 -0
  78. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_pagination.py +0 -0
  79. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_runs.py +0 -0
  80. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_settings.py +0 -0
  81. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_spcs.py +0 -0
  82. {datamasque_python-1.1.5 → datamasque_python-1.1.7}/tests/test_users.py +0 -0
@@ -2,6 +2,18 @@
2
2
  History
3
3
  =======
4
4
 
5
+ 1.1.7 (2026-07-14)
6
+ ------------------
7
+
8
+ * Added ``retry_writes`` to ``MongoConnectionConfig`` (default ``True``), serialized only when
9
+ disabled. Set it to ``False`` for AWS DocumentDB, which rejects retryable writes.
10
+ * Updated the validation error API on ``Ruleset`` and ``RulesetLibrary``.
11
+
12
+ 1.1.6 (2026-07-13)
13
+ ------------------
14
+
15
+ * Added discovery-config-library management APIs (``list_discovery_config_libraries``, ``create_discovery_config_library``, and friends).
16
+
5
17
  1.1.5 (2026-06-29)
6
18
  ------------------
7
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datamasque-python
3
- Version: 1.1.5
3
+ Version: 1.1.7
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/
@@ -80,6 +80,7 @@ from datamasque.client.models.discovery import (
80
80
  TableConstraints,
81
81
  )
82
82
  from datamasque.client.models.discovery_config import DiscoveryConfig, DiscoveryConfigId, DiscoveryConfigType
83
+ from datamasque.client.models.discovery_config_library import DiscoveryConfigLibrary, DiscoveryConfigLibraryId
83
84
  from datamasque.client.models.dm_instance import DataMasqueInstanceConfig
84
85
  from datamasque.client.models.files import (
85
86
  DataMasqueFile,
@@ -118,6 +119,7 @@ from datamasque.client.models.runs import (
118
119
  from datamasque.client.models.status import (
119
120
  AsyncRulesetGenerationTaskStatus,
120
121
  MaskingRunStatus,
122
+ ValidationErrorDetails,
121
123
  ValidationErrorType,
122
124
  ValidationStatus,
123
125
  )
@@ -148,6 +150,8 @@ __all__ = [
148
150
  "DatabricksConnectionConfig",
149
151
  "DiscoveryConfig",
150
152
  "DiscoveryConfigId",
153
+ "DiscoveryConfigLibrary",
154
+ "DiscoveryConfigLibraryId",
151
155
  "DiscoveryConfigNotFoundError",
152
156
  "DiscoveryConfigType",
153
157
  "DiscoveryMatch",
@@ -231,6 +235,7 @@ __all__ = [
231
235
  "UserId",
232
236
  "UserRole",
233
237
  "UserSelection",
238
+ "ValidationErrorDetails",
234
239
  "ValidationErrorType",
235
240
  "ValidationStatus",
236
241
  ]
@@ -0,0 +1,172 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from datamasque.client.base import BaseClient
5
+ from datamasque.client.exceptions import DataMasqueApiError
6
+ from datamasque.client.models.discovery_config import DiscoveryConfigType
7
+ from datamasque.client.models.discovery_config_library import DiscoveryConfigLibrary, DiscoveryConfigLibraryId
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class DiscoveryConfigLibraryClient(BaseClient):
13
+ """Discovery config library CRUD API methods. Mixed into `DataMasqueClient`."""
14
+
15
+ def list_discovery_config_libraries(self) -> list[DiscoveryConfigLibrary]:
16
+ """
17
+ Lists all discovery config libraries.
18
+
19
+ Unlike most list endpoints, this one is not paginated;
20
+ the server returns every library in a single response.
21
+ Note: the YAML content is not included in the list response for performance.
22
+ Use `get_discovery_config_library` to retrieve the full library with its YAML body.
23
+ """
24
+
25
+ response = self.make_request("GET", "/api/discovery/config-libraries/")
26
+ return [DiscoveryConfigLibrary.model_validate(item) for item in response.json()]
27
+
28
+ def get_discovery_config_library(self, library_id: DiscoveryConfigLibraryId) -> DiscoveryConfigLibrary:
29
+ """Retrieves a single discovery config library by ID, including its YAML content."""
30
+
31
+ response = self.make_request("GET", f"/api/discovery/config-libraries/{library_id}/")
32
+ return DiscoveryConfigLibrary.model_validate(response.json())
33
+
34
+ def _get_discovery_config_library_id_by_name(
35
+ self, name: str, config_type: DiscoveryConfigType, namespace: str
36
+ ) -> Optional[DiscoveryConfigLibraryId]:
37
+ """
38
+ Returns the ID of the library with the given name, type, and namespace, or `None` if there is none.
39
+
40
+ The listing is filtered by name to keep the response small;
41
+ type and namespace are matched client-side,
42
+ since the API's namespace filter cannot select the default (empty) namespace.
43
+ """
44
+
45
+ response = self.make_request(
46
+ "GET",
47
+ "/api/discovery/config-libraries/",
48
+ params={"name_exact": name},
49
+ )
50
+ entries = [DiscoveryConfigLibrary.model_validate(item) for item in response.json()]
51
+ matches = [
52
+ entry
53
+ for entry in entries
54
+ if entry.name == name and entry.namespace == namespace and entry.config_type is config_type
55
+ ]
56
+ if not matches:
57
+ return None
58
+
59
+ if matches[0].id is None:
60
+ raise DataMasqueApiError(
61
+ "Server returned a discovery config library list entry without an `id`.",
62
+ response=response,
63
+ )
64
+
65
+ return matches[0].id
66
+
67
+ def get_discovery_config_library_by_name(
68
+ self, name: str, config_type: DiscoveryConfigType, namespace: str = ""
69
+ ) -> Optional[DiscoveryConfigLibrary]:
70
+ """
71
+ Looks for a discovery config library matching the given name, type, and namespace (case-sensitive, exact match).
72
+
73
+ Library names are unique per type within a namespace,
74
+ so a type is required to identify a single library.
75
+ Returns it (with full YAML content) if found, otherwise `None`.
76
+ """
77
+
78
+ library_id = self._get_discovery_config_library_id_by_name(name, config_type, namespace)
79
+ if library_id is None:
80
+ return None
81
+
82
+ return self.get_discovery_config_library(library_id)
83
+
84
+ def create_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary:
85
+ """
86
+ Creates a new discovery config library on the server.
87
+
88
+ Sets the library's server-assigned fields
89
+ (`id`, `is_valid`, `validation_error`, `created`, `modified`) and returns the library.
90
+ """
91
+
92
+ data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
93
+ response = self.make_request("POST", "/api/discovery/config-libraries/", data=data)
94
+ created = DiscoveryConfigLibrary.model_validate(response.json())
95
+ library.id = created.id
96
+ library.is_valid = created.is_valid
97
+ library.validation_error = created.validation_error
98
+ library.created = created.created
99
+ library.modified = created.modified
100
+ logger.info('Creation of discovery config library "%s" successful', library.name)
101
+ return library
102
+
103
+ def update_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary:
104
+ """
105
+ Performs a full update of the discovery config library.
106
+
107
+ The library must have its `id` set (i.e., it must have been previously created or retrieved from the server)
108
+ and its `yaml` content present.
109
+ A library's `config_type` is fixed at creation and cannot be changed by an update.
110
+ """
111
+
112
+ if library.id is None:
113
+ raise ValueError("Cannot update a discovery config library that has not been created yet (id is None)")
114
+
115
+ if library.yaml is None:
116
+ raise ValueError(
117
+ "Cannot update a discovery config library without YAML content (yaml is None); "
118
+ "list results omit YAML, so fetch the full library with `get_discovery_config_library` first"
119
+ )
120
+
121
+ data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
122
+ response = self.make_request("PUT", f"/api/discovery/config-libraries/{library.id}/", data=data)
123
+ updated = DiscoveryConfigLibrary.model_validate(response.json())
124
+ library.is_valid = updated.is_valid
125
+ library.validation_error = updated.validation_error
126
+ library.modified = updated.modified
127
+ logger.debug('Update of discovery config library "%s" successful', library.name)
128
+ return library
129
+
130
+ def create_or_update_discovery_config_library(self, library: DiscoveryConfigLibrary) -> DiscoveryConfigLibrary:
131
+ """
132
+ Creates the library, or updates the existing one with the same name, namespace, and config type.
133
+
134
+ Sets the library's `id` property.
135
+ """
136
+
137
+ library_id = self._get_discovery_config_library_id_by_name(library.name, library.config_type, library.namespace)
138
+ if library_id is not None:
139
+ library.id = library_id
140
+ return self.update_discovery_config_library(library)
141
+
142
+ return self.create_discovery_config_library(library)
143
+
144
+ def delete_discovery_config_library_by_id_if_exists(
145
+ self, library_id: DiscoveryConfigLibraryId, *, force: bool = False
146
+ ) -> None:
147
+ """
148
+ Deletes the discovery config library with the given ID.
149
+
150
+ No-op if the library does not exist.
151
+
152
+ If the library is imported by any discovery configs,
153
+ the server will return 409 Conflict unless `force=True` is passed.
154
+ """
155
+
156
+ params = {"force": "true"} if force else None
157
+ self._delete_if_exists(f"/api/discovery/config-libraries/{library_id}/", params=params)
158
+
159
+ def delete_discovery_config_library_by_name_if_exists(
160
+ self, name: str, config_type: DiscoveryConfigType, namespace: str = "", *, force: bool = False
161
+ ) -> None:
162
+ """
163
+ Deletes the discovery config library with the given name, type, and namespace.
164
+
165
+ Library names are unique per type within a namespace,
166
+ so a type is required to identify a single library.
167
+ No-op if no such library exists.
168
+ """
169
+
170
+ library_id = self._get_discovery_config_library_id_by_name(name, config_type, namespace)
171
+ if library_id is not None:
172
+ self.delete_discovery_config_library_by_id_if_exists(library_id, force=force)
@@ -1,6 +1,7 @@
1
1
  from datamasque.client.base import FileOrContent, UploadFile
2
2
  from datamasque.client.connections import ConnectionClient
3
3
  from datamasque.client.discovery import DiscoveryClient
4
+ from datamasque.client.discovery_config_libraries import DiscoveryConfigLibraryClient
4
5
  from datamasque.client.discovery_configs import DiscoveryConfigClient
5
6
  from datamasque.client.files import FileClient
6
7
  from datamasque.client.license import LicenseClient
@@ -22,6 +23,7 @@ class DataMasqueClient(
22
23
  RunClient,
23
24
  DiscoveryClient,
24
25
  DiscoveryConfigClient,
26
+ DiscoveryConfigLibraryClient,
25
27
  UserClient,
26
28
  SettingsClient,
27
29
  ):
@@ -166,6 +166,7 @@ class MongoConnectionConfig(ConnectionConfig):
166
166
  tls_ca_file: str = ""
167
167
  tls_allow_invalid_certificates: bool = False
168
168
  direct_connection: bool = False
169
+ retry_writes: bool = True
169
170
  replica_set: str = ""
170
171
  is_read_only: bool = False
171
172
 
@@ -194,6 +195,8 @@ class MongoConnectionConfig(ConnectionConfig):
194
195
  d.pop("tls_allow_invalid_certificates", None)
195
196
  if not d.get("direct_connection"):
196
197
  d.pop("direct_connection", None)
198
+ if d.get("retry_writes", True):
199
+ d.pop("retry_writes", None)
197
200
  if not d.get("replica_set"):
198
201
  d.pop("replica_set", None)
199
202
  if not d.get("user"):
@@ -0,0 +1,33 @@
1
+ from datetime import datetime
2
+ from typing import NewType, Optional
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field
5
+
6
+ from datamasque.client.models.discovery_config import DiscoveryConfigType
7
+ from datamasque.client.models.status import ValidationStatus
8
+
9
+ DiscoveryConfigLibraryId = NewType("DiscoveryConfigLibraryId", str)
10
+
11
+
12
+ class DiscoveryConfigLibrary(BaseModel):
13
+ """
14
+ Represents a named, namespaced, persisted YAML discovery config library.
15
+
16
+ Library names are unique per config type within a namespace,
17
+ so a database and a file library may share a name.
18
+ """
19
+
20
+ model_config = ConfigDict(extra="allow", populate_by_name=True)
21
+
22
+ name: str
23
+ config_type: DiscoveryConfigType
24
+ namespace: str = ""
25
+ yaml: Optional[str] = Field(default=None, alias="config_yaml")
26
+ # Server-populated read-only fields, excluded from request bodies.
27
+ id: Optional[DiscoveryConfigLibraryId] = Field(default=None, exclude=True)
28
+ is_valid: Optional[ValidationStatus] = Field(default=None, exclude=True)
29
+ """Validation status; libraries are validated synchronously on create/update."""
30
+ validation_error: Optional[str] = Field(default=None, exclude=True)
31
+ """Human-readable validation error, or `None` when valid."""
32
+ created: Optional[datetime] = Field(default=None, exclude=True)
33
+ modified: Optional[datetime] = Field(default=None, exclude=True)
@@ -4,7 +4,7 @@ from typing import Any, NewType, Optional
4
4
  from pydantic import Field
5
5
 
6
6
  from datamasque.client.models.git import GitTrackedEntity
7
- from datamasque.client.models.status import ValidationErrorType, ValidationStatus
7
+ from datamasque.client.models.status import ValidationErrorDetails, ValidationStatus
8
8
 
9
9
  RulesetId = NewType("RulesetId", str)
10
10
 
@@ -44,7 +44,5 @@ class Ruleset(GitTrackedEntity):
44
44
  # Server-populated read-only fields, excluded from request bodies.
45
45
  id: Optional[RulesetId] = Field(default=None, exclude=True)
46
46
  is_valid: Optional[ValidationStatus] = Field(default=None, exclude=True)
47
- validation_error: Optional[str] = Field(default=None, exclude=True)
48
- """Human-readable validation error, or `None` when valid."""
49
- validation_error_type: Optional[ValidationErrorType] = Field(default=None, exclude=True)
50
- """Category of the validation failure, or `None` when valid."""
47
+ validation_errors: list[ValidationErrorDetails] = Field(default_factory=list, exclude=True)
48
+ """Validation errors surfaced by the server; empty when valid."""
@@ -4,7 +4,7 @@ from typing import NewType, Optional
4
4
  from pydantic import Field
5
5
 
6
6
  from datamasque.client.models.git import GitTrackedEntity
7
- from datamasque.client.models.status import ValidationStatus
7
+ from datamasque.client.models.status import ValidationErrorDetails, ValidationStatus
8
8
 
9
9
  RulesetLibraryId = NewType("RulesetLibraryId", str)
10
10
 
@@ -19,7 +19,7 @@ class RulesetLibrary(GitTrackedEntity):
19
19
  # Server-populated read-only fields, excluded from request bodies.
20
20
  id: Optional[RulesetLibraryId] = Field(default=None, exclude=True)
21
21
  is_valid: Optional[ValidationStatus] = Field(default=None, exclude=True)
22
- validation_error: Optional[str] = Field(default=None, exclude=True)
23
- """Human-readable validation error, or `None` when valid."""
22
+ validation_errors: list[ValidationErrorDetails] = Field(default_factory=list, exclude=True)
23
+ """Validation errors surfaced by the server; empty when valid."""
24
24
  created: Optional[datetime] = Field(default=None, exclude=True)
25
25
  modified: Optional[datetime] = Field(default=None, exclude=True)
@@ -1,4 +1,7 @@
1
1
  import enum
2
+ from typing import Optional
3
+
4
+ from pydantic import BaseModel
2
5
 
3
6
 
4
7
  class ValidationStatus(enum.Enum):
@@ -11,7 +14,7 @@ class ValidationStatus(enum.Enum):
11
14
 
12
15
 
13
16
  class ValidationErrorType(enum.Enum):
14
- """Categorises why a ruleset failed validation (see `Ruleset.validation_error_type`)."""
17
+ """Categorises why validation failed (see `ValidationErrorDetails.validation_error_type`)."""
15
18
 
16
19
  ruleset = "ruleset"
17
20
  library_missing = "library_missing"
@@ -19,6 +22,15 @@ class ValidationErrorType(enum.Enum):
19
22
  expansion = "expansion" # The ruleset is not valid once its library references are expanded.
20
23
 
21
24
 
25
+ class ValidationErrorDetails(BaseModel):
26
+ """A single validation error, with its position in the YAML when available."""
27
+
28
+ message: str
29
+ validation_error_type: Optional[ValidationErrorType] = None
30
+ line_number: Optional[int] = None
31
+ column_number: Optional[int] = None
32
+
33
+
22
34
  class MaskingRunStatus(enum.Enum):
23
35
  """List of valid masking run statuses."""
24
36
 
@@ -64,7 +64,7 @@ class RulesetLibraryClient(BaseClient):
64
64
  Creates a new ruleset library on the server.
65
65
 
66
66
  Sets the library's server-assigned fields
67
- (`id`, `is_valid`, `validation_error`, `git`, `created`, `modified`) and returns the library.
67
+ (`id`, `is_valid`, `validation_errors`, `git`, `created`, `modified`) and returns the library.
68
68
  """
69
69
 
70
70
  data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
@@ -72,7 +72,7 @@ class RulesetLibraryClient(BaseClient):
72
72
  created_library = RulesetLibrary.model_validate(response.json())
73
73
  library.id = created_library.id
74
74
  library.is_valid = created_library.is_valid
75
- library.validation_error = created_library.validation_error
75
+ library.validation_errors = created_library.validation_errors
76
76
  library.git = created_library.git
77
77
  library.created = created_library.created
78
78
  library.modified = created_library.modified
@@ -93,7 +93,7 @@ class RulesetLibraryClient(BaseClient):
93
93
  response = self.make_request("PUT", f"/api/ruleset-libraries/{library.id}/", data=data)
94
94
  updated_library = RulesetLibrary.model_validate(response.json())
95
95
  library.is_valid = updated_library.is_valid
96
- library.validation_error = updated_library.validation_error
96
+ library.validation_errors = updated_library.validation_errors
97
97
  library.git = updated_library.git
98
98
  library.modified = updated_library.modified
99
99
  logger.debug('Update of ruleset library "%s" successful', library.name)
@@ -20,7 +20,7 @@ class RulesetClient(BaseClient):
20
20
  """
21
21
  Creates or updates a ruleset.
22
22
 
23
- Populates the given ruleset's `id`, `is_valid`, `validation_error`, `validation_error_type`,
23
+ Populates the given ruleset's `id`, `is_valid`, `validation_errors`,
24
24
  and `git` fields from the server response, and returns the same ruleset instance for convenience.
25
25
  """
26
26
 
@@ -29,8 +29,7 @@ class RulesetClient(BaseClient):
29
29
  created = Ruleset.model_validate(response.json())
30
30
  ruleset.id = created.id
31
31
  ruleset.is_valid = created.is_valid
32
- ruleset.validation_error = created.validation_error
33
- ruleset.validation_error_type = created.validation_error_type
32
+ ruleset.validation_errors = created.validation_errors
34
33
  ruleset.git = created.git
35
34
 
36
35
  if response.status_code == 201:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "datamasque-python"
3
- version = "1.1.5"
3
+ version = "1.1.7"
4
4
  description = "Official Python client for the DataMasque data-masking API."
5
5
  authors = [
6
6
  { name = "DataMasque Ltd" },
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 1.1.3
2
+ current_version = 1.1.7
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -1169,6 +1169,29 @@ def test_mongo_connection_model_dump_omits_falsy_optional_flags():
1169
1169
  assert absent not in api_dict
1170
1170
 
1171
1171
 
1172
+ def test_mongo_connection_retry_writes_defaults_true_and_is_omitted():
1173
+ """retry_writes defaults to True (matching the server default), so it is left out of the payload."""
1174
+ conn = MongoConnectionConfig(name="mongo", host="mongo.example", database="people")
1175
+ assert conn.retry_writes is True
1176
+ api_dict = conn.model_dump(exclude_none=True, by_alias=True, mode="json")
1177
+ assert "retry_writes" not in api_dict
1178
+
1179
+
1180
+ def test_mongo_connection_sends_retry_writes_when_disabled():
1181
+ """retry_writes is sent only when explicitly disabled (e.g. AWS DocumentDB rejects retryable writes)."""
1182
+ conn = MongoConnectionConfig(name="mongo", host="mongo.example", database="people", retry_writes=False)
1183
+ api_dict = conn.model_dump(exclude_none=True, by_alias=True, mode="json")
1184
+ assert api_dict["retry_writes"] is False
1185
+
1186
+
1187
+ def test_documentdb_connection_sends_retry_writes_when_disabled():
1188
+ """A DocumentDB connection inherits retry_writes and sends it when disabled."""
1189
+ conn = DocumentDbConnectionConfig(name="docdb", host="docdb.example", database="people", retry_writes=False)
1190
+ api_dict = conn.model_dump(exclude_none=True, by_alias=True, mode="json")
1191
+ assert api_dict["db_type"] == "documentdb"
1192
+ assert api_dict["retry_writes"] is False
1193
+
1194
+
1172
1195
  def test_mongo_connection_model_validate_blanks_encrypted_password():
1173
1196
  payload = {
1174
1197
  "id": "mongo-id-1",