From 8594947e737bf15a4361ecc9f0ef85fb6d529a88 Mon Sep 17 00:00:00 2001 From: Elliot Simpson Date: Wed, 9 Sep 2026 13:15:31 +1200 Subject: [PATCH 1/2] feat: model max_length_unit on SchemaDiscoveryColumn The schema-discovery API now returns a max_length_unit field per column (server 3.26.17), which the drift guard in the dm-python live tests flagged as unmodelled. Add a LengthUnit enum (chars/bytes) and the optional field, export it, bump to 1.2.6, and record the change. --- HISTORY.rst | 9 +++++++++ datamasque/client/__init__.py | 2 ++ datamasque/client/models/discovery.py | 9 +++++++++ pyproject.toml | 2 +- tests/test_discovery.py | 17 +++++++++++++++++ uv.lock | 2 +- 6 files changed, 39 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 19749dd..b443c97 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,15 @@ History ======= +1.3.1 (2026-09-09) +------------------ + +* Added ``max_length_unit`` to ``SchemaDiscoveryColumn``, typed by the new ``LengthUnit`` enum + (``chars`` or ``bytes``), reporting the unit a column's ``max_length`` is counted in. + It is ``None`` for results from older servers that do not report a unit. + +Requires server version 3.26.17 + 1.3.0 (2026-09-04) ------------------ diff --git a/datamasque/client/__init__.py b/datamasque/client/__init__.py index cd9fea7..ea8e0c9 100644 --- a/datamasque/client/__init__.py +++ b/datamasque/client/__init__.py @@ -73,6 +73,7 @@ ForeignKeyRef, InDataDiscoveryConfig, InDataDiscoveryRule, + LengthUnit, ReferencingForeignKey, RulesetGenerationRequest, RulesetGenerationWithRGConfigRequest, @@ -239,6 +240,7 @@ "InvalidRulesetError", "JsonPath", "LengthEntry", + "LengthUnit", "LengthsStatistics", "LicenseInfo", "Locator", diff --git a/datamasque/client/models/discovery.py b/datamasque/client/models/discovery.py index 3e5f1fb..bacbc8d 100644 --- a/datamasque/client/models/discovery.py +++ b/datamasque/client/models/discovery.py @@ -367,6 +367,13 @@ class ReferencingForeignKey(BaseModel): referencing_column: str # Dotted path: "schema.table.column". +class LengthUnit(Enum): + """The unit a column's declared width (`max_length`) is counted in.""" + + chars = "chars" + bytes = "bytes" + + class SchemaDiscoveryColumn(BaseModel): """Column-level data in a schema discovery result.""" @@ -374,6 +381,8 @@ class SchemaDiscoveryColumn(BaseModel): data_type: Optional[str] = None max_length: Optional[int] = None + # None when the server does not report a unit (older runs count chars). + max_length_unit: Optional[LengthUnit] = None foreign_keys: list[ForeignKeyRef] discovery_matches: list[DiscoveryMatch] numeric_precision: Optional[int] = None diff --git a/pyproject.toml b/pyproject.toml index 6a68cd1..c907a34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "datamasque-python" -version = "1.3.0" +version = "1.3.1" description = "Official Python client for the DataMasque data-masking API." authors = [ { name = "DataMasque Ltd" }, diff --git a/tests/test_discovery.py b/tests/test_discovery.py index f73c902..83f9348 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -21,6 +21,7 @@ FileRulesetGenerationRequest, FileRulesetGenerationWithRGConfigRequest, InDataDiscoveryConfig, + LengthUnit, RGConfig, RGConfigId, RulesetGenerationRequest, @@ -795,6 +796,22 @@ def test_schema_discovery_result_parses_safe_data_preview(): assert preview.statistics_common.count_distinct == 988 +def test_schema_discovery_result_parses_max_length_unit() -> None: + row = _schema_discovery_row(1, "name") + row["data"]["max_length"] = 10 + row["data"]["max_length_unit"] = "bytes" + result = SchemaDiscoveryResult.model_validate(row) + assert result.data.max_length == 10 + assert result.data.max_length_unit is LengthUnit.bytes + + +def test_schema_discovery_result_without_max_length_unit_is_none() -> None: + row = _schema_discovery_row(1, "name") + assert "max_length_unit" not in row["data"] + result = SchemaDiscoveryResult.model_validate(row) + assert result.data.max_length_unit is None + + def test_list_schema_discovery_results_follows_pagination(client): run_id = RunId(42) page1 = { diff --git a/uv.lock b/uv.lock index 0d45760..c53fc85 100644 --- a/uv.lock +++ b/uv.lock @@ -419,7 +419,7 @@ toml = [ [[package]] name = "datamasque-python" -version = "1.3.0" +version = "1.3.1" source = { editable = "." } dependencies = [ { name = "pydantic" }, From 1f3c72bbefa9d064ba20f5c2146fc5cedb8859f3 Mon Sep 17 00:00:00 2001 From: Elliot Simpson Date: Wed, 9 Sep 2026 13:15:39 +1200 Subject: [PATCH 2/2] ci: auto-create the GitHub Release linking HISTORY.rst Add a github-release job to release.yml that, after the PyPI publish succeeds, creates the GitHub Release for the pushed tag, attaches the built distributions, and links to HISTORY.rst at that tag for the changelog (rather than autogenerated git-diff notes). Drop the now-obsolete manual GitHub Release steps from RELEASING.rst. --- .github/workflows/release.yml | 24 ++++++++++++++++++++++++ RELEASING.rst | 29 ++++------------------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4dc2ecb..5a2a1b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,3 +60,27 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + + github-release: + name: Publish GitHub Release + needs: publish + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download distributions + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${GITHUB_REF_NAME}" \ + --repo "${GITHUB_REPOSITORY}" \ + --title "${GITHUB_REF_NAME}" \ + --verify-tag \ + --notes "See [HISTORY.rst](https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_REF_NAME}/HISTORY.rst) for changelog." \ + dist/* diff --git a/RELEASING.rst b/RELEASING.rst index a2b40cb..473b3e6 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -68,31 +68,10 @@ Releasing to PyPI You can verify that the release appears at https://pypi.org/project/datamasque-python/, and Read the Docs has built the new tag at https://datamasque-python.readthedocs.io/. -5. Create the GitHub Release (see below). - -Creating the GitHub Release -=========================== - -Do this after the PyPI publish has succeeded, -so the release never points at a version nobody can install. - -1. On the repository home page, - find **Releases** in the right-hand sidebar under the *About* box, - and click it, - then **Draft a new release**. -2. **Choose a tag** — pick the existing tag you pushed as part of releasing to PyPI. - Don't let GitHub create a new one; the tag should already exist from the release flow. -3. Set the target to ``main``. -4. Title it the same as the tag, e.g. ``v1.2.2``. -5. Paste the ``HISTORY.rst`` entry for this version into the body, converted to Markdown. - **Generate release notes** is a reasonable starting point for the commit list, - but the hand-written changelog is what users read. -6. Leave *Set as a pre-release* unticked for a normal release. -7. Click **Publish release**. - -Creating releases can also be done from the ``gh`` CLI:: - - gh release create v1.2.2 --title v1.2.2 --notes-file notes.md +5. The GitHub Release is created automatically once the PyPI publish succeeds. + ``release.yml`` runs a ``github-release`` job that creates the release for the pushed tag, + attaches the built distributions, and links to ``HISTORY.rst`` at that tag for the changelog. + Nothing to do by hand; just confirm it appears under **Releases** on the repository home page. Releasing a dev build to TestPyPI =================================