linkml 1.9.4rc2__py3-none-any.whl → 1.9.5rc2__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.
Files changed (83) hide show
  1. linkml/cli/main.py +5 -1
  2. linkml/converter/__init__.py +0 -0
  3. linkml/generators/__init__.py +2 -0
  4. linkml/generators/common/build.py +5 -20
  5. linkml/generators/common/template.py +289 -3
  6. linkml/generators/docgen.py +55 -10
  7. linkml/generators/erdiagramgen.py +9 -5
  8. linkml/generators/graphqlgen.py +32 -6
  9. linkml/generators/jsonldcontextgen.py +78 -12
  10. linkml/generators/jsonschemagen.py +29 -12
  11. linkml/generators/mermaidclassdiagramgen.py +21 -3
  12. linkml/generators/owlgen.py +13 -2
  13. linkml/generators/panderagen/dataframe_class.py +13 -0
  14. linkml/generators/panderagen/dataframe_field.py +50 -0
  15. linkml/generators/panderagen/linkml_pandera_validator.py +186 -0
  16. linkml/generators/panderagen/panderagen.py +22 -5
  17. linkml/generators/panderagen/panderagen_class_based/class.jinja2 +70 -13
  18. linkml/generators/panderagen/panderagen_class_based/custom_checks.jinja2 +27 -0
  19. linkml/generators/panderagen/panderagen_class_based/enums.jinja2 +3 -3
  20. linkml/generators/panderagen/panderagen_class_based/pandera.jinja2 +12 -2
  21. linkml/generators/panderagen/panderagen_class_based/slots.jinja2 +19 -17
  22. linkml/generators/panderagen/slot_generator_mixin.py +143 -16
  23. linkml/generators/panderagen/transforms/__init__.py +19 -0
  24. linkml/generators/panderagen/transforms/collection_dict_model_transform.py +62 -0
  25. linkml/generators/panderagen/transforms/list_dict_model_transform.py +66 -0
  26. linkml/generators/panderagen/transforms/model_transform.py +8 -0
  27. linkml/generators/panderagen/transforms/nested_struct_model_transform.py +27 -0
  28. linkml/generators/panderagen/transforms/simple_dict_model_transform.py +86 -0
  29. linkml/generators/plantumlgen.py +17 -11
  30. linkml/generators/pydanticgen/pydanticgen.py +53 -2
  31. linkml/generators/pydanticgen/template.py +45 -233
  32. linkml/generators/pydanticgen/templates/attribute.py.jinja +1 -0
  33. linkml/generators/pydanticgen/templates/base_model.py.jinja +16 -2
  34. linkml/generators/pydanticgen/templates/imports.py.jinja +1 -1
  35. linkml/generators/rdfgen.py +11 -2
  36. linkml/generators/rustgen/__init__.py +3 -0
  37. linkml/generators/rustgen/build.py +97 -0
  38. linkml/generators/rustgen/cli.py +83 -0
  39. linkml/generators/rustgen/rustgen.py +1186 -0
  40. linkml/generators/rustgen/template.py +910 -0
  41. linkml/generators/rustgen/templates/Cargo.toml.jinja +42 -0
  42. linkml/generators/rustgen/templates/anything.rs.jinja +149 -0
  43. linkml/generators/rustgen/templates/as_key_value.rs.jinja +86 -0
  44. linkml/generators/rustgen/templates/class_module.rs.jinja +8 -0
  45. linkml/generators/rustgen/templates/enum.rs.jinja +70 -0
  46. linkml/generators/rustgen/templates/file.rs.jinja +75 -0
  47. linkml/generators/rustgen/templates/import.rs.jinja +4 -0
  48. linkml/generators/rustgen/templates/imports.rs.jinja +8 -0
  49. linkml/generators/rustgen/templates/lib_shim.rs.jinja +52 -0
  50. linkml/generators/rustgen/templates/poly.rs.jinja +9 -0
  51. linkml/generators/rustgen/templates/poly_containers.rs.jinja +439 -0
  52. linkml/generators/rustgen/templates/poly_trait.rs.jinja +15 -0
  53. linkml/generators/rustgen/templates/poly_trait_impl.rs.jinja +5 -0
  54. linkml/generators/rustgen/templates/poly_trait_impl_orsubtype.rs.jinja +5 -0
  55. linkml/generators/rustgen/templates/poly_trait_property.rs.jinja +8 -0
  56. linkml/generators/rustgen/templates/poly_trait_property_impl.rs.jinja +134 -0
  57. linkml/generators/rustgen/templates/poly_trait_property_match.rs.jinja +10 -0
  58. linkml/generators/rustgen/templates/property.rs.jinja +28 -0
  59. linkml/generators/rustgen/templates/pyproject.toml.jinja +10 -0
  60. linkml/generators/rustgen/templates/serde_utils.rs.jinja +490 -0
  61. linkml/generators/rustgen/templates/slot_range_as_union.rs.jinja +64 -0
  62. linkml/generators/rustgen/templates/struct.rs.jinja +81 -0
  63. linkml/generators/rustgen/templates/struct_or_subtype_enum.rs.jinja +111 -0
  64. linkml/generators/rustgen/templates/stub_gen.rs.jinja +71 -0
  65. linkml/generators/rustgen/templates/stub_utils.rs.jinja +76 -0
  66. linkml/generators/rustgen/templates/typealias.rs.jinja +13 -0
  67. linkml/generators/sqltablegen.py +18 -16
  68. linkml/generators/yarrrmlgen.py +173 -0
  69. linkml/linter/config/datamodel/config.py +160 -293
  70. linkml/linter/config/datamodel/config.yaml +34 -26
  71. linkml/linter/config/default.yaml +4 -0
  72. linkml/linter/config/recommended.yaml +4 -0
  73. linkml/linter/linter.py +1 -2
  74. linkml/linter/rules.py +37 -0
  75. linkml/utils/schema_builder.py +2 -0
  76. linkml/utils/schemaloader.py +55 -3
  77. {linkml-1.9.4rc2.dist-info → linkml-1.9.5rc2.dist-info}/METADATA +1 -1
  78. {linkml-1.9.4rc2.dist-info → linkml-1.9.5rc2.dist-info}/RECORD +82 -40
  79. {linkml-1.9.4rc2.dist-info → linkml-1.9.5rc2.dist-info}/entry_points.txt +2 -1
  80. linkml/generators/panderagen/panderagen_class_based/mixins.jinja2 +0 -26
  81. /linkml/{utils/converter.py → converter/cli.py} +0 -0
  82. {linkml-1.9.4rc2.dist-info → linkml-1.9.5rc2.dist-info}/WHEEL +0 -0
  83. {linkml-1.9.4rc2.dist-info → linkml-1.9.5rc2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,111 @@
1
+ #[derive(Debug, Clone, PartialEq)]
2
+ #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
3
+ {% if type_designator_field %}
4
+ #[cfg_attr(feature="serde", serde(tag = "{{ type_designator_field }}"))]
5
+ {% else %}
6
+ #[cfg_attr(feature="serde", serde(untagged))]
7
+ {% endif %}
8
+ pub enum {{ enum_name }} {
9
+ {%- for t in struct_names -%}
10
+ {% if type_designator_field %}
11
+ {% set tds = type_designators[t] %}
12
+ #[serde(rename = "{{ tds[0] }}", {% for t in tds[1:] %} alias="{{ t }}", {% endfor %} )]
13
+ {% endif %}
14
+ {{ t }}({{ t }}){% if not loop.last %}, {% endif %}
15
+ {%- endfor -%}
16
+ }
17
+
18
+ {% for t in struct_names %}
19
+ impl From<{{ t }}> for {{ enum_name }} { fn from(x: {{ t }}) -> Self { Self::{{ t }}(x) } }
20
+ {% endfor %}
21
+
22
+ #[cfg(feature = "pyo3")]
23
+ impl<'py> FromPyObject<'py> for {{ enum_name }} {
24
+ fn extract_bound(ob: &pyo3::Bound<'py, pyo3::types::PyAny>) -> pyo3::PyResult<Self> {
25
+ {% for t in struct_names %}
26
+ if let Ok(val) = ob.extract::<{{ t }}>() {
27
+ return Ok({{ enum_name }}::{{ t }}(val));
28
+ }
29
+ {%- endfor -%}
30
+ Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
31
+ "invalid {{ enum_name }}",
32
+ ))
33
+ }
34
+ }
35
+
36
+ #[cfg(feature = "pyo3")]
37
+ impl<'py> IntoPyObject<'py> for {{ enum_name }} {
38
+ type Target = PyAny;
39
+ type Output = Bound<'py, Self::Target>;
40
+ type Error = PyErr;
41
+
42
+ fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
43
+ match self {
44
+ {% for t in struct_names %}
45
+ {{ enum_name }}::{{ t }}(val) => val.into_pyobject(py).map(move |b| b.into_any()),
46
+ {% endfor %}
47
+ }
48
+ }
49
+ }
50
+
51
+
52
+ #[cfg(feature = "pyo3")]
53
+ impl<'py> IntoPyObject<'py> for Box<{{ enum_name }}>
54
+ {
55
+ type Target = PyAny;
56
+ type Output = Bound<'py, Self::Target>;
57
+ type Error = PyErr;
58
+ fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
59
+ (*self).into_pyobject(py).map(move |x| x.into_any())
60
+ }
61
+ }
62
+
63
+ #[cfg(feature = "pyo3")]
64
+ impl<'py> FromPyObject<'py> for Box<{{ enum_name }}> {
65
+ fn extract_bound(ob: &pyo3::Bound<'py, pyo3::types::PyAny>) -> pyo3::PyResult<Self> {
66
+ if let Ok(val) = ob.extract::<{{ enum_name }}>() {
67
+ return Ok(Box::new(val));
68
+ }
69
+ Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
70
+ "invalid {{ enum_name }}",
71
+ ))
72
+ }
73
+ }
74
+
75
+ {% if as_key_value %}
76
+ #[cfg(feature = "serde")]
77
+ impl serde_utils::InlinedPair for {{enum_name}} {
78
+ type Key = {{ key_property_type }};
79
+ type Value = serde_value::Value;
80
+ type Error = String;
81
+
82
+ fn from_pair_mapping(k: Self::Key, v: Self::Value) -> Result<Self, Self::Error> {
83
+ {% for t in struct_names %}
84
+ if let Ok(x) = {{t}}::from_pair_mapping(k.clone(), v.clone()) {
85
+ return Ok({{enum_name}}::{{t}}(x));
86
+ }
87
+ {% endfor %}
88
+ Err("none of the variants matched the mapping form".into())
89
+ }
90
+
91
+ fn from_pair_simple(k: Self::Key, v: Self::Value) -> Result<Self, Self::Error> {
92
+ {% for t in struct_names %}
93
+ if let Ok(x) = {{t}}::from_pair_simple(k.clone(), v.clone()) {
94
+ return Ok({{enum_name}}::{{t}}(x));
95
+ }
96
+ {% endfor %}
97
+ Err("none of the variants support the primitive form".into())
98
+ }
99
+
100
+ fn extract_key(&self) -> &Self::Key {
101
+ match self {
102
+ {% for t in struct_names %}
103
+ {{enum_name}}::{{t}}(inner) => inner.extract_key(),
104
+ {% endfor %}
105
+ }
106
+ }
107
+ }
108
+ {% endif %}
109
+
110
+ #[cfg(feature = "stubgen")]
111
+ ::pyo3_stub_gen::impl_stub_type!({{ enum_name }} = {{ struct_names | join(' | ') }});
@@ -0,0 +1,71 @@
1
+ #[cfg(feature = "stubgen")]
2
+ use {{ crate_module }}::stub_utils;
3
+
4
+ #[cfg(feature = "stubgen")]
5
+ fn main() -> pyo3_stub_gen::Result<()> {
6
+ let check_only = std::env::args().skip(1).any(|arg| arg == "--check");
7
+ let stub = {{ crate_name }}::stub_info()?;
8
+
9
+ if check_only {
10
+ check_stubs(&stub)
11
+ } else {
12
+ stub.generate()
13
+ }
14
+ }
15
+
16
+ #[cfg(feature = "stubgen")]
17
+ fn check_stubs(stub: &pyo3_stub_gen::StubInfo) -> pyo3_stub_gen::Result<()> {
18
+ use std::fs;
19
+ use std::io::ErrorKind;
20
+
21
+ let mut issues = Vec::new();
22
+
23
+ for (name, module) in &stub.modules {
24
+ let module_path = stub_utils::normalize_stub_module(name)?;
25
+ let mut dest = stub.python_root.join(&module_path);
26
+ if module.submodules.is_empty() {
27
+ dest.set_extension("pyi");
28
+ } else {
29
+ dest.push("__init__.pyi");
30
+ }
31
+
32
+ let expected = module.to_string();
33
+ match fs::read_to_string(&dest) {
34
+ Ok(actual) => {
35
+ if actual != expected {
36
+ issues.push(format!(
37
+ "updated content differs for `{}` (module `{name}`)",
38
+ dest.display()
39
+ ));
40
+ }
41
+ }
42
+ Err(err) if err.kind() == ErrorKind::NotFound => {
43
+ issues.push(format!(
44
+ "missing stub file `{}` for module `{name}`",
45
+ dest.display()
46
+ ));
47
+ }
48
+ Err(err) => return Err(err.into()),
49
+ }
50
+ }
51
+
52
+ if issues.is_empty() {
53
+ Ok(())
54
+ } else {
55
+ let mut msg = String::from("Stub files are out of date:\n");
56
+ for issue in issues {
57
+ msg.push_str(" - ");
58
+ msg.push_str(&issue);
59
+ msg.push('\n');
60
+ }
61
+ msg.push_str("Run `cargo run --bin stub_gen --features stubgen` to regenerate.");
62
+ Err(std::io::Error::new(ErrorKind::Other, msg).into())
63
+ }
64
+ }
65
+
66
+ #[cfg(not(feature = "stubgen"))]
67
+ fn main() {
68
+ eprintln!(
69
+ "Enable the `stubgen` feature (alongside `pyo3`) to run this generator."
70
+ );
71
+ }
@@ -0,0 +1,76 @@
1
+ use std::io::{Error, ErrorKind};
2
+ use std::path::{Component, PathBuf};
3
+
4
+ /// Convert a stub module name into a relative filesystem path.
5
+ ///
6
+ /// The generator currently expects module names that mirror Python package
7
+ /// semantics (``foo.bar``) with optional hyphens. Hyphens are replaced with
8
+ /// underscores and dots map to directory separators. The helper rejects
9
+ /// absolute paths, traversal markers, or characters that cannot appear in
10
+ /// stub files so we never escape the designated stub root.
11
+ pub fn normalize_stub_module(name: &str) -> std::io::Result<PathBuf> {
12
+ if name.is_empty() {
13
+ return Err(Error::new(
14
+ ErrorKind::InvalidInput,
15
+ "module name may not be empty",
16
+ ));
17
+ }
18
+
19
+ let normalized = name.replace('-', "_");
20
+ let path_str = normalized.replace('.', "/");
21
+ let path = PathBuf::from(&path_str);
22
+
23
+ if path.is_absolute() {
24
+ return Err(Error::new(
25
+ ErrorKind::InvalidInput,
26
+ format!("absolute module path `{name}` not allowed"),
27
+ ));
28
+ }
29
+
30
+ for component in path.components() {
31
+ match component {
32
+ Component::Normal(part) => {
33
+ let segment = part.to_string_lossy();
34
+ if segment.is_empty() {
35
+ return Err(Error::new(
36
+ ErrorKind::InvalidInput,
37
+ format!("module name `{name}` contains an empty segment"),
38
+ ));
39
+ }
40
+ if segment == "." || segment == ".." {
41
+ return Err(Error::new(
42
+ ErrorKind::InvalidInput,
43
+ format!(
44
+ "module name `{name}` contains disallowed segment `{segment}`"
45
+ ),
46
+ ));
47
+ }
48
+ if segment
49
+ .chars()
50
+ .any(|ch| matches!(ch, '*' | '?' | '<' | '>' | '|' | ':' | '\\' | '/'))
51
+ {
52
+ return Err(Error::new(
53
+ ErrorKind::InvalidInput,
54
+ format!(
55
+ "module name `{name}` contains invalid characters in `{segment}`"
56
+ ),
57
+ ));
58
+ }
59
+ }
60
+ Component::CurDir | Component::ParentDir => {
61
+ return Err(Error::new(
62
+ ErrorKind::InvalidInput,
63
+ format!("module name `{name}` attempts directory traversal"),
64
+ ));
65
+ }
66
+ Component::RootDir | Component::Prefix(_) => {
67
+ return Err(Error::new(
68
+ ErrorKind::InvalidInput,
69
+ format!("module name `{name}` resolves outside stub root"),
70
+ ));
71
+ }
72
+ }
73
+ }
74
+
75
+ Ok(path)
76
+ }
@@ -0,0 +1,13 @@
1
+ {% if attributes or description %}
2
+ #[
3
+ {%- if description -%}doc = r" {{ description | escape }}", {%- endif -%}
4
+ {%- for key, val in attributes.items() -%}
5
+ {{ key }} = "{{ val }}"{% if not loop.last %}, {% endif %}
6
+ {%- endfor -%}
7
+ ]
8
+ {%- endif -%}
9
+ pub type {{ name }} = {%- if multivalued %} Vec<{{ type_ }}>
10
+ {%- else %} {{ type_ }}{%- endif -%};
11
+ {%- if slot_range_as_union %}
12
+ {{ slot_range_as_union }}
13
+ {% endif -%}
@@ -149,8 +149,7 @@ class SQLTableGenerator(Generator):
149
149
  relative_slot_num: bool = False
