datamasque-python 1.1.1__tar.gz → 1.1.3__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 (77) hide show
  1. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/HISTORY.rst +15 -0
  2. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/PKG-INFO +1 -1
  3. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/__init__.py +2 -0
  4. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/discovery.py +6 -2
  5. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/connection.py +28 -0
  6. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/status.py +14 -1
  7. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/pyproject.toml +1 -1
  8. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/setup.cfg +1 -1
  9. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_connections.py +106 -0
  10. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_discovery.py +56 -0
  11. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/uv.lock +1 -1
  12. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/.editorconfig +0 -0
  13. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/.github/workflows/ci.yml +0 -0
  14. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/.github/workflows/release-testpypi.yml +0 -0
  15. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/.github/workflows/release.yml +0 -0
  16. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/.gitignore +0 -0
  17. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/.readthedocs.yaml +0 -0
  18. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/CONTRIBUTING.rst +0 -0
  19. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/LICENSE +0 -0
  20. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/MANIFEST.in +0 -0
  21. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/Makefile +0 -0
  22. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/NOTICE +0 -0
  23. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/README.rst +0 -0
  24. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/base.py +0 -0
  25. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/connections.py +0 -0
  26. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/discovery_configs.py +0 -0
  27. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/dmclient.py +0 -0
  28. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/exceptions.py +0 -0
  29. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/files.py +0 -0
  30. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/ifm.py +0 -0
  31. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/license.py +0 -0
  32. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/__init__.py +0 -0
  33. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/data_selection.py +0 -0
  34. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/discovery.py +0 -0
  35. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/discovery_config.py +0 -0
  36. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/dm_instance.py +0 -0
  37. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/files.py +0 -0
  38. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/git.py +0 -0
  39. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/ifm.py +0 -0
  40. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/license.py +0 -0
  41. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/pagination.py +0 -0
  42. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/ruleset.py +0 -0
  43. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/ruleset_library.py +0 -0
  44. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/runs.py +0 -0
  45. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/models/user.py +0 -0
  46. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/py.typed +0 -0
  47. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/ruleset_libraries.py +0 -0
  48. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/rulesets.py +0 -0
  49. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/runs.py +0 -0
  50. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/settings.py +0 -0
  51. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/datamasque/client/users.py +0 -0
  52. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/docs/Makefile +0 -0
  53. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/docs/client.models.rst +0 -0
  54. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/docs/client.rst +0 -0
  55. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/docs/conf.py +0 -0
  56. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/docs/contributing.rst +0 -0
  57. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/docs/history.rst +0 -0
  58. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/docs/index.rst +0 -0
  59. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/docs/installation.rst +0 -0
  60. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/docs/make.bat +0 -0
  61. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/docs/modules.rst +0 -0
  62. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/docs/readme.rst +0 -0
  63. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/docs/usage.rst +0 -0
  64. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/__init__.py +0 -0
  65. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/conftest.py +0 -0
  66. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/helpers.py +0 -0
  67. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_base.py +0 -0
  68. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_discovery_configs.py +0 -0
  69. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_files.py +0 -0
  70. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_ifm.py +0 -0
  71. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_license.py +0 -0
  72. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_pagination.py +0 -0
  73. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_ruleset_library.py +0 -0
  74. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_rulesets.py +0 -0
  75. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_runs.py +0 -0
  76. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_settings.py +0 -0
  77. {datamasque_python-1.1.1 → datamasque_python-1.1.3}/tests/test_users.py +0 -0
@@ -2,6 +2,21 @@
2
2
  History
3
3
  =======
4
4
 
5
+ 1.1.3 (2026-06-26)
6
+ ------------------
7
+
8
+ * Added ``tls_ca_file`` and ``tls_allow_invalid_certificates`` to ``MongoConnectionConfig``,
9
+ serialized only when TLS is enabled (``tls_ca_file`` only when set,
10
+ ``tls_allow_invalid_certificates`` only when ``True``).
11
+ * Added ``DatabaseType.documentdb`` and ``DocumentDbConnectionConfig`` for AWS DocumentDB,
12
+ which is MongoDB wire-compatible and reuses ``MongoConnectionConfig`` (differing only by ``db_type``).
13
+
14
+ 1.1.2 (2026-06-26)
15
+ ------------------
16
+
17
+ * Completed wiring of the ``finished_with_warnings`` status for ``AsyncRulesetGenerationTaskStatus``.
18
+ * Added the ``cancelled`` status to ``AsyncRulesetGenerationTaskStatus``.
19
+
5
20
  1.1.1 (2026-06-25)
