linkml-xsd 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,19 @@
1
+ """linkml-to-xsd: convert LinkML schemas to XSD (and example XML)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .config import ConversionOptions
6
+ from .emit import XSDGenerator
7
+ from .frontend import SchemaFrontend
8
+ from .schematron import SchematronGenerator
9
+ from .xml_generator import XMLExampleGenerator
10
+ from .xsdgen import XsdGenerator
11
+
12
+ __all__ = [
13
+ "ConversionOptions",
14
+ "SchemaFrontend",
15
+ "SchematronGenerator",
16
+ "XMLExampleGenerator",
17
+ "XSDGenerator",
18
+ "XsdGenerator",
19
+ ]
linkml_to_xsd/cli.py ADDED
@@ -0,0 +1,81 @@
1
+ """Command-line interface for linkml-to-xsd."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from lxml import etree
9
+
10
+ from linkml_to_xsd.xml_generator import XMLExampleGenerator
11
+ from linkml_to_xsd.xsdgen import cli as gen_xsd_cli
12
+
13
+
14
+ @click.group()
15
+ @click.version_option(package_name="linkml-xsd")
16
+ def cli() -> None:
17
+ """Convert LinkML schemas to XSD and generate example XML."""
18
+
19
+
20
+ # `linkml2xsd convert` is the same command as the standalone `gen-xsd` script —
21
+ # one implementation, one option surface (avoids the two drifting apart).
22
+ cli.add_command(gen_xsd_cli, name="convert")
23
+
24
+
25
+ @cli.command()
26
+ @click.argument("schema", type=click.Path(exists=True, path_type=Path))
27
+ @click.option(
28
+ "--output",
29
+ "-o",
30
+ type=click.Path(path_type=Path),
31
+ help="Output XML file. If omitted, prints to stdout.",
32
+ )
33
+ @click.option(
34
+ "--root-class",
35
+ default=None,
36
+ help="Root class to use for the example. Defaults to a leaf class.",
37
+ )
38
+ @click.option(
39
+ "--target-namespace",
40
+ default=None,
41
+ help="Namespace for the XML. Default: the same as `convert` derives, so the "
42
+ "example validates against the generated XSD.",
43
+ )
44
+ def example(
45
+ schema: Path, output: Path | None, root_class: str | None, target_namespace: str | None
46
+ ) -> None:
47
+ """Generate an example XML instance from a LinkML schema."""
48
+ # The generator derives the namespace exactly like `convert`, so the
49
+ # example validates against the generated XSD out of the box.
50
+ generator = XMLExampleGenerator(schema_path=schema, target_namespace=target_namespace)
51
+ xml_text = generator.generate(root_class=root_class)
52
+
53
+ if output:
54
+ output.write_text(xml_text, encoding="utf-8")
55
+ click.echo(f"Wrote example XML to {output}")
56
+ else:
57
+ click.echo(xml_text)
58
+
59
+
60
+ @cli.command()
61
+ @click.argument("xml", type=click.Path(exists=True, path_type=Path))
62
+ @click.option(
63
+ "--xsd",
64
+ required=True,
65
+ type=click.Path(exists=True, path_type=Path),
66
+ help="XSD file to validate against.",
67
+ )
68
+ def validate(xml: Path, xsd: Path) -> None:
69
+ """Validate an XML file against an XSD."""
70
+ with xsd.open("rb") as xsd_fh:
71
+ xmlschema_doc = etree.parse(xsd_fh)
72
+ xmlschema = etree.XMLSchema(xmlschema_doc)
73
+
74
+ with xml.open("rb") as xml_fh:
75
+ xml_doc = etree.parse(xml_fh)
76
+
77
+ try:
78
+ xmlschema.assertValid(xml_doc)
79
+ click.echo(f"{xml} is valid against {xsd}.")
80
+ except etree.DocumentInvalid as exc:
81
+ raise click.ClickException(f"Validation failed: {exc}") from exc
@@ -0,0 +1,123 @@
1
+ """Conversion options — one toggle per LinkML→XSD mapping decision.
2
+
3
+ Every place where the mapping from LinkML to XSD is genuinely a *choice* (inline
4
+ vs reference a class, XSD 1.0 + Schematron vs XSD 1.1 assertions, how to render
5
+ identity constraints, …) is exposed here rather than hard-coded, so callers can
6
+ pick a behaviour without forking the emitters. Defaults encode the project's
7
+ recommended mapping; the CLI and the public API both accept a
8
+ ``ConversionOptions`` instance.
9
+
10
+ The dataclass is frozen (options are a value, passed around, never mutated) and
11
+ validates its ``Literal`` fields at construction so a bad CLI string fails fast
12
+ with a clear message rather than deep inside an emitter.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from typing import Literal, get_args, get_origin, get_type_hints
19
+
20
+ #: How a slot whose range is a class is rendered in XSD.
21
+ #: ``linkml`` honours LinkML's own inlining inference (a non-inlined slot to a
22
+ #: class with an identifier becomes a *reference*); the other two force it.
23
+ InliningPolicy = Literal["linkml", "always_inline", "always_reference"]
24
+
25
+ #: How a *reference* (non-inlined class range) is expressed in XSD.
26
+ #: ``identifier`` types the referencing element as the referenced class's
27
+ #: identifier value (a soft reference); ``uri`` types it ``xs:anyURI``.
28
+ #: ``key`` / ``idref`` (full XSD identity constraints) are the design target and
29
+ #: currently fall back to the ``identifier`` rendering.
30
+ ReferenceStyle = Literal["identifier", "uri", "key", "idref"]
31
+
32
+ #: XSD flavour of the primary schema output. ``1.1`` unlocks ``xs:assert`` /
33
+ #: ``xs:alternative`` for constraints XSD 1.0 cannot express; ``1.0`` routes
34
+ #: those to Schematron instead (see ``emit_schematron``).
35
+ XSDVersion = Literal["1.0", "1.1"]
36
+
37
+ #: How a ``designates_type`` discriminator drives polymorphic typing.
38
+ #: ``base`` (default, XSD 1.0): the element uses the base class type, the
39
+ #: designator is a plain field. ``alternative``: emit ``xs:alternative``
40
+ #: conditional type assignment picking the concrete subtype by the designator
41
+ #: value (XSD 1.1 construct).
42
+ TypeDesignatorStrategy = Literal["base", "alternative"]
43
+
44
+ #: How an ``ArrayExpression`` slot is rendered. ``nested`` (default): one wrapper
45
+ #: element per dimension (named by the dimension alias), with occurrence bounds
46
+ #: from the dimension cardinalities. ``flat``: a single unbounded element — the
47
+ #: shape is not enforced (also the automatic fallback for any-shape arrays,
48
+ #: whose dimension count XSD cannot express).
49
+ ArrayStrategy = Literal["nested", "flat"]
50
+
51
+ #: Content model for a class's element children.
52
+ ContentModel = Literal["sequence", "all"]
53
+
54
+ #: XSD ``elementFormDefault`` / ``attributeFormDefault``.
55
+ FormDefault = Literal["qualified", "unqualified"]
56
+
57
+ #: What to do when a LinkML construct has no representation under the current
58
+ #: options: silently skip, record a diagnostic, or raise.
59
+ UnsupportedPolicy = Literal["ignore", "warn", "error"]
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ConversionOptions:
64
+ """Immutable bundle of every LinkML→XSD mapping choice."""
65
+
66
+ # -- output targets ----------------------------------------------------
67
+ xsd_version: XSDVersion = "1.0"
68
+ #: Emit a companion Schematron (.sch) for constraints XSD (of the chosen
69
+ #: version) cannot express — boolean slot expressions, cross-field rules, …
70
+ emit_schematron: bool = False
71
+
72
+ # -- structural mapping of class-range slots ---------------------------
73
+ inlining: InliningPolicy = "linkml"
74
+ reference_style: ReferenceStyle = "identifier"
75
+ type_designator: TypeDesignatorStrategy = "base"
76
+ array_strategy: ArrayStrategy = "nested"
77
+ content_model: ContentModel = "sequence"
78
+
79
+ # -- constraints -------------------------------------------------------
80
+ #: ``minimum_value`` / ``maximum_value`` / ``pattern`` → XSD facets.
81
+ emit_value_constraints: bool = True
82
+ #: ``identifier`` / ``key`` / ``unique_keys`` → ``xs:key`` / ``xs:unique``.
83
+ emit_identity_constraints: bool = True
84
+ #: ``equals_expression`` / ``string_serialization`` → consistency checks
85
+ #: (Schematron asserts, and ``xs:assert`` in the 1.1 mode) for the
86
+ #: compilable subset of the expression language.
87
+ emit_computed_checks: bool = True
88
+
89
+ # -- metadata ----------------------------------------------------------
90
+ #: ``description`` / ``title`` / comments → ``xs:annotation/xs:documentation``.
91
+ emit_documentation: bool = True
92
+ #: ``meaning`` / ``*_uri`` / ``annotations`` → ``xs:annotation/xs:appinfo``.
93
+ emit_appinfo: bool = False
94
+ #: Recognise the ``[en] … [de] …`` multilingual-description workaround (no
95
+ #: native per-language string in LinkML) and split it into one
96
+ #: ``xs:documentation`` per language, tagged with ``xml:lang``, instead of
97
+ #: emitting the raw ``[xx]``-tagged text as a single blob.
98
+ multilingual_documentation: bool = False
99
+
100
+ # -- namespaces --------------------------------------------------------
101
+ #: Explicit override; otherwise derived from ``default_prefix`` / schema id.
102
+ target_namespace: str | None = None
103
+ element_form_default: FormDefault = "qualified"
104
+ attribute_form_default: FormDefault = "unqualified"
105
+
106
+ # -- profiles ----------------------------------------------------------
107
+ #: Opinionated annotation vocabulary to honour, e.g. ``"akn"`` for the
108
+ #: eCH-0296 / Akoma Ntoso ``xml_element`` / ``xsd_content_model`` set.
109
+ profile: str | None = None
110
+
111
+ # -- diagnostics -------------------------------------------------------
112
+ on_unsupported: UnsupportedPolicy = "warn"
113
+
114
+ def __post_init__(self) -> None:
115
+ # Validate every Literal-typed field against its allowed values so a bad
116
+ # CLI/API string fails here, not deep inside an emitter. get_type_hints
117
+ # resolves the string annotations (PEP 563) and the module-level aliases.
118
+ for name, hint in get_type_hints(type(self)).items():
119
+ if get_origin(hint) is Literal:
120
+ value = getattr(self, name)
121
+ allowed = get_args(hint)
122
+ if value not in allowed:
123
+ raise ValueError(f"ConversionOptions.{name}={value!r} is not one of {allowed}")
@@ -0,0 +1,20 @@
1
+ """Unsupported-construct reporting, honouring ``ConversionOptions.on_unsupported``.
2
+
3
+ A construct the converters cannot (or cannot yet) express must never vanish
4
+ silently — a user would believe it is being validated when it is not. Every
5
+ emitter routes such cases through :func:`report_unsupported`: ``warn`` (default)
6
+ raises a ``UserWarning``, ``error`` aborts the conversion, ``ignore`` stays
7
+ silent (an explicit opt-in to silence).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import warnings
13
+
14
+
15
+ def report_unsupported(policy: str, message: str) -> None:
16
+ """Handle a construct with no representation under the current options."""
17
+ if policy == "error":
18
+ raise ValueError(message)
19
+ if policy == "warn":
20
+ warnings.warn(message, stacklevel=3)
@@ -0,0 +1,11 @@
1
+ """Generic LinkML→XSD emit engine (SchemaView-backed).
2
+
3
+ ``XSDGenerator`` is the entry point; the mapping details live in ``generator``,
4
+ ``typemap`` and ``naming``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .generator import XSDGenerator
10
+
11
+ __all__ = ["XSDGenerator"]