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.
- linkml_to_xsd/__init__.py +19 -0
- linkml_to_xsd/cli.py +81 -0
- linkml_to_xsd/config.py +123 -0
- linkml_to_xsd/diagnostics.py +20 -0
- linkml_to_xsd/emit/__init__.py +11 -0
- linkml_to_xsd/emit/generator.py +739 -0
- linkml_to_xsd/emit/typemap.py +80 -0
- linkml_to_xsd/expressions.py +133 -0
- linkml_to_xsd/frontend/__init__.py +14 -0
- linkml_to_xsd/frontend/schema_frontend.py +124 -0
- linkml_to_xsd/i18n.py +44 -0
- linkml_to_xsd/naming.py +25 -0
- linkml_to_xsd/profile/__init__.py +23 -0
- linkml_to_xsd/profile/akn.py +258 -0
- linkml_to_xsd/profile/base.py +117 -0
- linkml_to_xsd/py.typed +0 -0
- linkml_to_xsd/rules.py +85 -0
- linkml_to_xsd/schematron/__init__.py +13 -0
- linkml_to_xsd/schematron/generator.py +147 -0
- linkml_to_xsd/schematron/spec.py +30 -0
- linkml_to_xsd/xml_generator.py +133 -0
- linkml_to_xsd/xsdgen.py +186 -0
- linkml_xsd-0.2.0.dist-info/METADATA +215 -0
- linkml_xsd-0.2.0.dist-info/RECORD +30 -0
- linkml_xsd-0.2.0.dist-info/WHEEL +5 -0
- linkml_xsd-0.2.0.dist-info/entry_points.txt +3 -0
- linkml_xsd-0.2.0.dist-info/licenses/LICENSE +21 -0
- linkml_xsd-0.2.0.dist-info/scm_file_list.json +223 -0
- linkml_xsd-0.2.0.dist-info/scm_version.json +8 -0
- linkml_xsd-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
linkml_to_xsd/config.py
ADDED
|
@@ -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"]
|