Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ Pull requests
5. Open a PR against ``main`` and describe what the change does and why.
6. The maintainers will review and either merge, request changes, or close with an explanation.

Changelog
=========

Create a new version heading in ``HISTORY.rst``,
and add one concise bullet point for each high-level change.

Commit messages
===============

Expand Down
8 changes: 8 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
History
=======

1.3.2 (2026-09-17)
------------------

* Added ``unlicensed_feature_warnings`` to ``Ruleset``, naming the features the ruleset uses
that the server's license does not allow.

Requires server version 3.26.18

1.3.1 (2026-09-09)
------------------

Expand Down
6 changes: 6 additions & 0 deletions datamasque/client/models/ruleset.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,9 @@ class Ruleset(GitTrackedEntity):
is_valid: Optional[ValidationStatus] = Field(default=None, exclude=True)
validation_errors: list[ValidationErrorDetails] = Field(default_factory=list, exclude=True)
"""Validation errors surfaced by the server; empty when valid."""
unlicensed_feature_warnings: Optional[list[str]] = Field(default=None, exclude=True)
"""
One sentence per feature the ruleset uses that the server's license does not allow.
Empty when the ruleset needs nothing its license withholds,
and `None` until the ruleset has passed validation, which is not the same as having no warnings.
"""
3 changes: 2 additions & 1 deletion datamasque/client/rulesets.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def create_or_update_ruleset(self, ruleset: Ruleset) -> Ruleset:
"""
Creates or updates a ruleset.

Populates the given ruleset's `id`, `is_valid`, `validation_errors`,
Populates the given ruleset's `id`, `is_valid`, `validation_errors`, `unlicensed_feature_warnings`,
and `git` fields from the server response, and returns the same ruleset instance for convenience.
"""

Expand All @@ -30,6 +30,7 @@ def create_or_update_ruleset(self, ruleset: Ruleset) -> Ruleset:
ruleset.id = created.id
ruleset.is_valid = created.is_valid
ruleset.validation_errors = created.validation_errors
ruleset.unlicensed_feature_warnings = created.unlicensed_feature_warnings
ruleset.git = created.git

if response.status_code == 201:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "datamasque-python"
version = "1.3.1"
version = "1.3.2"
description = "Official Python client for the DataMasque data-masking API."
authors = [
{ name = "DataMasque Ltd" },
Expand Down
85 changes: 84 additions & 1 deletion tests/test_rulesets.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,86 @@ def test_create_or_update_ruleset_validation_errors_empty_when_valid(client, rul
assert result.validation_errors == []


UNSTRUCTURED_WARNING = "Foundation license does not allow Unstructured Masking. Upgrade to Enterprise."
SUBSETTING_WARNING = "Foundation license does not allow Subsetting. Upgrade to Enterprise."


@pytest.mark.parametrize(
("returned", "expected"),
[
([UNSTRUCTURED_WARNING, SUBSETTING_WARNING], [UNSTRUCTURED_WARNING, SUBSETTING_WARNING]),
([], []),
(None, None),
],
ids=["warned", "fully_licensed", "not_yet_known"],
)
def test_create_or_update_ruleset_populates_unlicensed_feature_warnings(client, ruleset, returned, expected):
"""Every warning the server sends is carried, and an empty list stays distinct from `None`."""
with requests_mock.Mocker() as m:
m.post(
"http://test-server/api/rulesets/?upsert=true",
json={"id": "2", "name": "test_ruleset", "is_valid": "valid", "unlicensed_feature_warnings": returned},
status_code=201,
)
result = client.create_or_update_ruleset(ruleset)

assert result.unlicensed_feature_warnings == expected


def test_create_or_update_ruleset_unlicensed_feature_warnings_default_to_none(client, ruleset):
"""A server that omits the field leaves the warnings unknown rather than empty."""
with requests_mock.Mocker() as m:
m.post(
"http://test-server/api/rulesets/?upsert=true",
json={"id": "2", "name": "test_ruleset", "is_valid": "in_progress"},
status_code=201,
)
result = client.create_or_update_ruleset(ruleset)

assert result.unlicensed_feature_warnings is None


def test_list_rulesets_unlicensed_feature_warnings(client):
"""Each listed ruleset carries its own `unlicensed_feature_warnings`, including `None` for an unvalidated one."""
with requests_mock.Mocker() as m:
m.get(
"http://test-server/api/v2/rulesets/",
json=[
{
"id": "1",
"name": "unstructured_ruleset",
"mask_type": "database",
"is_valid": "valid",
"unlicensed_feature_warnings": [
"Foundation license does not allow Unstructured Masking. Upgrade to Enterprise."
],
},
{
"id": "2",
"name": "licensed_ruleset",
"mask_type": "database",
"is_valid": "valid",
"unlicensed_feature_warnings": [],
},
{
"id": "3",
"name": "unvalidated_ruleset",
"mask_type": "database",
"is_valid": "in_progress",
"unlicensed_feature_warnings": None,
},
],
status_code=200,
)
rulesets = client.list_rulesets()

assert rulesets[0].unlicensed_feature_warnings == [
"Foundation license does not allow Unstructured Masking. Upgrade to Enterprise."
]
assert rulesets[1].unlicensed_feature_warnings == []
assert rulesets[2].unlicensed_feature_warnings is None


def test_create_or_update_ruleset_does_not_send_read_only_fields(client, ruleset):
"""Read-only server fields must never be echoed back into a re-submit's request body."""
with requests_mock.Mocker() as m:
Expand All @@ -187,6 +267,9 @@ def test_create_or_update_ruleset_does_not_send_read_only_fields(client, ruleset
"name": "test_ruleset",
"is_valid": "invalid",
"validation_errors": [{"message": "bad", "validation_error_type": "ruleset"}],
"unlicensed_feature_warnings": [
"Foundation license does not allow Unstructured Masking. Upgrade to Enterprise."
],
},
status_code=201,
)
Expand All @@ -196,7 +279,7 @@ def test_create_or_update_ruleset_does_not_send_read_only_fields(client, ruleset
client.create_or_update_ruleset(ruleset)

body = m.last_request.json()
for read_only_field in ("id", "is_valid", "validation_errors"):
for read_only_field in ("id", "is_valid", "validation_errors", "unlicensed_feature_warnings"):
assert read_only_field not in body
# Input fields are still present.
assert body["config_yaml"] == "version: '1.0'\ntasks: []"
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading