datamasque-python 1.1.6__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.6 → datamasque_python-1.1.7}/HISTORY.rst +7 -0
  2. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/PKG-INFO +1 -1
  3. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/__init__.py +2 -0
  4. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/connection.py +3 -0
  5. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/ruleset.py +3 -5
  6. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/ruleset_library.py +3 -3
  7. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/status.py +13 -1
  8. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/ruleset_libraries.py +3 -3
  9. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/rulesets.py +2 -3
  10. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/pyproject.toml +1 -1
  11. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/setup.cfg +1 -1
  12. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_connections.py +23 -0
  13. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_ruleset_library.py +14 -11
  14. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_rulesets.py +21 -13
  15. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/uv.lock +1 -1
  16. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/.editorconfig +0 -0
  17. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/.github/workflows/ci.yml +0 -0
  18. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/.github/workflows/release-testpypi.yml +0 -0
  19. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/.github/workflows/release.yml +0 -0
  20. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/.gitignore +0 -0
  21. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/.readthedocs.yaml +0 -0
  22. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/CONTRIBUTING.rst +0 -0
  23. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/LICENSE +0 -0
  24. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/MANIFEST.in +0 -0
  25. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/Makefile +0 -0
  26. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/NOTICE +0 -0
  27. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/README.rst +0 -0
  28. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/base.py +0 -0
  29. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/connections.py +0 -0
  30. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/discovery.py +0 -0
  31. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/discovery_config_libraries.py +0 -0
  32. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/discovery_configs.py +0 -0
  33. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/dmclient.py +0 -0
  34. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/exceptions.py +0 -0
  35. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/files.py +0 -0
  36. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/ifm.py +0 -0
  37. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/license.py +0 -0
  38. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/__init__.py +0 -0
  39. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/data_selection.py +0 -0
  40. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/discovery.py +0 -0
  41. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/discovery_config.py +0 -0
  42. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/discovery_config_library.py +0 -0
  43. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/dm_instance.py +0 -0
  44. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/files.py +0 -0
  45. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/git.py +0 -0
  46. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/ifm.py +0 -0
  47. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/license.py +0 -0
  48. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/pagination.py +0 -0
  49. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/runs.py +0 -0
  50. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/models/user.py +0 -0
  51. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/py.typed +0 -0
  52. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/runs.py +0 -0
  53. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/settings.py +0 -0
  54. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/spcs.py +0 -0
  55. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/datamasque/client/users.py +0 -0
  56. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/docs/Makefile +0 -0
  57. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/docs/client.models.rst +0 -0
  58. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/docs/client.rst +0 -0
  59. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/docs/conf.py +0 -0
  60. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/docs/contributing.rst +0 -0
  61. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/docs/history.rst +0 -0
  62. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/docs/index.rst +0 -0
  63. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/docs/installation.rst +0 -0
  64. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/docs/make.bat +0 -0
  65. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/docs/modules.rst +0 -0
  66. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/docs/readme.rst +0 -0
  67. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/docs/usage.rst +0 -0
  68. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/__init__.py +0 -0
  69. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/conftest.py +0 -0
  70. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/helpers.py +0 -0
  71. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_base.py +0 -0
  72. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_discovery.py +0 -0
  73. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_discovery_config_libraries.py +0 -0
  74. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_discovery_configs.py +0 -0
  75. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_files.py +0 -0
  76. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_ifm.py +0 -0
  77. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_license.py +0 -0
  78. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_pagination.py +0 -0
  79. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_runs.py +0 -0
  80. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_settings.py +0 -0
  81. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_spcs.py +0 -0
  82. {datamasque_python-1.1.6 → datamasque_python-1.1.7}/tests/test_users.py +0 -0
@@ -2,6 +2,13 @@
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
+
5
12
  1.1.6 (2026-07-13)
6
13
  ------------------
