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
1 change: 1 addition & 0 deletions packages/dsw-tdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Update to DT metamodel 18.3
- Metamodel version handling is shared with `dsw-models` (now a dependency)
- The `template.json` written into a package is built and validated as the shared `DocumentTemplateBundle` from `dsw-models`, so the package shape has one definition; a template that does not match it is reported as a warning and still packaged, as before

### Fixed

Expand Down
41 changes: 36 additions & 5 deletions packages/dsw-tdk/dsw/tdk/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
import typing
import zipfile

import pydantic
import watchfiles

from dsw.models.document_template.metadata import DocumentTemplateBundle
from dsw.models.errors import MetamodelVersionError
from dsw.models.strictness import load as load_model
from dsw.models.versions import MetamodelVersion

from . import consts
Expand Down Expand Up @@ -361,6 +364,24 @@ def create_pot_file(self, output: pathlib.Path, force: bool) -> PotFile:
output.write_bytes(pot_file.data)
return pot_file

def _package_descriptor(self, descriptor: dict) -> dict:
"""Normalize the package descriptor through the shared DocumentTemplateBundle.

`package` does not run TemplateValidator, so it has always accepted a template.json
that `verify` rejects. Validation failures are therefore reported but not fatal: the
descriptor is written as assembled, exactly as before.
"""
try:
return load_model(DocumentTemplateBundle, descriptor).to_json_data()
except pydantic.ValidationError as e:
problems = ', '.join(
f'{".".join(str(part) for part in error["loc"])}: {error["msg"]}'
for error in e.errors()
)
self.logger.warning('Template does not match the document template bundle (%s) - '
'packaging it anyway, run "dsw-tdk verify" for details', problems)
return descriptor

def create_package(self, output: pathlib.Path, force: bool):
if output.exists() and not force:
raise RuntimeError(f'File {output} already exists (not forced)')
Expand Down Expand Up @@ -389,17 +410,27 @@ def create_package(self, output: pathlib.Path, force: bool):
file.filename.as_posix())
pkg.writestr(f'template/assets/{file.filename.as_posix()}',
file.content)
descriptor['files'] = files
descriptor['assets'] = assets
if len(files) == 0 and len(assets) == 0:
self.logger.warning('No files or assets found in the template, maybe you forgot '
'to update _tdk.files patterns in template.json?')
timestamp = datetime.datetime.now(tz=datetime.UTC).strftime('%Y-%m-%dT%H:%M:%S.%fZ')
descriptor['createdAt'] = timestamp
descriptor['updatedAt'] = timestamp
# The package descriptor is the shared DocumentTemplateBundle: building it through
# dsw-models validates what we ship and keeps the shape in one place. The local
# template.json stays the TDK's own lenient model, as `verify` reports on it.
metamodel_version = descriptor['metamodelVersion']
descriptor.update({
# the local descriptor accepts an int (see TemplateValidator), the bundle does not;
# anything else is left as it is so an invalid value is reported, not rewritten
'metamodelVersion': (str(metamodel_version)
if isinstance(metamodel_version, int) else metamodel_version),
'files': files,
'assets': assets,
'createdAt': timestamp,
'updatedAt': timestamp,
})
self.logger.debug('Packaging template.json file')
pkg.writestr('template/template.json',
data=json.dumps(descriptor, indent=4))
data=json.dumps(self._package_descriptor(descriptor), indent=4))
self.logger.debug('ZIP packaging done')

def extract_package(self, zip_data: bytes, template_dir: pathlib.Path | None, force: bool):
Expand Down
2 changes: 1 addition & 1 deletion packages/dsw-tdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ build-backend = "hatchling.build"
source = "uv-dynamic-versioning"

[tool.hatch.metadata.hooks.uv-dynamic-versioning]
dependencies = ["aiohttp", "Babel", "click", "colorama", "humanize", "Jinja2", "multidict", "pathspec", "python-dotenv", "python-slugify", "watchfiles", "dsw-models=={{ version }}"]
dependencies = ["aiohttp", "Babel", "click", "colorama", "humanize", "Jinja2", "multidict", "pathspec", "pydantic", "python-dotenv", "python-slugify", "watchfiles", "dsw-models=={{ version }}"]

[tool.hatch.build.targets.wheel]
packages = ["dsw"]
Expand Down
45 changes: 45 additions & 0 deletions packages/dsw-tdk/tests/test_cmd_package.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import json
import pathlib
import zipfile

import click.testing

from dsw.models.document_template.metadata import DocumentTemplateBundle
from dsw.models.strictness import load
from dsw.tdk import main


Expand Down Expand Up @@ -55,3 +59,44 @@ def test_package_faulty01(fixtures_path: pathlib.Path, tmp_path: pathlib.Path):
result = runner.invoke(main, args=['package', template_path.as_posix(), '-o', 'my-template.zip'])
assert result.exit_code == 1
assert not zip_file.exists()


def _packaged_descriptor(fixtures_path: pathlib.Path, tmp_path: pathlib.Path,
fixture: str) -> tuple[int, str, dict]:
runner = click.testing.CliRunner()
template_path = fixtures_path / fixture
with runner.isolated_filesystem(temp_dir=tmp_path) as isolated_dir:
zip_file = pathlib.Path(isolated_dir) / 'my-template.zip'
result = runner.invoke(main, args=['package', template_path.as_posix(),
'-o', 'my-template.zip'])
if not zip_file.exists():
return result.exit_code, result.output, {}
with zipfile.ZipFile(zip_file) as pkg:
descriptor = json.loads(pkg.read('template/template.json'))
return result.exit_code, result.output, descriptor


def test_package_matches_document_template_bundle(fixtures_path, tmp_path):
"""What `package` writes is exactly the bundle shape shared via dsw-models."""
exit_code, _, descriptor = _packaged_descriptor(fixtures_path, tmp_path, 'test_example01')
assert exit_code == 0
bundle = load(DocumentTemplateBundle, descriptor)
assert bundle.coordinate == descriptor['id']
assert bundle.metamodel_version == '17.1'
assert [f.file_name for f in bundle.files] == [f['fileName'] for f in descriptor['files']]


def test_package_keeps_allowed_package_options(fixtures_path, tmp_path):
"""`options` is optional on a package pattern: present when set, absent otherwise."""
_, _, descriptor = _packaged_descriptor(fixtures_path, tmp_path, 'test_example01')
for pattern in descriptor['allowedPackages']:
assert set(pattern) >= {'orgId', 'kmId', 'minVersion', 'maxVersion'}
assert 'options' not in pattern or isinstance(pattern['options'], dict)


def test_package_of_invalid_template_still_succeeds(fixtures_path, tmp_path):
"""`package` never ran TemplateValidator, so bundle mismatches warn rather than fail."""
exit_code, output, descriptor = _packaged_descriptor(fixtures_path, tmp_path, 'test_faulty03')
assert exit_code == 0
assert 'does not match the document template bundle' in output
assert descriptor['name'] is None # written as assembled, exactly as before
2 changes: 2 additions & 0 deletions uv.lock

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

Loading