150
150
  default_length_oracle: int = ORACLE_MAX_VARCHAR_LENGTH
151
151
  generate_abstract_class_ddl: bool = True
152
- autogenerate_pk_index: bool = True
153
- autogenerate_fk_index: bool = True
152
+ autogenerate_index: bool = True
154
153
 
155
154
  def serialize(self, **kwargs: dict[str, Any]) -> str:
156
155
  return self.generate_ddl(**kwargs)
@@ -235,8 +234,8 @@ class SQLTableGenerator(Generator):
235
234
  fk = sql_name(self.get_id_or_key(s.range, sv))
236
235
  args = [ForeignKey(fk)]
237
236
  field_type = self.get_sql_range(s, schema)
238
- fk_index_cond = (s.key or s.identifier) and self.autogenerate_fk_index
239
- pk_index_cond = is_pk and self.autogenerate_pk_index
237
+ fk_index_cond = (s.key or s.identifier) and self.autogenerate_index
238
+ pk_index_cond = is_pk and self.autogenerate_index
240
239
  is_index = fk_index_cond or pk_index_cond
241
240
  col = Column(
242
241
  sql_name(sn),
@@ -266,18 +265,21 @@ class SQLTableGenerator(Generator):
266
265
  sql_uc = UniqueConstraint(*sql_names)
267
266
  cols.append(sql_uc)
268
267
  # Anything that has a unique constraint should have an associated index with it
269
- uc_index_name = sql_name(cn)
270
- for name in sql_names:
271
- uc_index_name = uc_index_name + "_" + name
272
- uc_index_name = uc_index_name + "_idx"
273
- is_duplicate = self.check_duplicate_entry_names(autogenerated_item_names, sql_name(uc_index_name))
274
- if not is_duplicate:
275
- sql_names = [sql_name(uc_index_name)] + sql_names
276
- uc_index = Index(*sql_names)
277
- cols.append(uc_index)
278
- autogenerated_item_names.append(sql_name(uc_index_name))
268
+ if self.autogenerate_index:
269
+ uc_index_name = sql_name(cn)
270
+ for name in sql_names:
271
+ uc_index_name = uc_index_name + "_" + name
272
+ uc_index_name = uc_index_name + "_idx"
273
+ is_duplicate = self.check_duplicate_entry_names(
274
+ autogenerated_item_names, sql_name(uc_index_name)
275
+ )
276
+ if not is_duplicate:
277
+ sql_names = [sql_name(uc_index_name)] + sql_names
278
+ uc_index = Index(*sql_names)
279
+ cols.append(uc_index)
280
+ autogenerated_item_names.append(sql_name(uc_index_name))
279
281
  if not c.abstract or (c.abstract and self.generate_abstract_class_ddl):
280
- for tag, annotation in c.annotations.items():
282
+ for tag, annotation in sorted(c.annotations.items()):
281
283
  if tag == "index":
282
284
  value_dict = {k: annotation for k, annotation in annotation.value._items()}
283
285
  for key, value in value_dict.items():
@@ -424,7 +426,7 @@ class SQLTableGenerator(Generator):
424
426
  )