6
21
  ------------------
7
22
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datamasque-python
3
- Version: 1.1.1
3
+ Version: 1.1.3
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/
@@ -33,6 +33,7 @@ from datamasque.client.models.connection import (
33
33
  DatabaseConnectionConfig,
34
34
  DatabaseType,
35
35
  DatabricksConnectionConfig,
36
+ DocumentDbConnectionConfig,
36
37
  DynamoConnectionConfig,
37
38
  FileConnectionConfig,
38
39
  MongoConnectionConfig,
@@ -150,6 +151,7 @@ __all__ = [
150
151
  "DiscoveryConfigNotFoundError",
151
152
  "DiscoveryConfigType",
152
153
  "DiscoveryMatch",
154
+ "DocumentDbConnectionConfig",
153
155
  "DynamoConnectionConfig",
154
156
  "FailedToStartError",
155
157
  "FileConnectionConfig",
@@ -166,7 +166,7 @@ class DiscoveryClient(BaseClient):
166
166
  returns a list containing one ruleset
167
167
 
168
168
  Raises `AsyncRulesetGenerationInProgressError` if the task hasn't finished yet,
169
- and `DataMasqueException` if it failed.
169
+ and `DataMasqueException` if it failed or was cancelled.
170
170
 
171
171
  Note that the ruleset(s) have autogenerated names, which you may want to customize before uploading.
172
172
  """
@@ -176,7 +176,11 @@ class DiscoveryClient(BaseClient):
176
176
  logger.error("Ruleset generation failed for connection: %s", connection_id)
177
177
  raise DataMasqueException(f"Ruleset generation failed for connection: {connection_id}")
178
178
 
179
- if status is not AsyncRulesetGenerationTaskStatus.finished:
179
+ if status is AsyncRulesetGenerationTaskStatus.cancelled:
180
+ logger.error("Ruleset generation was cancelled for connection: %s", connection_id)
181
+ raise DataMasqueException(f"Ruleset generation was cancelled for connection: {connection_id}")
182
+
183
+ if not status.is_finished:
180
184
  logger.error(
181
185
  "Ruleset generation is still in progress for connection: %s. Status: `%s`",
182
186
  connection_id,
@@ -44,6 +44,7 @@ class DatabaseType(Enum):
44
44
  mssql_linked = "mssql_linked"
45
45
  snowflake = "snowflake"
46
46
  mongodb = "mongodb"
47
+ documentdb = "documentdb"
47
48
  databricks_lakebase = "databricks_lakebase"
48
49
  databricks = "databricks"
49
50
  informix = "informix"
@@ -160,6 +161,8 @@ class MongoConnectionConfig(ConnectionConfig):
160
161
  password: Optional[str] = None
161
162
  auth_source: str = "admin"
162
163
  tls: bool = False
164
+ tls_ca_file: str = ""
165
+ tls_allow_invalid_certificates: bool = False
163
166
  direct_connection: bool = False
164
167
  replica_set: str = ""
165
168
  is_read_only: bool = False
@@ -180,6 +183,13 @@ class MongoConnectionConfig(ConnectionConfig):
180
183
  d["dbpassword"] = password
181
184
  if not d.get("tls"):
182
185
  d.pop("tls", None)
186
+ d.pop("tls_ca_file", None)
187
+ d.pop("tls_allow_invalid_certificates", None)
188
+ else:
189
+ if not d.get("tls_ca_file"):
190
+ d.pop("tls_ca_file", None)
191
+ if not d.get("tls_allow_invalid_certificates"):
192
+ d.pop("tls_allow_invalid_certificates", None)
183
193
  if not d.get("direct_connection"):
184
194
  d.pop("direct_connection", None)
185
195
  if not d.get("replica_set"):
@@ -197,6 +207,23 @@ class MongoConnectionConfig(ConnectionConfig):
197
207
  return data
198
208
 
199
209
 
210
+ class DocumentDbConnectionConfig(MongoConnectionConfig):
211
+ """
212
+ Connection configuration for an AWS DocumentDB cluster.
213
+
214
+ DocumentDB is MongoDB wire-compatible,
215
+ so it reuses `MongoConnectionConfig` (including the TLS handling)
216
+ and only differs by `db_type`/`database_type`.
217
+ """
218
+
219
+ # Narrowing the inherited Literal is a deliberate Pydantic discriminator override.
220
+ db_type: Literal["documentdb"] = "documentdb" # type: ignore[assignment]
221
+
222
+ @property
223
+ def database_type(self) -> DatabaseType:
224
+ return DatabaseType.documentdb
225
+
226
+
200
227
  class SnowflakeConnectionConfig(ConnectionConfig):
201
228
  """
202
229
  Connection configuration for a Snowflake database.
@@ -430,6 +457,7 @@ FILE_TYPE_MAP: dict[str, type[FileConnectionConfig]] = {
430
457
  DB_TYPE_MAP: dict[str, type[ConnectionConfig]] = {
431
458
  DatabaseType.dynamodb.value: DynamoConnectionConfig,
432
459
  DatabaseType.mongodb.value: MongoConnectionConfig,
460
+ DatabaseType.documentdb.value: DocumentDbConnectionConfig,
433
461
  DatabaseType.snowflake.value: SnowflakeConnectionConfig,
434
462
  DatabaseType.mssql_linked.value: MssqlLinkedServerConnectionConfig,
435
463
  DatabaseType.databricks.value: DatabricksConnectionConfig,
@@ -64,15 +64,28 @@ class AsyncRulesetGenerationTaskStatus(enum.Enum):
64
64
  failed = "failed"
65
65
  running = "running"
66
66
  queued = "queued"
67
+ cancelled = "cancelled"
67
68
 
68
69
  @classmethod
69
70
  def get_final_states(cls) -> set["AsyncRulesetGenerationTaskStatus"]:
70
71
  """Returns the list of final statuses, i.e. the ruleset generation has completed, successfully or otherwise."""
71
72
 
72
- return {cls.finished, cls.failed}
73
+ return {cls.finished, cls.finished_with_warnings, cls.failed, cls.cancelled}
74
+
75
+ @classmethod
76
+ def get_finished_states(cls) -> set["AsyncRulesetGenerationTaskStatus"]:
77
+ """Returns the list of statuses that indicate the ruleset generation completed successfully."""
78
+
79
+ return {cls.finished, cls.finished_with_warnings}
73
80
 
74
81
  @property
75
82
  def is_in_final_state(self) -> bool:
76
83
  """Returns True if this status is a final status."""
77
84
 
78
85
  return self in self.get_final_states()
86
+
87
+ @property
88
+ def is_finished(self) -> bool:
89
+ """Returns True if this status is a finished status."""
90
+
91
+ return self in self.get_finished_states()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "datamasque-python"
3
- version = "1.1.1"
3
+ version = "1.1.3"
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.1
2
+ current_version = 1.1.3
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -10,6 +10,7 @@ from datamasque.client.models.connection import (
10
10
  DatabaseConnectionConfig,
11
11
  DatabaseType,
12
12
  DatabricksConnectionConfig,
13
+ DocumentDbConnectionConfig,
13
14
  DynamoConnectionConfig,
14
15
  MongoConnectionConfig,
15
16
  MountedShareConnectionConfig,
@@ -1201,6 +1202,111 @@ def test_connection_config_dispatch_picks_mongo_subclass():
1201
1202
  assert isinstance(validate_connection(payload), MongoConnectionConfig)
1202
1203
 
1203
1204
 
1205
+ def test_mongo_tls_ca_fields_emitted_when_tls_enabled():
1206
+ conn = MongoConnectionConfig(
1207
+ name="mongo",
1208
+ host="mongo.example",
1209
+ database="people",
1210
+ tls=True,
1211
+ tls_ca_file="/certs/rds-ca.pem",
1212
+ tls_allow_invalid_certificates=True,
1213
+ )
1214
+ d = conn.model_dump(exclude_none=True, by_alias=True, mode="json")
1215
+ assert d["tls"] is True
1216
+ assert d["tls_ca_file"] == "/certs/rds-ca.pem"
1217
+ assert d["tls_allow_invalid_certificates"] is True
1218
+
1219
+
1220
+ def test_mongo_tls_ca_fields_omitted_when_unset_but_tls_enabled():
1221
+ conn = MongoConnectionConfig(name="mongo", host="mongo.example", database="people", tls=True)
1222
+ d = conn.model_dump(exclude_none=True, by_alias=True, mode="json")
1223
+ assert d["tls"] is True
1224
+ assert "tls_ca_file" not in d
1225
+ assert "tls_allow_invalid_certificates" not in d
1226
+
1227
+
1228
+ def test_mongo_tls_ca_fields_omitted_when_tls_disabled():
1229
+ """Even when set, the TLS-dependent fields are not sent while TLS is disabled."""
1230
+ conn = MongoConnectionConfig(
1231
+ name="mongo",
1232
+ host="mongo.example",
1233
+ database="people",
1234
+ tls=False,
1235
+ tls_ca_file="/certs/rds-ca.pem",
1236
+ tls_allow_invalid_certificates=True,
1237
+ )
1238
+ d = conn.model_dump(exclude_none=True, by_alias=True, mode="json")
1239
+ for absent in ("tls", "tls_ca_file", "tls_allow_invalid_certificates"):
1240
+ assert absent not in d
1241
+
1242
+
1243
+ def test_mongo_tls_ca_fields_roundtrip():
1244
+ payload = {
1245
+ "id": "mongo-tls-1",
1246
+ "name": "mongo",
1247
+ "mask_type": "database",
1248
+ "db_type": "mongodb",
1249
+ "host": "mongo.example",
1250
+ "database": "people",
1251
+ "tls": True,
1252
+ "tls_ca_file": "/certs/rds-ca.pem",
1253
+ "tls_allow_invalid_certificates": True,
1254
+ }
1255
+ conn = MongoConnectionConfig.model_validate(payload)
1256
+ assert conn.tls is True
1257
+ assert conn.tls_ca_file == "/certs/rds-ca.pem"
1258
+ assert conn.tls_allow_invalid_certificates is True
1259
+
1260
+
1261
+ def test_mongo_tls_ca_fields_default_when_missing():
1262
+ payload = {
1263
+ "id": "mongo-tls-2",
1264
+ "name": "mongo",
1265
+ "mask_type": "database",
1266
+ "db_type": "mongodb",
1267
+ "host": "mongo.example",
1268
+ "database": "people",
1269
+ }
1270
+ conn = MongoConnectionConfig.model_validate(payload)
1271
+ assert conn.tls_ca_file == ""
1272
+ assert conn.tls_allow_invalid_certificates is False
1273
+
1274
+
1275
+ def test_documentdb_connection_model_dump():
1276
+ conn = DocumentDbConnectionConfig(
1277
+ name="docdb",
1278
+ host="dtq-documentdb.cluster.example",
1279
+ database="people",
1280
+ user="dmadmin",
1281
+ password="hunter2",
1282
+ tls=True,
1283
+ tls_ca_file="/certs/rds-ca.pem",
1284
+ retry_writes=False,
1285
+ )
1286
+ d = conn.model_dump(exclude_none=True, by_alias=True, mode="json")
1287
+ assert d["db_type"] == "documentdb"
1288
+ assert d["mask_type"] == "database"
1289
+ assert d["dbpassword"] == "hunter2"
1290
+ assert d["tls"] is True
1291
+ assert d["tls_ca_file"] == "/certs/rds-ca.pem"
1292
+ assert d["retry_writes"] is False
1293
+ assert conn.database_type is DatabaseType.documentdb
1294
+
1295
+
1296
+ def test_connection_config_dispatch_picks_documentdb_subclass():
1297
+ payload = {
1298
+ "id": "docdb-id-1",
1299
+ "name": "docdb",
1300
+ "mask_type": "database",
1301
+ "db_type": "documentdb",
1302
+ "host": "dtq-documentdb.cluster.example",
1303
+ "database": "people",
1304
+ }
1305
+ conn = validate_connection(payload)
1306
+ assert isinstance(conn, DocumentDbConnectionConfig)
1307
+ assert conn.database_type is DatabaseType.documentdb
1308
+
1309
+
1204
1310
  def test_database_connection_config_rejects_mongodb_database_type():
1205
1311
  """`DatabaseConnectionConfig` is for SQL engines; MongoDB users must use `MongoConnectionConfig`."""
1206
1312
  with pytest.raises(ValueError, match="For MongoDB"):
@@ -204,6 +204,47 @@ def test_get_generated_rulesets_success(client):
204
204
  assert rulesets[1].yaml == yaml_content_2.decode("utf-8")
205
205
 
206
206
 
207
+ def test_get_generated_rulesets_finished_with_warnings_success(client):
208
+ """A task that finishes with warnings is still successful: its rulesets are returned, not treated as in-progress."""
209
+ connection_id = ConnectionId("1")
210
+ yaml_content = b"""
211
+ version: "1.0"
212
+ tasks:
213
+ - type: mask_table
214
+ table: table1
215
+ key: id
216
+ rules:
217
+ - column: col1
218
+ masks:
219
+ - type: do_nothing
220
+ """
221
+
222
+ with requests_mock.Mocker() as m:
223
+ m.get(
224
+ f"http://test-server/api/async-generate-ruleset/{connection_id}/",
225
+ json={"status": "finished_with_warnings"},
226
+ status_code=200,
227
+ )
228
+
229
+ zip_buffer = BytesIO()
230
+ with zipfile.ZipFile(zip_buffer, "w") as zip_file:
231
+ zip_file.writestr("ruleset1.yml", yaml_content.decode("utf-8"))
232
+ zip_buffer.seek(0)
233
+
234
+ m.get(
235
+ f"http://test-server/api/async-generate-ruleset/{connection_id}/download-rulesets/",
236
+ content=zip_buffer.getvalue(),
237
+ headers={"Content-Disposition": 'attachment; filename="rulesets.zip"'},
238
+ status_code=200,
239
+ )
240
+
241
+ rulesets = client.get_generated_rulesets(connection_id)
242
+
243
+ assert len(rulesets) == 1
244
+ assert rulesets[0].name == "ruleset1"
245
+ assert rulesets[0].yaml == yaml_content.decode("utf-8")
246
+
247
+
207
248
  def test_get_generated_rulesets_empty_archive_raises(client):
208
249
  """A finished task whose download archive contains no ruleset files raises a clear error."""
209
250
  connection_id = ConnectionId("1")
@@ -290,6 +331,21 @@ def test_get_generated_rulesets_failed(client):
290
331
  client.get_generated_rulesets(connection_id)
291
332
 
292
333
 
334
+ def test_get_generated_rulesets_cancelled(client):
335
+ """A cancelled task is terminal, so it raises a non-retryable DataMasqueException, not the in-progress error."""
336
+ connection_id = ConnectionId("1")
337
+
338
+ with requests_mock.Mocker() as m:
339
+ m.get(
340
+ f"http://test-server/api/async-generate-ruleset/{connection_id}/",
341
+ json={"status": "cancelled"},
342
+ status_code=200,
343
+ )
344
+
345
+ with pytest.raises(DataMasqueException, match="Ruleset generation was cancelled for connection"):
346
+ client.get_generated_rulesets(connection_id)
347
+
348
+
293
349
  def test_get_generated_rulesets_in_progress(client):
294
350
  connection_id = ConnectionId("1")
295
351
 
@@ -428,7 +428,7 @@ toml = [
428
428
 
429
429
  [[package]]
430
430
  name = "datamasque-python"
431
- version = "1.1.1"
431
+ version = "1.1.3"
432
432
  source = { editable = "." }
433
433
  dependencies = [
434
434
  { name = "pydantic" },