linkml 1.9.4rc2__py3-none-any.whl → 1.9.5__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/cli/main.py +5 -1
- linkml/converter/__init__.py +0 -0
- linkml/generators/__init__.py +2 -0
- linkml/generators/common/build.py +5 -20
- linkml/generators/common/template.py +289 -3
- linkml/generators/docgen.py +55 -10
- linkml/generators/erdiagramgen.py +9 -5
- linkml/generators/graphqlgen.py +32 -6
- linkml/generators/jsonldcontextgen.py +78 -12
- linkml/generators/jsonschemagen.py +29 -12
- linkml/generators/mermaidclassdiagramgen.py +21 -3
- linkml/generators/owlgen.py +13 -2
- linkml/generators/panderagen/dataframe_class.py +13 -0
- linkml/generators/panderagen/dataframe_field.py +50 -0
- linkml/generators/panderagen/linkml_pandera_validator.py +186 -0
- linkml/generators/panderagen/panderagen.py +22 -5
- linkml/generators/panderagen/panderagen_class_based/class.jinja2 +70 -13
- linkml/generators/panderagen/panderagen_class_based/custom_checks.jinja2 +27 -0
- linkml/generators/panderagen/panderagen_class_based/enums.jinja2 +3 -3
- linkml/generators/panderagen/panderagen_class_based/pandera.jinja2 +12 -2
- linkml/generators/panderagen/panderagen_class_based/slots.jinja2 +19 -17
- linkml/generators/panderagen/slot_generator_mixin.py +143 -16
- linkml/generators/panderagen/transforms/__init__.py +19 -0
- linkml/generators/panderagen/transforms/collection_dict_model_transform.py +62 -0
- linkml/generators/panderagen/transforms/list_dict_model_transform.py +66 -0
- linkml/generators/panderagen/transforms/model_transform.py +8 -0
- linkml/generators/panderagen/transforms/nested_struct_model_transform.py +27 -0
- linkml/generators/panderagen/transforms/simple_dict_model_transform.py +86 -0
- linkml/generators/plantumlgen.py +17 -11
- linkml/generators/pydanticgen/pydanticgen.py +53 -2
- linkml/generators/pydanticgen/template.py +45 -233
- linkml/generators/pydanticgen/templates/attribute.py.jinja +1 -0
- linkml/generators/pydanticgen/templates/base_model.py.jinja +16 -2
- linkml/generators/pydanticgen/templates/imports.py.jinja +1 -1
- linkml/generators/rdfgen.py +11 -2
- linkml/generators/rustgen/__init__.py +3 -0
- linkml/generators/rustgen/build.py +97 -0
- linkml/generators/rustgen/cli.py +83 -0
- linkml/generators/rustgen/rustgen.py +1186 -0
- linkml/generators/rustgen/template.py +910 -0
- linkml/generators/rustgen/templates/Cargo.toml.jinja +42 -0
- linkml/generators/rustgen/templates/anything.rs.jinja +149 -0
- linkml/generators/rustgen/templates/as_key_value.rs.jinja +86 -0
- linkml/generators/rustgen/templates/class_module.rs.jinja +8 -0
- linkml/generators/rustgen/templates/enum.rs.jinja +70 -0
- linkml/generators/rustgen/templates/file.rs.jinja +75 -0
- linkml/generators/rustgen/templates/import.rs.jinja +4 -0
- linkml/generators/rustgen/templates/imports.rs.jinja +8 -0
- linkml/generators/rustgen/templates/lib_shim.rs.jinja +52 -0
- linkml/generators/rustgen/templates/poly.rs.jinja +9 -0
- linkml/generators/rustgen/templates/poly_containers.rs.jinja +439 -0
- linkml/generators/rustgen/templates/poly_trait.rs.jinja +15 -0
- linkml/generators/rustgen/templates/poly_trait_impl.rs.jinja +5 -0
- linkml/generators/rustgen/templates/poly_trait_impl_orsubtype.rs.jinja +5 -0
- linkml/generators/rustgen/templates/poly_trait_property.rs.jinja +8 -0
- linkml/generators/rustgen/templates/poly_trait_property_impl.rs.jinja +134 -0
- linkml/generators/rustgen/templates/poly_trait_property_match.rs.jinja +10 -0
- linkml/generators/rustgen/templates/property.rs.jinja +28 -0
- linkml/generators/rustgen/templates/pyproject.toml.jinja +10 -0
- linkml/generators/rustgen/templates/serde_utils.rs.jinja +490 -0
- linkml/generators/rustgen/templates/slot_range_as_union.rs.jinja +64 -0
- linkml/generators/rustgen/templates/struct.rs.jinja +81 -0
- linkml/generators/rustgen/templates/struct_or_subtype_enum.rs.jinja +111 -0
- linkml/generators/rustgen/templates/stub_gen.rs.jinja +71 -0
- linkml/generators/rustgen/templates/stub_utils.rs.jinja +76 -0
- linkml/generators/rustgen/templates/typealias.rs.jinja +13 -0
- linkml/generators/sqltablegen.py +18 -16
- linkml/generators/yarrrmlgen.py +173 -0
- linkml/linter/config/datamodel/config.py +160 -293
- linkml/linter/config/datamodel/config.yaml +34 -26
- linkml/linter/config/default.yaml +4 -0
- linkml/linter/config/recommended.yaml +4 -0
- linkml/linter/linter.py +1 -2
- linkml/linter/rules.py +37 -0
- linkml/utils/schema_builder.py +2 -0
- linkml/utils/schemaloader.py +76 -3
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/METADATA +1 -1
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/RECORD +82 -40
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/entry_points.txt +2 -1
- linkml/generators/panderagen/panderagen_class_based/mixins.jinja2 +0 -26
- /linkml/{utils/converter.py → converter/cli.py} +0 -0
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/WHEEL +0 -0
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5.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 -%}
|
linkml/generators/sqltablegen.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
239
|
-
pk_index_cond = is_pk and self.
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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=
|
|
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()
|