datamasque-python 1.3.1__tar.gz → 1.4.0__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.
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/CONTRIBUTING.rst +6 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/HISTORY.rst +23 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/PKG-INFO +1 -1
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/__init__.py +2 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/connection.py +27 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/ruleset.py +7 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/ruleset_library.py +7 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/ruleset_libraries.py +4 -1
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/rulesets.py +2 -1
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/pyproject.toml +1 -1
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_connections.py +198 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_ruleset_library.py +35 -1
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_rulesets.py +84 -1
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/uv.lock +1 -1
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/.editorconfig +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/.github/workflows/ci.yml +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/.github/workflows/release-testpypi.yml +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/.github/workflows/release.yml +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/.gitignore +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/.readthedocs.yaml +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/LICENSE +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/MANIFEST.in +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/Makefile +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/NOTICE +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/README.rst +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/RELEASING.rst +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/base.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/connections.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/discovery.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/discovery_config_libraries.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/discovery_configs.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/dmclient.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/exceptions.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/files.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/ifm.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/license.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/__init__.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/data_selection.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/discovery.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/discovery_config.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/discovery_config_library.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/dm_instance.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/files.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/git.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/ifm.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/license.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/pagination.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/rg_config.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/runs.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/safe_data_preview.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/status.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/table_reference.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/user.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/py.typed +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/rg_configs.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/runs.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/settings.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/spcs.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/table_references.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/users.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/docs/Makefile +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/docs/client.models.rst +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/docs/client.rst +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/docs/conf.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/docs/contributing.rst +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/docs/history.rst +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/docs/index.rst +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/docs/installation.rst +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/docs/make.bat +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/docs/modules.rst +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/docs/readme.rst +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/docs/usage.rst +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/__init__.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/conftest.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/helpers.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_base.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_discovery.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_discovery_config_libraries.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_discovery_configs.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_files.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_ifm.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_license.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_pagination.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_rg_configs.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_runs.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_safe_data_preview.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_settings.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_spcs.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_table_references.py +0 -0
- {datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_users.py +0 -0
|
@@ -149,6 +149,12 @@ Pull requests
|
|
|
149
149
|
5. Open a PR against ``main`` and describe what the change does and why.
|
|
150
150
|
6. The maintainers will review and either merge, request changes, or close with an explanation.
|
|
151
151
|
|
|
152
|
+
Changelog
|
|
153
|
+
=========
|
|
154
|
+
|
|
155
|
+
Create a new version heading in ``HISTORY.rst``,
|
|
156
|
+
and add one concise bullet point for each high-level change.
|
|
157
|
+
|
|
152
158
|
Commit messages
|
|
153
159
|
===============
|
|
154
160
|
|
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
History
|
|
3
3
|
=======
|
|
4
4
|
|
|
5
|
+
1.4.0 (2026-09-25)
|
|
6
|
+
------------------
|
|
7
|
+
|
|
8
|
+
* Added ``LicenseLock`` (``locked_at`` and ``editable_from``) and a read-only ``license_lock`` field on connection configs.
|
|
9
|
+
The server reports it only on instances whose license caps the number of connections
|
|
10
|
+
and counts that connection's type against the cap,
|
|
11
|
+
once a connection's first run has finished successfully;
|
|
12
|
+
otherwise it is ``None``.
|
|
13
|
+
It is excluded when a config is serialized back to the API,
|
|
14
|
+
as it must never be sent on create or update.
|
|
15
|
+
|
|
16
|
+
* Added ``unlicensed_feature_warnings`` to ``Ruleset`` and ``RulesetLibrary``: at most one sentence naming the features
|
|
17
|
+
each uses that the server's license does not allow.
|
|
18
|
+
|
|
19
|
+
* Added read-only ``target_frozen``, ``frozen_target_fields`` and ``deletion_frozen`` fields on connection configs.
|
|
20
|
+
``target_frozen`` is ``True`` while the connection's protected fields are locked under the license edit lock,
|
|
21
|
+
``frozen_target_fields`` lists those locked fields as server config keys, such as ``schema`` (empty when not frozen),
|
|
22
|
+
and ``deletion_frozen`` is whether deleting the connection is currently blocked
|
|
23
|
+
(it can differ from ``target_frozen``: deletion stays allowed while over the connection cap).
|
|
24
|
+
All three are excluded when a config is serialized back to the API.
|
|
25
|
+
|
|
26
|
+
Requires server version 3.26.18
|
|
27
|
+
|
|
5
28
|
1.3.1 (2026-09-09)
|
|
6
29
|
------------------
|
|
7
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: datamasque-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
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/
|
|
@@ -38,6 +38,7 @@ from datamasque.client.models.connection import (
|
|
|
38
38
|
DocumentDbConnectionConfig,
|
|
39
39
|
DynamoConnectionConfig,
|
|
40
40
|
FileConnectionConfig,
|
|
41
|
+
LicenseLock,
|
|
41
42
|
MongoConnectionConfig,
|
|
42
43
|
MountedShareConnectionConfig,
|
|
43
44
|
MssqlLinkedServerConnectionConfig,
|
|
@@ -243,6 +244,7 @@ __all__ = [
|
|
|
243
244
|
"LengthUnit",
|
|
244
245
|
"LengthsStatistics",
|
|
245
246
|
"LicenseInfo",
|
|
247
|
+
"LicenseLock",
|
|
246
248
|
"Locator",
|
|
247
249
|
"MaskType",
|
|
248
250
|
"MaskedFormEntry",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Connection configuration models for the DataMasque API."""
|
|
2
2
|
|
|
3
|
+
from datetime import datetime
|
|
3
4
|
from enum import Enum
|
|
4
5
|
from typing import Any, Callable, Literal, NewType, Optional
|
|
5
6
|
|
|
@@ -99,6 +100,22 @@ class SseConfig(BaseModel):
|
|
|
99
100
|
return self
|
|
100
101
|
|
|
101
102
|
|
|
103
|
+
class LicenseLock(BaseModel):
|
|
104
|
+
"""
|
|
105
|
+
Read-only license-lock metadata for a connection.
|
|
106
|
+
|
|
107
|
+
The server reports this only on instances whose license has a finite connection cap
|
|
108
|
+
and counts this connection's type against that cap,
|
|
109
|
+
once the connection's first run has finished successfully;
|
|
110
|
+
otherwise it is `null`.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
model_config = ConfigDict(extra="allow")
|
|
114
|
+
|
|
115
|
+
locked_at: datetime
|
|
116
|
+
editable_from: datetime
|
|
117
|
+
|
|
118
|
+
|
|
102
119
|
class ConnectionConfig(BaseModel):
|
|
103
120
|
"""
|
|
104
121
|
Base class for all connection configurations.
|
|
@@ -111,6 +128,16 @@ class ConnectionConfig(BaseModel):
|
|
|
111
128
|
|
|
112
129
|
name: str
|
|
113
130
|
id: Optional[ConnectionId] = None
|
|
131
|
+
# Server-populated and read-only, so it is excluded when serializing a config back to the API.
|
|
132
|
+
license_lock: Optional[LicenseLock] = Field(default=None, exclude=True)
|
|
133
|
+
# Whether the connection's protected fields are currently locked under the license edit lock.
|
|
134
|
+
target_frozen: bool = Field(default=False, exclude=True)
|
|
135
|
+
# The protected fields currently locked, as server config keys (e.g. `schema`, not `db_schema`);
|
|
136
|
+
# empty when `target_frozen` is False.
|
|
137
|
+
frozen_target_fields: list[str] = Field(default_factory=list, exclude=True)
|
|
138
|
+
# Whether deleting the connection is currently blocked. Not always equal to `target_frozen`:
|
|
139
|
+
# deletion stays allowed while over the connection cap, so a downgrade can be recovered from.
|
|
140
|
+
deletion_frozen: bool = Field(default=False, exclude=True)
|
|
114
141
|
|
|
115
142
|
|
|
116
143
|
class DynamoConnectionConfig(ConnectionConfig):
|
|
@@ -46,3 +46,10 @@ class Ruleset(GitTrackedEntity):
|
|
|
46
46
|
is_valid: Optional[ValidationStatus] = Field(default=None, exclude=True)
|
|
47
47
|
validation_errors: list[ValidationErrorDetails] = Field(default_factory=list, exclude=True)
|
|
48
48
|
"""Validation errors surfaced by the server; empty when valid."""
|
|
49
|
+
unlicensed_feature_warnings: Optional[list[str]] = Field(default=None, exclude=True)
|
|
50
|
+
"""
|
|
51
|
+
At most one sentence, naming every feature the ruleset uses that the server's license does not allow.
|
|
52
|
+
Empty when the ruleset needs nothing its license withholds,
|
|
53
|
+
and `None` when the server does not know which features the ruleset uses, for example before it has passed
|
|
54
|
+
validation. `None` is not the same as having no warnings.
|
|
55
|
+
"""
|
{datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/ruleset_library.py
RENAMED
|
@@ -21,5 +21,12 @@ class RulesetLibrary(GitTrackedEntity):
|
|
|
21
21
|
is_valid: Optional[ValidationStatus] = Field(default=None, exclude=True)
|
|
22
22
|
validation_errors: list[ValidationErrorDetails] = Field(default_factory=list, exclude=True)
|
|
23
23
|
"""Validation errors surfaced by the server; empty when valid."""
|
|
24
|
+
unlicensed_feature_warnings: Optional[list[str]] = Field(default=None, exclude=True)
|
|
25
|
+
"""
|
|
26
|
+
At most one sentence, naming every feature the library uses that the server's license does not allow.
|
|
27
|
+
Empty when the license allows every feature,
|
|
28
|
+
and `None` when the server does not know which features the library uses, for example before it has passed
|
|
29
|
+
validation.
|
|
30
|
+
"""
|
|
24
31
|
created: Optional[datetime] = Field(default=None, exclude=True)
|
|
25
32
|
modified: Optional[datetime] = Field(default=None, exclude=True)
|
|
@@ -64,7 +64,8 @@ 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_errors`, `git`, `created`, `modified`)
|
|
67
|
+
(`id`, `is_valid`, `validation_errors`, `unlicensed_feature_warnings`, `git`, `created`, `modified`)
|
|
68
|
+
and returns the library.
|
|
68
69
|
"""
|
|
69
70
|
|
|
70
71
|
data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
|
|
@@ -73,6 +74,7 @@ class RulesetLibraryClient(BaseClient):
|
|
|
73
74
|
library.id = created_library.id
|
|
74
75
|
library.is_valid = created_library.is_valid
|
|
75
76
|
library.validation_errors = created_library.validation_errors
|
|
77
|
+
library.unlicensed_feature_warnings = created_library.unlicensed_feature_warnings
|
|
76
78
|
library.git = created_library.git
|
|
77
79
|
library.created = created_library.created
|
|
78
80
|
library.modified = created_library.modified
|
|
@@ -94,6 +96,7 @@ class RulesetLibraryClient(BaseClient):
|
|
|
94
96
|
updated_library = RulesetLibrary.model_validate(response.json())
|
|
95
97
|
library.is_valid = updated_library.is_valid
|
|
96
98
|
library.validation_errors = updated_library.validation_errors
|
|
99
|
+
library.unlicensed_feature_warnings = updated_library.unlicensed_feature_warnings
|
|
97
100
|
library.git = updated_library.git
|
|
98
101
|
library.modified = updated_library.modified
|
|
99
102
|
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_errors`,
|
|
23
|
+
Populates the given ruleset's `id`, `is_valid`, `validation_errors`, `unlicensed_feature_warnings`,
|
|
24
24
|
and `git` fields from the server response, and returns the same ruleset instance for convenience.
|
|
25
25
|
"""
|
|
26
26
|
|
|
@@ -30,6 +30,7 @@ class RulesetClient(BaseClient):
|
|
|
30
30
|
ruleset.id = created.id
|
|
31
31
|
ruleset.is_valid = created.is_valid
|
|
32
32
|
ruleset.validation_errors = created.validation_errors
|
|
33
|
+
ruleset.unlicensed_feature_warnings = created.unlicensed_feature_warnings
|
|
33
34
|
ruleset.git = created.git
|
|
34
35
|
|
|
35
36
|
if response.status_code == 201:
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"""Tests for `ConnectionClient` (CRUD + Snowflake-specific behaviour)."""
|
|
2
2
|
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
3
5
|
import pytest
|
|
4
6
|
import requests_mock
|
|
5
7
|
|
|
6
8
|
from datamasque.client.exceptions import DataMasqueApiError, DataMasqueException
|
|
7
9
|
from datamasque.client.models.connection import (
|
|
8
10
|
AzureConnectionConfig,
|
|
11
|
+
ConnectionConfig,
|
|
9
12
|
ConnectionId,
|
|
10
13
|
CosmosDbConnectionConfig,
|
|
11
14
|
DatabaseConnectionConfig,
|
|
@@ -13,6 +16,7 @@ from datamasque.client.models.connection import (
|
|
|
13
16
|
DatabricksConnectionConfig,
|
|
14
17
|
DocumentDbConnectionConfig,
|
|
15
18
|
DynamoConnectionConfig,
|
|
19
|
+
LicenseLock,
|
|
16
20
|
MongoConnectionConfig,
|
|
17
21
|
MountedShareConnectionConfig,
|
|
18
22
|
MssqlLinkedServerConnectionConfig,
|
|
@@ -682,6 +686,200 @@ def test_s3_connection_model_validate():
|
|
|
682
686
|
assert conn.iam_role_arn == "arn:aws:iam::111122223333:role/s3-role"
|
|
683
687
|
|
|
684
688
|
|
|
689
|
+
def test_connection_model_validate_parses_license_lock() -> None:
|
|
690
|
+
payload = {
|
|
691
|
+
"id": "88dabb63-aca5-4cc4-8f76-f78736a42f39",
|
|
692
|
+
"name": "s3",
|
|
693
|
+
"mask_type": "file",
|
|
694
|
+
"type": "s3_connection",
|
|
695
|
+
"base_directory": "data/",
|
|
696
|
+
"is_file_mask_source": True,
|
|
697
|
+
"is_file_mask_destination": False,
|
|
698
|
+
"bucket": "my-bucket",
|
|
699
|
+
"license_lock": {
|
|
700
|
+
"locked_at": "2026-01-01T12:00:00Z",
|
|
701
|
+
"editable_from": "2026-04-01T12:00:00Z",
|
|
702
|
+
},
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
conn = validate_connection(payload)
|
|
706
|
+
|
|
707
|
+
assert isinstance(conn.license_lock, LicenseLock)
|
|
708
|
+
assert conn.license_lock.locked_at == datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)
|
|
709
|
+
assert conn.license_lock.editable_from == datetime(2026, 4, 1, 12, 0, tzinfo=timezone.utc)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def test_connection_model_validate_null_license_lock() -> None:
|
|
713
|
+
payload = {
|
|
714
|
+
"id": "id-1",
|
|
715
|
+
"name": "s3",
|
|
716
|
+
"mask_type": "file",
|
|
717
|
+
"type": "s3_connection",
|
|
718
|
+
"base_directory": "",
|
|
719
|
+
"is_file_mask_source": True,
|
|
720
|
+
"is_file_mask_destination": False,
|
|
721
|
+
"bucket": "my-bucket",
|
|
722
|
+
"license_lock": None,
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
conn = validate_connection(payload)
|
|
726
|
+
assert conn.license_lock is None
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def test_connection_model_validate_parses_freeze_fields() -> None:
|
|
730
|
+
payload = {
|
|
731
|
+
"id": "db-1",
|
|
732
|
+
"name": "pg",
|
|
733
|
+
"mask_type": "database",
|
|
734
|
+
"db_type": "postgres",
|
|
735
|
+
"host": "h",
|
|
736
|
+
"port": 5432,
|
|
737
|
+
"database": "d",
|
|
738
|
+
"user": "u",
|
|
739
|
+
"schema": "public",
|
|
740
|
+
"target_frozen": True,
|
|
741
|
+
"frozen_target_fields": ["host", "port"],
|
|
742
|
+
"deletion_frozen": True,
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
conn = validate_connection(payload)
|
|
746
|
+
|
|
747
|
+
assert conn.target_frozen is True
|
|
748
|
+
assert conn.frozen_target_fields == ["host", "port"]
|
|
749
|
+
assert conn.deletion_frozen is True
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def test_connection_freeze_fields_default_when_absent() -> None:
|
|
753
|
+
payload = {
|
|
754
|
+
"id": "db-1",
|
|
755
|
+
"name": "pg",
|
|
756
|
+
"mask_type": "database",
|
|
757
|
+
"db_type": "postgres",
|
|
758
|
+
"host": "h",
|
|
759
|
+
"port": 5432,
|
|
760
|
+
"database": "d",
|
|
761
|
+
"user": "u",
|
|
762
|
+
"schema": "public",
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
conn = validate_connection(payload)
|
|
766
|
+
|
|
767
|
+
assert conn.target_frozen is False
|
|
768
|
+
assert conn.frozen_target_fields == []
|
|
769
|
+
assert conn.deletion_frozen is False
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
_LICENSE_LOCK_PAYLOAD = {"locked_at": "2026-01-01T12:00:00Z", "editable_from": "2026-04-01T12:00:00Z"}
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
@pytest.mark.parametrize(
|
|
776
|
+
"config_class, payload",
|
|
777
|
+
[
|
|
778
|
+
# Default serializer.
|
|
779
|
+
(
|
|
780
|
+
S3ConnectionConfig,
|
|
781
|
+
{
|
|
782
|
+
"id": "id-1",
|
|
783
|
+
"name": "s3",
|
|
784
|
+
"mask_type": "file",
|
|
785
|
+
"type": "s3_connection",
|
|
786
|
+
"base_directory": "",
|
|
787
|
+
"is_file_mask_source": True,
|
|
788
|
+
"is_file_mask_destination": False,
|
|
789
|
+
"bucket": "my-bucket",
|
|
790
|
+
},
|
|
791
|
+
),
|
|
792
|
+
# Each subclass below overrides `@model_serializer`, so exclusion must be proven for each.
|
|
793
|
+
(
|
|
794
|
+
DatabaseConnectionConfig,
|
|
795
|
+
{
|
|
796
|
+
"id": "db-1",
|
|
797
|
+
"name": "pg",
|
|
798
|
+
"mask_type": "database",
|
|
799
|
+
"db_type": "postgres",
|
|
800
|
+
"host": "h",
|
|
801
|
+
"port": 5432,
|
|
802
|
+
"database": "d",
|
|
803
|
+
"user": "u",
|
|
804
|
+
"schema": "public",
|
|
805
|
+
"is_read_only": False,
|
|
806
|
+
},
|
|
807
|
+
),
|
|
808
|
+
(
|
|
809
|
+
DynamoConnectionConfig,
|
|
810
|
+
{
|
|
811
|
+
"id": "dyn-1",
|
|
812
|
+
"name": "dyn",
|
|
813
|
+
"mask_type": "database",
|
|
814
|
+
"db_type": "dynamo_db",
|
|
815
|
+
"s3_bucket_name": "bucket",
|
|
816
|
+
"dynamo_append_datetime": False,
|
|
817
|
+
"dynamo_append_suffix": "-masked",
|
|
818
|
+
"dynamo_replace_tables": True,
|
|
819
|
+
"dynamo_default_region": "us-east-1",
|
|
820
|
+
},
|
|
821
|
+
),
|
|
822
|
+
(
|
|
823
|
+
MongoConnectionConfig,
|
|
824
|
+
{
|
|
825
|
+
"id": "mongo-1",
|
|
826
|
+
"name": "mongo",
|
|
827
|
+
"mask_type": "database",
|
|
828
|
+
"db_type": "mongodb",
|
|
829
|
+
"host": "mongo.example",
|
|
830
|
+
"port": 27017,
|
|
831
|
+
"database": "people",
|
|
832
|
+
"user": "alice",
|
|
833
|
+
"auth_source": "admin",
|
|
834
|
+
"tls": True,
|
|
835
|
+
"direct_connection": False,
|
|
836
|
+
"replica_set": "rs0",
|
|
837
|
+
"is_read_only": False,
|
|
838
|
+
},
|
|
839
|
+
),
|
|
840
|
+
(
|
|
841
|
+
SnowflakeConnectionConfig,
|
|
842
|
+
{
|
|
843
|
+
"id": "sf-1",
|
|
844
|
+
"name": "snowflake",
|
|
845
|
+
"mask_type": "database",
|
|
846
|
+
"db_type": "snowflake",
|
|
847
|
+
"user": "snowman",
|
|
848
|
+
"database": "icicle",
|
|
849
|
+
"snowflake_account_id": "ABCDEF-123456",
|
|
850
|
+
"snowflake_warehouse": "warehouse1",
|
|
851
|
+
"snowflake_storage_integration_name": "mysi",
|
|
852
|
+
"host": "snowflake.com",
|
|
853
|
+
"port": 443,
|
|
854
|
+
"s3_bucket_name": "ice-bucket",
|
|
855
|
+
"iam_role_arn": "role",
|
|
856
|
+
"snowflake_stage_location": "aws_s3",
|
|
857
|
+
"is_read_only": False,
|
|
858
|
+
},
|
|
859
|
+
),
|
|
860
|
+
],
|
|
861
|
+
)
|
|
862
|
+
def test_read_only_server_fields_are_excluded_when_serializing(
|
|
863
|
+
config_class: type[ConnectionConfig], payload: dict
|
|
864
|
+
) -> None:
|
|
865
|
+
# `license_lock` and the target-freeze fields are server-populated and read-only, so they must
|
|
866
|
+
# never be sent back on create/update. Cover the subclasses with custom `@model_serializer`
|
|
867
|
+
# overrides, not just the default serializer.
|
|
868
|
+
conn = config_class.model_validate(
|
|
869
|
+
{
|
|
870
|
+
**payload,
|
|
871
|
+
"license_lock": _LICENSE_LOCK_PAYLOAD,
|
|
872
|
+
"target_frozen": True,
|
|
873
|
+
"frozen_target_fields": ["host"],
|
|
874
|
+
"deletion_frozen": True,
|
|
875
|
+
}
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
dumped = conn.model_dump(by_alias=True, mode="json")
|
|
879
|
+
for field in ("license_lock", "target_frozen", "frozen_target_fields", "deletion_frozen"):
|
|
880
|
+
assert field not in dumped
|
|
881
|
+
|
|
882
|
+
|
|
685
883
|
def test_s3_connection_model_validate_no_iam_role():
|
|
686
884
|
payload = {
|
|
687
885
|
"id": "id-1",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Tests for ruleset library support in the DataMasque client."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, Optional
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
import requests_mock
|
|
@@ -739,3 +739,37 @@ def test_create_ruleset_library_collapses_git_snapshot(
|
|
|
739
739
|
assert result.git.commit_sha == "abc123"
|
|
740
740
|
assert result.git.repo_url == "https://git.example.com/repo.git"
|
|
741
741
|
assert result.git.synced_at == datetime.fromisoformat("2025-06-01T10:00:00+00:00")
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
UNSTRUCTURED_WARNING = "Foundation license does not allow Unstructured Masking. Upgrade to Enterprise."
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
@pytest.mark.parametrize("returned", [[UNSTRUCTURED_WARNING], [], None])
|
|
748
|
+
def test_create_and_update_ruleset_library_populate_unlicensed_feature_warnings(
|
|
749
|
+
client: DataMasqueClient, ruleset_library: RulesetLibrary, returned: Optional[list[str]]
|
|
750
|
+
) -> None:
|
|
751
|
+
"""`None` (not yet validated) stays distinct from `[]` (no warnings) on both the create and the update path."""
|
|
752
|
+
response = {
|
|
753
|
+
"id": LIBRARY_ID_1,
|
|
754
|
+
"name": "test_library",
|
|
755
|
+
"namespace": "test_ns",
|
|
756
|
+
"config_yaml": "version: '1.0'\nfunctions: []",
|
|
757
|
+
"is_valid": "valid",
|
|
758
|
+
"unlicensed_feature_warnings": returned,
|
|
759
|
+
"created": "2025-06-01T10:00:00Z",
|
|
760
|
+
"modified": "2025-06-01T10:00:00Z",
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
with requests_mock.Mocker() as m:
|
|
764
|
+
m.post("http://test-server/api/ruleset-libraries/", json=response, status_code=201)
|
|
765
|
+
m.put(f"http://test-server/api/ruleset-libraries/{LIBRARY_ID_1}/", json=response)
|
|
766
|
+
|
|
767
|
+
created = client.create_ruleset_library(ruleset_library)
|
|
768
|
+
assert created.unlicensed_feature_warnings == returned
|
|
769
|
+
assert "unlicensed_feature_warnings" not in m.last_request.json()
|
|
770
|
+
|
|
771
|
+
created.unlicensed_feature_warnings = ["stale"]
|
|
772
|
+
updated = client.update_ruleset_library(created)
|
|
773
|
+
|
|
774
|
+
assert updated.unlicensed_feature_warnings == returned
|
|
775
|
+
assert "unlicensed_feature_warnings" not in m.last_request.json()
|
|
@@ -177,6 +177,86 @@ def test_create_or_update_ruleset_validation_errors_empty_when_valid(client, rul
|
|
|
177
177
|
assert result.validation_errors == []
|
|
178
178
|
|
|
179
179
|
|
|
180
|
+
UNSTRUCTURED_WARNING = "Foundation license does not allow Unstructured Masking. Upgrade to Enterprise."
|
|
181
|
+
SUBSETTING_WARNING = "Foundation license does not allow Subsetting. Upgrade to Structured or Enterprise."
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@pytest.mark.parametrize(
|
|
185
|
+
("returned", "expected"),
|
|
186
|
+
[
|
|
187
|
+
([UNSTRUCTURED_WARNING, SUBSETTING_WARNING], [UNSTRUCTURED_WARNING, SUBSETTING_WARNING]),
|
|
188
|
+
([], []),
|
|
189
|
+
(None, None),
|
|
190
|
+
],
|
|
191
|
+
ids=["warned", "fully_licensed", "not_yet_known"],
|
|
192
|
+
)
|
|
193
|
+
def test_create_or_update_ruleset_populates_unlicensed_feature_warnings(client, ruleset, returned, expected):
|
|
194
|
+
"""Every warning the server sends is carried, and an empty list stays distinct from `None`."""
|
|
195
|
+
with requests_mock.Mocker() as m:
|
|
196
|
+
m.post(
|
|
197
|
+
"http://test-server/api/rulesets/?upsert=true",
|
|
198
|
+
json={"id": "2", "name": "test_ruleset", "is_valid": "valid", "unlicensed_feature_warnings": returned},
|
|
199
|
+
status_code=201,
|
|
200
|
+
)
|
|
201
|
+
result = client.create_or_update_ruleset(ruleset)
|
|
202
|
+
|
|
203
|
+
assert result.unlicensed_feature_warnings == expected
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_create_or_update_ruleset_unlicensed_feature_warnings_default_to_none(client, ruleset):
|
|
207
|
+
"""A server that omits the field leaves the warnings unknown rather than empty."""
|
|
208
|
+
with requests_mock.Mocker() as m:
|
|
209
|
+
m.post(
|
|
210
|
+
"http://test-server/api/rulesets/?upsert=true",
|
|
211
|
+
json={"id": "2", "name": "test_ruleset", "is_valid": "in_progress"},
|
|
212
|
+
status_code=201,
|
|
213
|
+
)
|
|
214
|
+
result = client.create_or_update_ruleset(ruleset)
|
|
215
|
+
|
|
216
|
+
assert result.unlicensed_feature_warnings is None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_list_rulesets_unlicensed_feature_warnings(client):
|
|
220
|
+
"""Each listed ruleset carries its own `unlicensed_feature_warnings`, including `None` for an unvalidated one."""
|
|
221
|
+
with requests_mock.Mocker() as m:
|
|
222
|
+
m.get(
|
|
223
|
+
"http://test-server/api/v2/rulesets/",
|
|
224
|
+
json=[
|
|
225
|
+
{
|
|
226
|
+
"id": "1",
|
|
227
|
+
"name": "unstructured_ruleset",
|
|
228
|
+
"mask_type": "database",
|
|
229
|
+
"is_valid": "valid",
|
|
230
|
+
"unlicensed_feature_warnings": [
|
|
231
|
+
"Foundation license does not allow Unstructured Masking. Upgrade to Enterprise."
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"id": "2",
|
|
236
|
+
"name": "licensed_ruleset",
|
|
237
|
+
"mask_type": "database",
|
|
238
|
+
"is_valid": "valid",
|
|
239
|
+
"unlicensed_feature_warnings": [],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"id": "3",
|
|
243
|
+
"name": "unvalidated_ruleset",
|
|
244
|
+
"mask_type": "database",
|
|
245
|
+
"is_valid": "in_progress",
|
|
246
|
+
"unlicensed_feature_warnings": None,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
status_code=200,
|
|
250
|
+
)
|
|
251
|
+
rulesets = client.list_rulesets()
|
|
252
|
+
|
|
253
|
+
assert rulesets[0].unlicensed_feature_warnings == [
|
|
254
|
+
"Foundation license does not allow Unstructured Masking. Upgrade to Enterprise."
|
|
255
|
+
]
|
|
256
|
+
assert rulesets[1].unlicensed_feature_warnings == []
|
|
257
|
+
assert rulesets[2].unlicensed_feature_warnings is None
|
|
258
|
+
|
|
259
|
+
|
|
180
260
|
def test_create_or_update_ruleset_does_not_send_read_only_fields(client, ruleset):
|
|
181
261
|
"""Read-only server fields must never be echoed back into a re-submit's request body."""
|
|
182
262
|
with requests_mock.Mocker() as m:
|
|
@@ -187,6 +267,9 @@ def test_create_or_update_ruleset_does_not_send_read_only_fields(client, ruleset
|
|
|
187
267
|
"name": "test_ruleset",
|
|
188
268
|
"is_valid": "invalid",
|
|
189
269
|
"validation_errors": [{"message": "bad", "validation_error_type": "ruleset"}],
|
|
270
|
+
"unlicensed_feature_warnings": [
|
|
271
|
+
"Foundation license does not allow Unstructured Masking. Upgrade to Enterprise."
|
|
272
|
+
],
|
|
190
273
|
},
|
|
191
274
|
status_code=201,
|
|
192
275
|
)
|
|
@@ -196,7 +279,7 @@ def test_create_or_update_ruleset_does_not_send_read_only_fields(client, ruleset
|
|
|
196
279
|
client.create_or_update_ruleset(ruleset)
|
|
197
280
|
|
|
198
281
|
body = m.last_request.json()
|
|
199
|
-
for read_only_field in ("id", "is_valid", "validation_errors"):
|
|
282
|
+
for read_only_field in ("id", "is_valid", "validation_errors", "unlicensed_feature_warnings"):
|
|
200
283
|
assert read_only_field not in body
|
|
201
284
|
# Input fields are still present.
|
|
202
285
|
assert body["config_yaml"] == "version: '1.0'\ntasks: []"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/discovery_config_libraries.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/data_selection.py
RENAMED
|
File without changes
|
|
File without changes
|
{datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/discovery_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/safe_data_preview.py
RENAMED
|
File without changes
|
|
File without changes
|
{datamasque_python-1.3.1 → datamasque_python-1.4.0}/datamasque/client/models/table_reference.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datamasque_python-1.3.1 → datamasque_python-1.4.0}/tests/test_discovery_config_libraries.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|