datamasque-python 1.3.0__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.0 → datamasque_python-1.4.0}/.github/workflows/release.yml +24 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/CONTRIBUTING.rst +6 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/HISTORY.rst +32 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/PKG-INFO +1 -1
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/RELEASING.rst +4 -25
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/__init__.py +4 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/connection.py +27 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/discovery.py +9 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/ruleset.py +7 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/ruleset_library.py +7 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/ruleset_libraries.py +4 -1
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/rulesets.py +2 -1
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/pyproject.toml +1 -1
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_connections.py +198 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_discovery.py +17 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_ruleset_library.py +35 -1
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_rulesets.py +84 -1
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/uv.lock +1 -1
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/.editorconfig +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/.github/workflows/ci.yml +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/.github/workflows/release-testpypi.yml +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/.gitignore +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/.readthedocs.yaml +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/LICENSE +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/MANIFEST.in +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/Makefile +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/NOTICE +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/README.rst +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/base.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/connections.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/discovery.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/discovery_config_libraries.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/discovery_configs.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/dmclient.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/exceptions.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/files.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/ifm.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/license.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/__init__.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/data_selection.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/discovery_config.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/discovery_config_library.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/dm_instance.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/files.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/git.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/ifm.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/license.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/pagination.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/rg_config.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/runs.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/safe_data_preview.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/status.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/table_reference.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/models/user.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/py.typed +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/rg_configs.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/runs.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/settings.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/spcs.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/table_references.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/datamasque/client/users.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/docs/Makefile +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/docs/client.models.rst +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/docs/client.rst +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/docs/conf.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/docs/contributing.rst +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/docs/history.rst +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/docs/index.rst +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/docs/installation.rst +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/docs/make.bat +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/docs/modules.rst +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/docs/readme.rst +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/docs/usage.rst +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/__init__.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/conftest.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/helpers.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_base.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_discovery_config_libraries.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_discovery_configs.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_files.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_ifm.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_license.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_pagination.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_rg_configs.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_runs.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_safe_data_preview.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_settings.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_spcs.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_table_references.py +0 -0
- {datamasque_python-1.3.0 → datamasque_python-1.4.0}/tests/test_users.py +0 -0
|
@@ -60,3 +60,27 @@ jobs:
|
|
|
60
60
|
|
|
61
61
|
- name: Publish to PyPI
|
|
62
62
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
63
|
+
|
|
64
|
+
github-release:
|
|
65
|
+
name: Publish GitHub Release
|
|
66
|
+
needs: publish
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
permissions:
|
|
69
|
+
contents: write
|
|
70
|
+
steps:
|
|
71
|
+
- name: Download distributions
|
|
72
|
+
uses: actions/download-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: dist
|
|
75
|
+
path: dist/
|
|
76
|
+
|
|
77
|
+
- name: Create GitHub Release
|
|
78
|
+
env:
|
|
79
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
80
|
+
run: |
|
|
81
|
+
gh release create "${GITHUB_REF_NAME}" \
|
|
82
|
+
--repo "${GITHUB_REPOSITORY}" \
|
|
83
|
+
--title "${GITHUB_REF_NAME}" \
|
|
84
|
+
--verify-tag \
|
|
85
|
+
--notes "See [HISTORY.rst](https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_REF_NAME}/HISTORY.rst) for changelog." \
|
|
86
|
+
dist/*
|
|
@@ -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,38 @@
|
|
|
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
|
+
|
|
28
|
+
1.3.1 (2026-09-09)
|
|
29
|
+
------------------
|
|
30
|
+
|
|
31
|
+
* Added ``max_length_unit`` to ``SchemaDiscoveryColumn``, typed by the new ``LengthUnit`` enum
|
|
32
|
+
(``chars`` or ``bytes``), reporting the unit a column's ``max_length`` is counted in.
|
|
33
|
+
It is ``None`` for results from older servers that do not report a unit.
|
|
34
|
+
|
|
35
|
+
Requires server version 3.26.17
|
|
36
|
+
|
|
5
37
|
1.3.0 (2026-09-04)
|
|
6
38
|
------------------
|
|
7
39
|
|
|
@@ -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/
|
|
@@ -68,31 +68,10 @@ Releasing to PyPI
|
|
|
68
68
|
You can verify that the release appears at https://pypi.org/project/datamasque-python/,
|
|
69
69
|
and Read the Docs has built the new tag
|
|
70
70
|
at https://datamasque-python.readthedocs.io/.
|
|
71
|
-
5.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Do this after the PyPI publish has succeeded,
|
|
77
|
-
so the release never points at a version nobody can install.
|
|
78
|
-
|
|
79
|
-
1. On the repository home page,
|
|
80
|
-
find **Releases** in the right-hand sidebar under the *About* box,
|
|
81
|
-
and click it,
|
|
82
|
-
then **Draft a new release**.
|
|
83
|
-
2. **Choose a tag** — pick the existing tag you pushed as part of releasing to PyPI.
|
|
84
|
-
Don't let GitHub create a new one; the tag should already exist from the release flow.
|
|
85
|
-
3. Set the target to ``main``.
|
|
86
|
-
4. Title it the same as the tag, e.g. ``v1.2.2``.
|
|
87
|
-
5. Paste the ``HISTORY.rst`` entry for this version into the body, converted to Markdown.
|
|
88
|
-
**Generate release notes** is a reasonable starting point for the commit list,
|
|
89
|
-
but the hand-written changelog is what users read.
|
|
90
|
-
6. Leave *Set as a pre-release* unticked for a normal release.
|
|
91
|
-
7. Click **Publish release**.
|
|
92
|
-
|
|
93
|
-
Creating releases can also be done from the ``gh`` CLI::
|
|
94
|
-
|
|
95
|
-
gh release create v1.2.2 --title v1.2.2 --notes-file notes.md
|
|
71
|
+
5. The GitHub Release is created automatically once the PyPI publish succeeds.
|
|
72
|
+
``release.yml`` runs a ``github-release`` job that creates the release for the pushed tag,
|
|
73
|
+
attaches the built distributions, and links to ``HISTORY.rst`` at that tag for the changelog.
|
|
74
|
+
Nothing to do by hand; just confirm it appears under **Releases** on the repository home page.
|
|
96
75
|
|
|
97
76
|
Releasing a dev build to TestPyPI
|
|
98
77
|
=================================
|
|
@@ -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,
|
|
@@ -73,6 +74,7 @@ from datamasque.client.models.discovery import (
|
|
|
73
74
|
ForeignKeyRef,
|
|
74
75
|
InDataDiscoveryConfig,
|
|
75
76
|
InDataDiscoveryRule,
|
|
77
|
+
LengthUnit,
|
|
76
78
|
ReferencingForeignKey,
|
|
77
79
|
RulesetGenerationRequest,
|
|
78
80
|
RulesetGenerationWithRGConfigRequest,
|
|
@@ -239,8 +241,10 @@ __all__ = [
|
|
|
239
241
|
"InvalidRulesetError",
|
|
240
242
|
"JsonPath",
|
|
241
243
|
"LengthEntry",
|
|
244
|
+
"LengthUnit",
|
|
242
245
|
"LengthsStatistics",
|
|
243
246
|
"LicenseInfo",
|
|
247
|
+
"LicenseLock",
|
|
244
248
|
"Locator",
|
|
245
249
|
"MaskType",
|
|
246
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):
|
|
@@ -367,6 +367,13 @@ class ReferencingForeignKey(BaseModel):
|
|
|
367
367
|
referencing_column: str # Dotted path: "schema.table.column".
|
|
368
368
|
|
|
369
369
|
|
|
370
|
+
class LengthUnit(Enum):
|
|
371
|
+
"""The unit a column's declared width (`max_length`) is counted in."""
|
|
372
|
+
|
|
373
|
+
chars = "chars"
|
|
374
|
+
bytes = "bytes"
|
|
375
|
+
|
|
376
|
+
|
|
370
377
|
class SchemaDiscoveryColumn(BaseModel):
|
|
371
378
|
"""Column-level data in a schema discovery result."""
|
|
372
379
|
|
|
@@ -374,6 +381,8 @@ class SchemaDiscoveryColumn(BaseModel):
|
|
|
374
381
|
|
|
375
382
|
data_type: Optional[str] = None
|
|
376
383
|
max_length: Optional[int] = None
|
|
384
|
+
# None when the server does not report a unit (older runs count chars).
|
|
385
|
+
max_length_unit: Optional[LengthUnit] = None
|
|
377
386
|
foreign_keys: list[ForeignKeyRef]
|
|
378
387
|
discovery_matches: list[DiscoveryMatch]
|
|
379
388
|
numeric_precision: Optional[int] = None
|
|
@@ -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.0 → 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",
|
|
@@ -21,6 +21,7 @@ from datamasque.client import (
|
|
|
21
21
|
FileRulesetGenerationRequest,
|
|
22
22
|
FileRulesetGenerationWithRGConfigRequest,
|
|
23
23
|
InDataDiscoveryConfig,
|
|
24
|
+
LengthUnit,
|
|
24
25
|
RGConfig,
|
|
25
26
|
RGConfigId,
|
|
26
27
|
RulesetGenerationRequest,
|
|
@@ -795,6 +796,22 @@ def test_schema_discovery_result_parses_safe_data_preview():
|
|
|
795
796
|
assert preview.statistics_common.count_distinct == 988
|
|
796
797
|
|
|
797
798
|
|
|
799
|
+
def test_schema_discovery_result_parses_max_length_unit() -> None:
|
|
800
|
+
row = _schema_discovery_row(1, "name")
|
|
801
|
+
row["data"]["max_length"] = 10
|
|
802
|
+
row["data"]["max_length_unit"] = "bytes"
|
|
803
|
+
result = SchemaDiscoveryResult.model_validate(row)
|
|
804
|
+
assert result.data.max_length == 10
|
|
805
|
+
assert result.data.max_length_unit is LengthUnit.bytes
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def test_schema_discovery_result_without_max_length_unit_is_none() -> None:
|
|
809
|
+
row = _schema_discovery_row(1, "name")
|
|
810
|
+
assert "max_length_unit" not in row["data"]
|
|
811
|
+
result = SchemaDiscoveryResult.model_validate(row)
|
|
812
|
+
assert result.data.max_length_unit is None
|
|
813
|
+
|
|
814
|
+
|
|
798
815
|
def test_list_schema_discovery_results_follows_pagination(client):
|
|
799
816
|
run_id = RunId(42)
|
|
800
817
|
page1 = {
|
|
@@ -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
|
{datamasque_python-1.3.0 → 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.0 → datamasque_python-1.4.0}/datamasque/client/models/data_selection.py
RENAMED
|
File without changes
|
{datamasque_python-1.3.0 → 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.0 → datamasque_python-1.4.0}/datamasque/client/models/safe_data_preview.py
RENAMED
|
File without changes
|
|
File without changes
|
{datamasque_python-1.3.0 → 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
|
{datamasque_python-1.3.0 → 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
|