7
14
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datamasque-python
3
- Version: 1.1.6
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/
@@ -119,6 +119,7 @@ from datamasque.client.models.runs import (
119
119
  from datamasque.client.models.status import (
120
120
  AsyncRulesetGenerationTaskStatus,
121
121
  MaskingRunStatus,
122
+ ValidationErrorDetails,
122
123
  ValidationErrorType,
123
124
  ValidationStatus,
124
125
  )
@@ -234,6 +235,7 @@ __all__ = [
234
235
  "UserId",
235
236
  "UserRole",
236
237
  "UserSelection",
238
+ "ValidationErrorDetails",
237
239
  "ValidationErrorType",
238
240
  "ValidationStatus",
239
241
  ]
@@ -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"):
@@ -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.6"
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.6
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",
@@ -14,6 +14,7 @@ from datamasque.client.models.ruleset_library import (
14
14
  RulesetLibraryId,
15
15
  ValidationStatus,
16
16
  )
17
+ from datamasque.client.models.status import ValidationErrorDetails
17
18
 
18
19
  LIBRARY_ID_1 = "aaaaaaaa-1111-2222-3333-444444444444"
19
20
  LIBRARY_ID_2 = "bbbbbbbb-1111-2222-3333-444444444444"
@@ -618,17 +619,17 @@ def test_list_rulesets_using_library_pagination(client: DataMasqueClient) -> Non
618
619
  assert rulesets[2].name == "r3"
619
620
 
620
621
 
621
- def test_create_ruleset_library_populates_validation_error(
622
+ def test_create_ruleset_library_populates_validation_errors(
622
623
  client: DataMasqueClient, ruleset_library: RulesetLibrary
623
624
  ) -> None:
624
- """The server's `validation_error` string is surfaced on the returned library."""
625
+ """The server's `validation_errors` list is surfaced on the returned library."""
625
626
  create_response = {
626
627
  "id": LIBRARY_ID_1,
627
628
  "name": "test_library",
628
629
  "namespace": "test_ns",
629
630
  "config_yaml": "version: '1.0'\nfunctions: []",
630
631
  "is_valid": "invalid",
631
- "validation_error": "Invalid function definition",
632
+ "validation_errors": [{"message": "Invalid function definition"}],
632
633
  "created": "2025-06-01T10:00:00Z",
633
634
  "modified": "2025-06-01T10:00:00Z",
634
635
  }
@@ -641,13 +642,14 @@ def test_create_ruleset_library_populates_validation_error(
641
642
  )
642
643
  result = client.create_ruleset_library(ruleset_library)
643
644
 
644
- assert result.validation_error == "Invalid function definition"
645
+ assert len(result.validation_errors) == 1
646
+ assert result.validation_errors[0].message == "Invalid function definition"
645
647
 
646
648
 
647
- def test_update_ruleset_library_populates_validation_error(
649
+ def test_update_ruleset_library_populates_validation_errors(
648
650
  client: DataMasqueClient, ruleset_library: RulesetLibrary
649
651
  ) -> None:
650
- """A full update also surfaces the server's `validation_error` string."""
652
+ """A full update also surfaces the server's `validation_errors` list."""
651
653
  ruleset_library.id = RulesetLibraryId(LIBRARY_ID_1)
652
654
  update_response = {
653
655
  "id": LIBRARY_ID_1,
@@ -655,7 +657,7 @@ def test_update_ruleset_library_populates_validation_error(
655
657
  "namespace": "test_ns",
656
658
  "config_yaml": "version: '1.0'\nfunctions: []",
657
659
  "is_valid": "invalid",
658
- "validation_error": "Invalid function definition",
660
+ "validation_errors": [{"message": "Invalid function definition"}],
659
661
  "created": "2025-06-01T10:00:00Z",
660
662
  "modified": "2025-06-02T10:00:00Z",
661
663
  }
@@ -668,7 +670,8 @@ def test_update_ruleset_library_populates_validation_error(
668
670
  )
669
671
  result = client.update_ruleset_library(ruleset_library)
670
672
 
671
- assert result.validation_error == "Invalid function definition"
673
+ assert len(result.validation_errors) == 1
674
+ assert result.validation_errors[0].message == "Invalid function definition"
672
675
 
673
676
 
674
677
  def test_create_ruleset_library_does_not_send_read_only_fields(
@@ -677,14 +680,14 @@ def test_create_ruleset_library_does_not_send_read_only_fields(
677
680
  """Read-only server fields must never appear in the create request body."""
678
681
  ruleset_library.id = RulesetLibraryId(LIBRARY_ID_1)
679
682
  ruleset_library.is_valid = ValidationStatus.invalid
680
- ruleset_library.validation_error = "stale error"
683
+ ruleset_library.validation_errors = [ValidationErrorDetails(message="stale error")]
681
684
  create_response = {
682
685
  "id": LIBRARY_ID_1,
683
686
  "name": "test_library",
684
687
  "namespace": "test_ns",
685
688
  "config_yaml": "version: '1.0'\nfunctions: []",
686
689
  "is_valid": "invalid",
687
- "validation_error": "Invalid function definition",
690
+ "validation_errors": [{"message": "Invalid function definition"}],
688
691
  "created": "2025-06-01T10:00:00Z",
689
692
  "modified": "2025-06-01T10:00:00Z",
690
693
  }
@@ -698,7 +701,7 @@ def test_create_ruleset_library_does_not_send_read_only_fields(
698
701
  client.create_ruleset_library(ruleset_library)
699
702
 
700
703
  body = m.last_request.json()
701
- for read_only_field in ("id", "is_valid", "validation_error", "created", "modified"):
704
+ for read_only_field in ("id", "is_valid", "validation_errors", "created", "modified"):
702
705
  assert read_only_field not in body
703
706
  # Input fields are still present.
704
707
  assert body["name"] == "test_library"
@@ -134,8 +134,8 @@ def test_delete_ruleset_that_does_not_exist(client, existing_rulesets_json):
134
134
  assert m.request_history[0].method == "GET"
135
135
 
136
136
 
137
- def test_create_or_update_ruleset_populates_validation_error(client, ruleset):
138
- """The server's `validation_error` string and `validation_error_type` are surfaced on the returned ruleset."""
137
+ def test_create_or_update_ruleset_populates_validation_errors(client, ruleset):
138
+ """The server's `validation_errors` list is surfaced on the returned ruleset."""
139
139
  with requests_mock.Mocker() as m:
140
140
  m.post(
141
141
  "http://test-server/api/rulesets/?upsert=true",
@@ -143,19 +143,29 @@ def test_create_or_update_ruleset_populates_validation_error(client, ruleset):
143
143
  "id": "2",
144
144
  "name": "test_ruleset",
145
145
  "is_valid": "invalid",
146
- "validation_error": "Ruleset error: Missing required field: table",
147
- "validation_error_type": "ruleset",
146
+ "validation_errors": [
147
+ {
148
+ "message": "Missing required field: table",
149
+ "validation_error_type": "ruleset",
150
+ "line_number": 5,
151
+ "column_number": 3,
152
+ }
153
+ ],
148
154
  },
149
155
  status_code=201,
150
156
  )
151
157
  result = client.create_or_update_ruleset(ruleset)
152
158
 
153
- assert result.validation_error == "Ruleset error: Missing required field: table"
154
- assert result.validation_error_type is ValidationErrorType.ruleset
159
+ assert len(result.validation_errors) == 1
160
+ error = result.validation_errors[0]
161
+ assert error.message == "Missing required field: table"
162
+ assert error.validation_error_type is ValidationErrorType.ruleset
163
+ assert error.line_number == 5
164
+ assert error.column_number == 3
155
165
 
156
166
 
157
- def test_create_or_update_ruleset_validation_error_none_when_absent(client, ruleset):
158
- """`validation_error` and `validation_error_type` are `None` when the server omits them (a valid ruleset)."""
167
+ def test_create_or_update_ruleset_validation_errors_empty_when_valid(client, ruleset):
168
+ """`validation_errors` is empty when the server omits it (a valid ruleset)."""
159
169
  with requests_mock.Mocker() as m:
160
170
  m.post(
161
171
  "http://test-server/api/rulesets/?upsert=true",
@@ -164,8 +174,7 @@ def test_create_or_update_ruleset_validation_error_none_when_absent(client, rule
164
174
  )
165
175
  result = client.create_or_update_ruleset(ruleset)
166
176
 
167
- assert result.validation_error is None
168
- assert result.validation_error_type is None
177
+ assert result.validation_errors == []
169
178
 
170
179
 
171
180
  def test_create_or_update_ruleset_does_not_send_read_only_fields(client, ruleset):
@@ -177,8 +186,7 @@ def test_create_or_update_ruleset_does_not_send_read_only_fields(client, ruleset
177
186
  "id": "2",
178
187
  "name": "test_ruleset",
179
188
  "is_valid": "invalid",
180
- "validation_error": "Ruleset error: bad",
181
- "validation_error_type": "ruleset",
189
+ "validation_errors": [{"message": "bad", "validation_error_type": "ruleset"}],
182
190
  },
183
191
  status_code=201,
184
192
  )
@@ -188,7 +196,7 @@ def test_create_or_update_ruleset_does_not_send_read_only_fields(client, ruleset
188
196
  client.create_or_update_ruleset(ruleset)
189
197
 
190
198
  body = m.last_request.json()
191
- for read_only_field in ("id", "is_valid", "validation_error", "validation_error_type"):
199
+ for read_only_field in ("id", "is_valid", "validation_errors"):
192
200
  assert read_only_field not in body
193
201
  # Input fields are still present.
194
202
  assert body["config_yaml"] == "version: '1.0'\ntasks: []"
@@ -428,7 +428,7 @@ toml = [
428
428
 
429
429
  [[package]]
430
430
  name = "datamasque-python"
431
- version = "1.1.6"
431
+ version = "1.1.7"
432
432
  source = { editable = "." }
433
433
  dependencies = [
434
434
  { name = "pydantic" },