Skip to content
Merged
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
23 changes: 23 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@
History
=======

1.4.0 (2026-09-25)
------------------

* Added ``LicenseLock`` (``locked_at`` and ``editable_from``) and a read-only ``license_lock`` field on connection configs.
The server reports it only on instances whose license caps the number of connections
and counts that connection's type against the cap,
once a connection's first run has finished successfully;
otherwise it is ``None``.
It is excluded when a config is serialized back to the API,
as it must never be sent on create or update.

* Added ``unlicensed_feature_warnings`` to ``Ruleset`` and ``RulesetLibrary``: at most one sentence naming the features
each uses that the server's license does not allow.

* Added read-only ``target_frozen``, ``frozen_target_fields`` and ``deletion_frozen`` fields on connection configs.
``target_frozen`` is ``True`` while the connection's protected fields are locked under the license edit lock,
``frozen_target_fields`` lists those locked fields as server config keys, such as ``schema`` (empty when not frozen),
and ``deletion_frozen`` is whether deleting the connection is currently blocked
(it can differ from ``target_frozen``: deletion stays allowed while over the connection cap).
All three are excluded when a config is serialized back to the API.

Requires server version 3.26.18

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

Expand Down
2 changes: 2 additions & 0 deletions datamasque/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
DocumentDbConnectionConfig,
DynamoConnectionConfig,
FileConnectionConfig,
LicenseLock,
MongoConnectionConfig,
MountedShareConnectionConfig,
MssqlLinkedServerConnectionConfig,
Expand Down Expand Up @@ -243,6 +244,7 @@
"LengthUnit",
"LengthsStatistics",
"LicenseInfo",
"LicenseLock",
"Locator",
"MaskType",
"MaskedFormEntry",
Expand Down
27 changes: 27 additions & 0 deletions datamasque/client/models/connection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Connection configuration models for the DataMasque API."""

from datetime import datetime
from enum import Enum
from typing import Any, Callable, Literal, NewType, Optional

Expand Down Expand Up @@ -99,6 +100,22 @@ def _validate_kms_key(self) -> "SseConfig":
return self


class LicenseLock(BaseModel):
"""
Read-only license-lock metadata for a connection.

The server reports this only on instances whose license has a finite connection cap
and counts this connection's type against that cap,
once the connection's first run has finished successfully;
otherwise it is `null`.
"""

model_config = ConfigDict(extra="allow")

locked_at: datetime
editable_from: datetime


class ConnectionConfig(BaseModel):
"""
Base class for all connection configurations.
Expand All @@ -111,6 +128,16 @@ class ConnectionConfig(BaseModel):

name: str
id: Optional[ConnectionId] = None
# Server-populated and read-only, so it is excluded when serializing a config back to the API.
license_lock: Optional[LicenseLock] = Field(default=None, exclude=True)
# Whether the connection's protected fields are currently locked under the license edit lock.
target_frozen: bool = Field(default=False, exclude=True)
# The protected fields currently locked, as server config keys (e.g. `schema`, not `db_schema`);
# empty when `target_frozen` is False.
frozen_target_fields: list[str] = Field(default_factory=list, exclude=True)
# Whether deleting the connection is currently blocked. Not always equal to `target_frozen`:
# deletion stays allowed while over the connection cap, so a downgrade can be recovered from.
deletion_frozen: bool = Field(default=False, exclude=True)


class DynamoConnectionConfig(ConnectionConfig):
Expand Down
7 changes: 7 additions & 0 deletions datamasque/client/models/ruleset.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,10 @@ 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)
"""
At most one sentence, naming every feature the ruleset uses that the server's license does not allow.
Empty when the ruleset needs nothing its license withholds,
and `None` when the server does not know which features the ruleset uses, for example before it has passed
validation. `None` is not the same as having no warnings.
"""
7 changes: 7 additions & 0 deletions datamasque/client/models/ruleset_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,12 @@ class RulesetLibrary(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)
"""
At most one sentence, naming every feature the library uses that the server's license does not allow.
Empty when the license allows every feature,
and `None` when the server does not know which features the library uses, for example before it has passed
validation.
"""
created: Optional[datetime] = Field(default=None, exclude=True)
modified: Optional[datetime] = Field(default=None, exclude=True)
5 changes: 4 additions & 1 deletion datamasque/client/ruleset_libraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ def create_ruleset_library(self, library: RulesetLibrary) -> RulesetLibrary:
Creates a new ruleset library on the server.

Sets the library's server-assigned fields
(`id`, `is_valid`, `validation_errors`, `git`, `created`, `modified`) and returns the library.
(`id`, `is_valid`, `validation_errors`, `unlicensed_feature_warnings`, `git`, `created`, `modified`)
and returns the library.
"""

data = library.model_dump(exclude_none=True, by_alias=True, mode="json")
Expand All @@ -73,6 +74,7 @@ def create_ruleset_library(self, library: RulesetLibrary) -> RulesetLibrary:
library.id = created_library.id
library.is_valid = created_library.is_valid
library.validation_errors = created_library.validation_errors
library.unlicensed_feature_warnings = created_library.unlicensed_feature_warnings
library.git = created_library.git
library.created = created_library.created
library.modified = created_library.modified
Expand All @@ -94,6 +96,7 @@ def update_ruleset_library(self, library: RulesetLibrary) -> RulesetLibrary:
updated_library = RulesetLibrary.model_validate(response.json())
library.is_valid = updated_library.is_valid
library.validation_errors = updated_library.validation_errors
library.unlicensed_feature_warnings = updated_library.unlicensed_feature_warnings
library.git = updated_library.git
library.modified = updated_library.modified
logger.debug('Update of ruleset library "%s" successful', library.name)
Expand Down
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.4.0"
description = "Official Python client for the DataMasque data-masking API."
authors = [
{ name = "DataMasque Ltd" },
Expand Down
Loading
Loading