425
427
  @click.option(
426
428
  "--autogenerate_index",
427
- default=False,
429
+ default=True,
428
430
  show_default=True,
429
431
  help="Enable the creation of indexes on all columns generated",
430
432
  )
@@ -0,0 +1,173 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any, TextIO
5
+
6
+ import click
7
+ import yaml
8
+ from linkml_runtime.linkml_model.meta import ClassDefinition, SchemaDefinition
9
+ from linkml_runtime.utils.schemaview import SchemaView
10
+
11
+ from linkml._version import __version__
12
+ from linkml.utils.generator import Generator, shared_arguments
13
+
14
+ # defaults
15
+ DEFAULT_SOURCE_JSON = "data.json~jsonpath"
16
+ DEFAULT_SOURCE_CSV = "data.csv~csv"
17
+ DEFAULT_ITERATOR = "$.items[*]" # generic top-level array
18
+
19
+
20
+ class _YamlDumper(yaml.Dumper):
21
+ # keep list indentation stable
22
+ def increase_indent(self, flow: bool = False, indentless: bool = False):
23
+ return super().increase_indent(flow, False)
24
+
25
+
26
+ class YarrrmlGenerator(Generator):
27
+ """LinkML -> YARRRML exporter."""
28
+
29
+ generatorname = os.path.basename(__file__)
30
+ generatorversion = "0.2.0"
31
+ valid_formats = ["yml", "yaml"]
32
+ visit_all_class_slots = False
33
+
34
+ def __init__(self, schema: str | TextIO | SchemaDefinition, format: str = "yml", **kwargs):
35
+ def _infer_source_suffix(path: str) -> str:
36
+ p = (path or "").lower()
37
+ if "~" in p:
38
+ return path # already has ~jsonpath or ~csv
39
+ if p.endswith(".json"):
40
+ return f"{path}~jsonpath"
41
+ if p.endswith(".csv") or p.endswith(".tsv"):
42
+ return f"{path}~csv"
43
+ return path
44
+
45
+ # in __init__ right after you read src:
46
+ raw_src = kwargs.pop("source", None)
47
+ it = kwargs.pop("iterator_template", None)
48
+
49
+ super().__init__(schema, **kwargs)
50
+
51
+ self.schemaview = SchemaView(schema)
52
+ self.schema: SchemaDefinition = self.schemaview.schema
53
+
54
+ self.format = format
55
+
56
+ # normalize source: if user passed file without "~csv/~jsonpath", infer it
57
+ if raw_src:
58
+ self.source = _infer_source_suffix(raw_src)
59
+ else:
60
+ self.source = DEFAULT_SOURCE_JSON
61
+
62
+ self.iterator_template: str = it or DEFAULT_ITERATOR
63
+
64
+ # public
65
+ def serialize(self, **args) -> str:
66
+ data = yaml.dump(
67
+ self.as_dict(),
68
+ Dumper=_YamlDumper,
69
+ sort_keys=False,
70
+ default_flow_style=False,
71
+ allow_unicode=True,
72
+ )
73
+ return data
74
+
75
+ def as_dict(self) -> dict[str, Any]:
76
+ sv = self.schemaview
77
+ mappings = {}
78
+ for cls in sv.all_classes().values():
79
+ if not (sv.get_identifier_slot(cls.name) or sv.get_key_slot(cls.name)):
80
+ continue
81
+
82
+ mapping = {
83
+ "s": self._subject_template_for_class(cls),
84
+ "po": self._po_list_for_class(cls),
85
+ }
86
+ if self._is_json_source():
87
+ mapping["sources"] = [[self.source, self._iterator_for_class(cls)]]
88
+ else:
89
+ mapping["sources"] = [[self.source]]
90
+
91
+ mappings[str(cls.name)] = mapping
92
+
93
+ return {"prefixes": self._prefixes(), "mappings": mappings}
94
+
95
+ # helpers
96
+ def _is_json_source(self) -> bool:
97
+ return "~jsonpath" in (self.source or "")
98
+
99
+ def _prefixes(self) -> dict[str, str]:
100
+ px: dict[str, str] = {}
101
+ if self.schema.prefixes:
102
+ for p in self.schema.prefixes.values():
103
+ if p.prefix_prefix and p.prefix_reference:
104
+ px[str(p.prefix_prefix)] = str(p.prefix_reference)
105
+ px.setdefault("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
106
+ return px
107
+
108
+ def _iterator_for_class(self, c: ClassDefinition) -> str:
109
+ # supports {Class}
110
+ return self.iterator_template.replace("{Class}", c.name)
111
+
112
+ def _subject_template_for_class(self, c: ClassDefinition) -> str:
113
+ sv = self.schemaview
114
+ default_prefix = sv.schema.default_prefix or "ex"
115
+ id_slot = sv.get_identifier_slot(c.name)
116
+ if id_slot:
117
+ return f"{default_prefix}:$({id_slot.name})"
118
+ key_slot = sv.get_key_slot(c.name)
119
+ if key_slot:
120
+ return f"{default_prefix}:$({key_slot.name})"
121
+ return f"{default_prefix}:{c.name}/$(subject_id)" # safe fallback
122
+
123
+ def _po_list_for_class(self, c: ClassDefinition) -> list[dict[str, Any]]:
124
+ sv = self.schemaview
125
+ po = []
126
+ class_curie = sv.get_uri(c, expand=False)
127
+ if class_curie:
128
+ po.append({"p": "rdf:type", "o": str(class_curie)})
129
+
130
+ default_prefix = sv.schema.default_prefix or "ex"
131
+
132
+ for s in sv.class_induced_slots(c.name):
133
+ pred = sv.get_uri(s, expand=False) or f"{default_prefix}:{s.name}"
134
+ decl = sv.get_slot(s.name)
135
+ alias = decl.alias if decl and decl.alias else s.alias
136
+ var = alias or s.name
137
+
138
+ is_obj = sv.get_class(s.range) is not None if s.range else False
139
+ if is_obj:
140
+ inlined = None
141
+ if decl and decl.inlined is not None:
142
+ inlined = decl.inlined
143
+ if inlined is False:
144
+ po.append({"p": pred, "o": {"value": f"$({var})", "type": "iri"}})
145
+ continue
146
+
147
+ po.append({"p": pred, "o": f"$({var})"})
148
+ return po
149
+
150
+
151
+ @shared_arguments(YarrrmlGenerator)
152
+ @click.command(name="yarrrml")
153
+ @click.option(
154
+ "--source",
155
+ help="YARRRML source shorthand, e.g., data.json~jsonpath or data.csv~csv (TSV works too)",
156
+ )
157
+ @click.option(
158
+ "--iterator-template",
159
+ help='JSONPath iterator template; supports {Class}, default: "$.items[*]"',
160
+ )
161
+ @click.version_option(__version__, "-V", "--version")
162
+ def cli(yamlfile, source, iterator_template, **args):
163
+ """Generate YARRRML mappings from a LinkML schema."""
164
+ if source:
165
+ args["source"] = source
166
+ if iterator_template:
167
+ args["iterator_template"] = iterator_template
168
+ gen = YarrrmlGenerator(yamlfile, **args)
169
+ print(gen.serialize(**args))
170
+
171
+
172
+ if __name__ == "__main__":
173
+ cli()