linkml 1.9.3rc1__py3-none-any.whl → 1.9.3rc2__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/generators/PythonGenNotes.md +49 -49
- linkml/generators/README.md +2 -2
- linkml/generators/dbmlgen.py +0 -1
- linkml/generators/docgen/class.md.jinja2 +35 -11
- linkml/generators/docgen/class_diagram.md.jinja2 +15 -15
- linkml/generators/docgen/common_metadata.md.jinja2 +3 -5
- linkml/generators/docgen/enum.md.jinja2 +4 -5
- linkml/generators/docgen/index.md.jinja2 +5 -5
- linkml/generators/docgen/index.tex.jinja2 +1 -1
- linkml/generators/docgen/schema.md.jinja2 +1 -3
- linkml/generators/docgen/slot.md.jinja2 +6 -8
- linkml/generators/docgen/subset.md.jinja2 +4 -7
- linkml/generators/docgen/type.md.jinja2 +2 -3
- linkml/generators/docgen.py +92 -4
- linkml/generators/erdiagramgen.py +1 -1
- linkml/generators/graphqlgen.py +1 -1
- linkml/generators/javagen/example_template.java.jinja2 +0 -1
- linkml/generators/jsonldcontextgen.py +0 -1
- linkml/generators/jsonldgen.py +3 -1
- linkml/generators/jsonschemagen.py +2 -2
- linkml/generators/linkmlgen.py +1 -1
- linkml/generators/markdowngen.py +20 -9
- linkml/generators/mermaidclassdiagramgen.py +4 -0
- linkml/generators/projectgen.py +17 -20
- linkml/generators/pydanticgen/includes.py +5 -5
- linkml/generators/pydanticgen/pydanticgen.py +1 -2
- linkml/generators/pydanticgen/template.py +1 -1
- linkml/generators/pydanticgen/templates/base_model.py.jinja +1 -1
- linkml/generators/pydanticgen/templates/class.py.jinja +1 -1
- linkml/generators/pydanticgen/templates/conditional_import.py.jinja +1 -1
- linkml/generators/pydanticgen/templates/footer.py.jinja +1 -1
- linkml/generators/pydanticgen/templates/imports.py.jinja +1 -1
- linkml/generators/python/python_ifabsent_processor.py +1 -1
- linkml/generators/pythongen.py +9 -8
- linkml/generators/shacl/shacl_ifabsent_processor.py +0 -1
- linkml/generators/shaclgen.py +1 -1
- linkml/generators/sparqlgen.py +1 -1
- linkml/generators/sqlalchemy/__init__.py +0 -4
- linkml/generators/sqlalchemygen.py +9 -15
- linkml/generators/sqltablegen.py +70 -4
- linkml/generators/string_template.md +3 -4
- linkml/generators/terminusdbgen.py +1 -2
- linkml/linter/cli.py +3 -4
- linkml/linter/config/datamodel/config.py +286 -135
- linkml/linter/config/datamodel/config.yaml +26 -11
- linkml/linter/config/default.yaml +6 -0
- linkml/linter/config/recommended.yaml +6 -0
- linkml/linter/linter.py +10 -6
- linkml/linter/rules.py +144 -46
- linkml/transformers/relmodel_transformer.py +2 -1
- linkml/utils/generator.py +3 -0
- linkml/utils/logictools.py +5 -5
- linkml/utils/schemaloader.py +4 -4
- linkml/utils/schemasynopsis.py +11 -7
- linkml/workspaces/datamodel/workspaces.yaml +0 -5
- {linkml-1.9.3rc1.dist-info → linkml-1.9.3rc2.dist-info}/LICENSE +1 -1
- {linkml-1.9.3rc1.dist-info → linkml-1.9.3rc2.dist-info}/METADATA +1 -1
- {linkml-1.9.3rc1.dist-info → linkml-1.9.3rc2.dist-info}/RECORD +60 -60
- {linkml-1.9.3rc1.dist-info → linkml-1.9.3rc2.dist-info}/WHEEL +0 -0
- {linkml-1.9.3rc1.dist-info → linkml-1.9.3rc2.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
# Subset: {{ gen.name(element) }} {% if element.deprecated %} <span style="color: red;"><strong> (DEPRECATED) </strong> {% endif %}
|
1
|
+
# Subset: {{ gen.name(element) }} {% if element.deprecated %} <span style="color: red;"><strong> (DEPRECATED) </strong></span> {% endif %}
|
2
2
|
|
3
3
|
{%- if header -%}
|
4
4
|
{{ header }}
|
@@ -45,7 +45,7 @@ URI: {{ gen.link(element) }}
|
|
45
45
|
| --- | --- |
|
46
46
|
{% for c in classes_in_subset -%}
|
47
47
|
{%- if element.name in c.in_subset -%}
|
48
|
-
| {{gen.link(c)}} | {{c.description|enshorten}} |
|
48
|
+
| {{ gen.link(c) }} | {{ c.description|enshorten }} |
|
49
49
|
{% endif -%}
|
50
50
|
{% endfor %}
|
51
51
|
|
@@ -67,17 +67,15 @@ URI: {{ gen.link(element) }}
|
|
67
67
|
| Name | Cardinality and Range | Description |
|
68
68
|
| --- | --- | --- |
|
69
69
|
{% for s in filtered_slots -%}
|
70
|
-
| {{gen.link(s)}} | {{ gen.cardinality(s) }} <br/> {{gen.link(s.range)}} | {{s.description|enshorten}} {% if s.identifier %}**identifier**{% endif %} |
|
70
|
+
| {{ gen.link(s) }} | {{ gen.cardinality(s) }} <br/> {{ gen.link(s.range) }} | {{ s.description|enshorten }} {% if s.identifier %}**identifier**{% endif %} |
|
71
71
|
{% endfor %}
|
72
72
|
{%- endif %}
|
73
73
|
|
74
|
-
|
75
74
|
{%- endif %}
|
76
75
|
{% endfor %}
|
77
76
|
|
78
77
|
{%- endif %}
|
79
78
|
|
80
|
-
|
81
79
|
{% if slots_in_subset %}
|
82
80
|
## Slots in subset
|
83
81
|
|
@@ -91,14 +89,13 @@ URI: {{ gen.link(element) }}
|
|
91
89
|
|
92
90
|
{%- endif %}
|
93
91
|
|
94
|
-
|
95
92
|
{% if enums_in_subset %}
|
96
93
|
## Enumerations in subset
|
97
94
|
|
98
95
|
| Enumeration | Description |
|
99
96
|
| --- | --- |
|
100
97
|
{% for e in enums_in_subset|sort(attribute='name') -%}
|
101
|
-
{
|
98
|
+
{% if element.name in e.in_subset -%}
|
102
99
|
| {{ gen.link(e) }} | {{ e.description|enshorten }} |
|
103
100
|
{%- endif %}
|
104
101
|
{% endfor %}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Type: {{ gen.name(element) }} {% if element.deprecated %} <span style="color: red;"><strong> (DEPRECATED) </strong> {% endif %}
|
1
|
+
# Type: {{ gen.name(element) }} {% if element.deprecated %} <span style="color: red;"><strong> (DEPRECATED) </strong></span> {% endif %}
|
2
2
|
|
3
3
|
{% if element.description %}
|
4
4
|
{% set element_description_lines = element.description.split('\n') %}
|
@@ -15,8 +15,7 @@ URI: {{ gen.uri_link(element) }}
|
|
15
15
|
{{ gen.bullet(element, "typeof") }}
|
16
16
|
{{ gen.bullet(element, "pattern", backquote=True) }}
|
17
17
|
{% if gen.number_value_range(element) %}
|
18
|
-
* Numeric Value Range: {{gen.number_value_range(element)}}
|
18
|
+
* Numeric Value Range: {{ gen.number_value_range(element) }}
|
19
19
|
{% endif %}
|
20
20
|
|
21
21
|
{% include "common_metadata.md.jinja2" %}
|
22
|
-
|
linkml/generators/docgen.py
CHANGED
@@ -6,11 +6,11 @@ from copy import deepcopy
|
|
6
6
|
from dataclasses import dataclass, field
|
7
7
|
from enum import Enum
|
8
8
|
from pathlib import Path
|
9
|
-
from typing import Optional, Union
|
9
|
+
from typing import Any, Optional, Union
|
10
10
|
|
11
11
|
import click
|
12
12
|
from jinja2 import Environment, FileSystemLoader, Template
|
13
|
-
from linkml_runtime.dumpers import yaml_dumper
|
13
|
+
from linkml_runtime.dumpers import json_dumper, yaml_dumper
|
14
14
|
from linkml_runtime.linkml_model.meta import (
|
15
15
|
ClassDefinition,
|
16
16
|
ClassDefinitionName,
|
@@ -410,6 +410,21 @@ class DocGenerator(Generator):
|
|
410
410
|
curie = self.uri(element, expand=False)
|
411
411
|
return f"[{curie}]({uri})"
|
412
412
|
|
413
|
+
def link_mermaid(self, e: Union[Definition, DefinitionName]) -> str:
|
414
|
+
"""
|
415
|
+
Return link to insert in mermaid diagrams for a given element
|
416
|
+
|
417
|
+
:param e: element to be linked
|
418
|
+
:return: string with link
|
419
|
+
"""
|
420
|
+
# Reuse markdown link generation to avoid code duplication.
|
421
|
+
md_link = self.link(e)
|
422
|
+
if not md_link.endswith(")"):
|
423
|
+
return md_link
|
424
|
+
link = md_link.rsplit("(")[-1][:-1]
|
425
|
+
link = link.removesuffix(".md")
|
426
|
+
return f"../{link}/"
|
427
|
+
|
413
428
|
def link(self, e: Union[Definition, DefinitionName], index_link: bool = False) -> str:
|
414
429
|
"""
|
415
430
|
Render an element as a hyperlink
|
@@ -485,14 +500,13 @@ class DocGenerator(Generator):
|
|
485
500
|
return self._is_external(t) and not self.schemaview.schema.id.startswith("https://w3id.org/linkml/")
|
486
501
|
|
487
502
|
def _is_external(self, element: Element) -> bool:
|
488
|
-
# note: this is currently incomplete. See: https://github.com/linkml/linkml/issues/782
|
489
503
|
if element.from_schema == "https://w3id.org/linkml/types" and not self.genmeta:
|
490
504
|
return True
|
491
505
|
else:
|
492
506
|
return False
|
493
507
|
|
494
508
|
@staticmethod
|
495
|
-
def _markdown_link(n: str, name: str =
|
509
|
+
def _markdown_link(n: str, name: str = "", subfolder: str = "") -> str:
|
496
510
|
if subfolder:
|
497
511
|
rel_path = f"{subfolder}/{n}"
|
498
512
|
else:
|
@@ -965,6 +979,80 @@ class DocGenerator(Generator):
|
|
965
979
|
objs.append((stem, f.read()))
|
966
980
|
return objs
|
967
981
|
|
982
|
+
def _remove_name_keys(self, obj):
|
983
|
+
"""Recursively removes 'name' keys from a JSON object representation.
|
984
|
+
|
985
|
+
This is used to clean up the output of ClassRule objects. For example, if we had a rule like:
|
986
|
+
rules:
|
987
|
+
- title: calibration_standard_if_rt
|
988
|
+
preconditions:
|
989
|
+
slot_conditions:
|
990
|
+
calibration_target:
|
991
|
+
equals_string: retention_index
|
992
|
+
|
993
|
+
The JSON representation would include redundant "name" keys:
|
994
|
+
{
|
995
|
+
"slot_conditions": {
|
996
|
+
"name": "slot_conditions"
|
997
|
+
"calibration_target": {
|
998
|
+
"name": "calibration_target"
|
999
|
+
"equals_string": "retention_index",
|
1000
|
+
},
|
1001
|
+
}
|
1002
|
+
}
|
1003
|
+
|
1004
|
+
This function removes those redundant "name" keys to make the output cleaner.
|
1005
|
+
|
1006
|
+
:param obj: The object to clean (dict, list, or primitive value)
|
1007
|
+
:return: The same object with all "name" keys removed from dicts
|
1008
|
+
"""
|
1009
|
+
if isinstance(obj, dict):
|
1010
|
+
return {k: self._remove_name_keys(v) for k, v in obj.items() if k != "name"}
|
1011
|
+
elif isinstance(obj, list):
|
1012
|
+
return [self._remove_name_keys(item) for item in obj]
|
1013
|
+
else:
|
1014
|
+
return obj
|
1015
|
+
|
1016
|
+
def classrule_to_dict_view(self, element: ClassDefinition) -> list[dict[str, Any]]:
|
1017
|
+
"""Process all rules (of type ClassRule) asserted on a class.
|
1018
|
+
|
1019
|
+
This method iterates through all rules asserted on a class and returns a list of
|
1020
|
+
dictionaries, each containing four pieces of information about a rule:
|
1021
|
+
_title_, _preconditions_, _postconditions_, and _elseconditions_.
|
1022
|
+
These values will be read in the jinja template and formatted into a tabular
|
1023
|
+
view for users to understand the rules applied to the class.
|
1024
|
+
|
1025
|
+
Note: This method removes redundant "name" keys which are asserted on some
|
1026
|
+
classes in the Python representation of classes from the metamodel.
|
1027
|
+
|
1028
|
+
:param element: LinkML class object with `rules` asserted on it
|
1029
|
+
:return: List of dictionaries with title and "sanitized" conditions for each rule
|
1030
|
+
"""
|
1031
|
+
if not element.rules:
|
1032
|
+
return []
|
1033
|
+
|
1034
|
+
rule_dicts = []
|
1035
|
+
|
1036
|
+
for rule in element.rules:
|
1037
|
+
# TODO: expand this list of ClassRule metaslots based on use case
|
1038
|
+
rule_dict = {
|
1039
|
+
"title": rule.title or "",
|
1040
|
+
"preconditions": None,
|
1041
|
+
"postconditions": None,
|
1042
|
+
"elseconditions": None,
|
1043
|
+
}
|
1044
|
+
|
1045
|
+
for key in ["preconditions", "postconditions", "elseconditions"]:
|
1046
|
+
condition_obj = getattr(rule, key, None)
|
1047
|
+
if condition_obj:
|
1048
|
+
json_obj = json_dumper.to_dict(condition_obj)
|
1049
|
+
sanitized_condition = self._remove_name_keys(json_obj)
|
1050
|
+
rule_dict[key] = sanitized_condition
|
1051
|
+
|
1052
|
+
rule_dicts.append(rule_dict)
|
1053
|
+
|
1054
|
+
return rule_dicts
|
1055
|
+
|
968
1056
|
def customize_environment(self, env: Environment):
|
969
1057
|
if self.truncate_descriptions:
|
970
1058
|
env.filters["enshorten"] = enshorten
|
@@ -23,7 +23,7 @@ class Attribute(pydantic.BaseModel):
|
|
23
23
|
|
24
24
|
def __str__(self):
|
25
25
|
cmt = f'"{self.comment}"' if self.comment else ""
|
26
|
-
return f
|
26
|
+
return f" {self.datatype} {self.name} {self.key if self.key else ''} {cmt}"
|
27
27
|
|
28
28
|
|
29
29
|
class IdentifyingType(str, Enum):
|
linkml/generators/graphqlgen.py
CHANGED
@@ -66,7 +66,7 @@ class GraphqlGenerator(Generator):
|
|
66
66
|
for value in enum.permissible_values:
|
67
67
|
permissible_values.append(self.name_compatiblity.compatible(value))
|
68
68
|
values = "\n ".join(permissible_values)
|
69
|
-
return f"enum {camelcase(enum.name).replace(' ','')}\n {{\n {values}\n }}\n\n"
|
69
|
+
return f"enum {camelcase(enum.name).replace(' ', '')}\n {{\n {values}\n }}\n\n"
|
70
70
|
else:
|
71
71
|
logging.warning(
|
72
72
|
f"Enumeration {enum.name} using `reachable_from` instead of `permissible_values` "
|
@@ -197,7 +197,6 @@ class ContextGenerator(Generator):
|
|
197
197
|
elif not uri_prefix or is_default_namespace:
|
198
198
|
definition["@id"] = uri_suffix
|
199
199
|
else:
|
200
|
-
|
201
200
|
definition["@id"] = (uri_prefix + ":" + uri_suffix) if uri_prefix else uri
|
202
201
|
|
203
202
|
if uri_prefix and not is_default_namespace:
|
linkml/generators/jsonldgen.py
CHANGED
@@ -106,7 +106,9 @@ class JSONLDGenerator(Generator):
|
|
106
106
|
else (
|
107
107
|
SubsetDefinitionName(camelcase(node))
|
108
108
|
if node in self.schema.subsets
|
109
|
-
else TypeDefinitionName(underscore(node))
|
109
|
+
else TypeDefinitionName(underscore(node))
|
110
|
+
if node in self.schema.types
|
111
|
+
else None
|
110
112
|
)
|
111
113
|
)
|
112
114
|
)
|
@@ -694,8 +694,8 @@ class JsonSchemaGenerator(Generator, LifecycleMixin):
|
|
694
694
|
if self.materialize_patterns:
|
695
695
|
logger.info("Materializing patterns in the schema before serialization")
|
696
696
|
self.schemaview.materialize_patterns()
|
697
|
-
|
698
|
-
return
|
697
|
+
result = self.generate().to_json(sort_keys=True, indent=self.indent if self.indent > 0 else None)
|
698
|
+
return result.rstrip() + "\n"
|
699
699
|
|
700
700
|
|
701
701
|
@shared_arguments(JsonSchemaGenerator)
|
linkml/generators/linkmlgen.py
CHANGED
@@ -73,7 +73,7 @@ class LinkmlGenerator(Generator):
|
|
73
73
|
return yaml_str
|
74
74
|
else:
|
75
75
|
raise ValueError(
|
76
|
-
f"{self.format} is an invalid format. Use one of the following
|
76
|
+
f"{self.format} is an invalid format. Use one of the following formats: {self.valid_formats}"
|
77
77
|
)
|
78
78
|
|
79
79
|
|
linkml/generators/markdowngen.py
CHANGED
@@ -149,6 +149,7 @@ class MarkdownGenerator(Generator):
|
|
149
149
|
items = [i for i in items if i is not None]
|
150
150
|
out = "\n".join(items) + "\n"
|
151
151
|
out = pad_heading(out)
|
152
|
+
out = out.rstrip() + "\n"
|
152
153
|
ixfile.write(out)
|
153
154
|
return out
|
154
155
|
|
@@ -179,7 +180,7 @@ class MarkdownGenerator(Generator):
|
|
179
180
|
yg = YumlGenerator(self)
|
180
181
|
img_url = (
|
181
182
|
yg.serialize(classes=[cls.name]).replace("?", "%3F").replace(" ", "%20").replace("|", "|")
|
182
|
-
)
|
183
|
+
).rstrip()
|
183
184
|
|
184
185
|
items.append(f"[]({img_url})")
|
185
186
|
|
@@ -204,7 +205,7 @@ class MarkdownGenerator(Generator):
|
|
204
205
|
if cls.name in self.synopsis.mixinrefs:
|
205
206
|
items.append(self.header(2, f"{mixin_local_name} for"))
|
206
207
|
for mixin in sorted(self.synopsis.mixinrefs[cls.name].classrefs):
|
207
|
-
items.append(self.bullet(f
|
208
|
+
items.append(self.bullet(f"{self.class_link(mixin, use_desc=True, after_link='(mixin)')}"))
|
208
209
|
|
209
210
|
if cls.name in self.synopsis.classrefs:
|
210
211
|
items.append(self.header(2, f"Referenced by {class_local_name}"))
|
@@ -253,6 +254,7 @@ class MarkdownGenerator(Generator):
|
|
253
254
|
items.append(self.element_properties(cls))
|
254
255
|
out = "\n".join(items)
|
255
256
|
out = pad_heading(out)
|
257
|
+
out = out.rstrip() + "\n"
|
256
258
|
clsfile.write(out)
|
257
259
|
return out
|
258
260
|
|
@@ -272,6 +274,7 @@ class MarkdownGenerator(Generator):
|
|
272
274
|
out += self.element_properties(typ)
|
273
275
|
out += "\n"
|
274
276
|
out = pad_heading(out)
|
277
|
+
out = out.rstrip() + "\n"
|
275
278
|
typefile.write(out)
|
276
279
|
return out
|
277
280
|
|
@@ -312,6 +315,7 @@ class MarkdownGenerator(Generator):
|
|
312
315
|
items.append(self.element_properties(slot))
|
313
316
|
out = "\n".join(items)
|
314
317
|
out = pad_heading(out)
|
318
|
+
out = out.rstrip() + "\n"
|
315
319
|
slotfile.write(out)
|
316
320
|
return out
|
317
321
|
|
@@ -324,6 +328,7 @@ class MarkdownGenerator(Generator):
|
|
324
328
|
items.append(self.element_properties(enum))
|
325
329
|
out = "\n".join(items)
|
326
330
|
out = pad_heading(out)
|
331
|
+
out = out.rstrip() + "\n"
|
327
332
|
enumfile.write(out)
|
328
333
|
return out
|
329
334
|
|
@@ -359,6 +364,7 @@ class MarkdownGenerator(Generator):
|
|
359
364
|
items.append(self.element_properties(subset))
|
360
365
|
out = "\n".join(items)
|
361
366
|
out = pad_heading(out)
|
367
|
+
out = out.rstrip() + "\n"
|
362
368
|
subsetfile.write(out)
|
363
369
|
return out
|
364
370
|
|
@@ -513,7 +519,9 @@ class MarkdownGenerator(Generator):
|
|
513
519
|
else (
|
514
520
|
underscore(obj.name)
|
515
521
|
if isinstance(obj, SlotDefinition)
|
516
|
-
else underscore(obj.name)
|
522
|
+
else underscore(obj.name)
|
523
|
+
if isinstance(obj, EnumDefinition)
|
524
|
+
else camelcase(obj.name)
|
517
525
|
)
|
518
526
|
)
|
519
527
|
subdir = "/types" if isinstance(obj, TypeDefinition) and not self.no_types_dir else ""
|
@@ -548,7 +556,7 @@ class MarkdownGenerator(Generator):
|
|
548
556
|
for example in slot.examples:
|
549
557
|
items.append(
|
550
558
|
self.bullet(
|
551
|
-
f
|
559
|
+
f"Example: {getattr(example, 'value', ' ')} {getattr(example, 'description', ' ')}",
|
552
560
|
level=1,
|
553
561
|
)
|
554
562
|
)
|
@@ -599,7 +607,7 @@ class MarkdownGenerator(Generator):
|
|
599
607
|
|
600
608
|
def header(self, level: int, txt: str) -> str:
|
601
609
|
txt = self.get_metamodel_slot_name(txt)
|
602
|
-
out = f
|
610
|
+
out = f"\n{'#' * level} {txt}\n"
|
603
611
|
return out
|
604
612
|
|
605
613
|
@staticmethod
|
@@ -608,7 +616,7 @@ class MarkdownGenerator(Generator):
|
|
608
616
|
|
609
617
|
@staticmethod
|
610
618
|
def bullet(txt: str, level=0) -> str:
|
611
|
-
return f'
|
619
|
+
return f"{' ' * level} * {txt}"
|
612
620
|
|
613
621
|
def frontmatter(self, thingtype: str, layout="default") -> str:
|
614
622
|
return self.header(1, thingtype)
|
@@ -674,9 +682,12 @@ class MarkdownGenerator(Generator):
|
|
674
682
|
link_name = obj.name
|
675
683
|
link_ref = link_name
|
676
684
|
desc = self.desc_for(obj, use_desc)
|
677
|
-
return
|
678
|
-
f"
|
679
|
-
|
685
|
+
return (
|
686
|
+
f"[{link_name}]"
|
687
|
+
f"({link_ref}.{self.format})"
|
688
|
+
+ (f" {after_link} " if after_link else "")
|
689
|
+
+ (f" - {desc.split(nl)[0]}" if desc else "")
|
690
|
+
).rstrip()
|
680
691
|
|
681
692
|
def type_link(
|
682
693
|
self,
|
@@ -90,6 +90,10 @@ class MermaidClassDiagramGenerator(Generator):
|
|
90
90
|
"""Returns the canonical name for an element."""
|
91
91
|
return element.name
|
92
92
|
|
93
|
+
def link_mermaid(self, element):
|
94
|
+
"""Generates a link for the given element."""
|
95
|
+
return f"../{self.name(element)}"
|
96
|
+
|
93
97
|
def all_type_object_names(self):
|
94
98
|
return list(self.schemaview.all_types().keys())
|
95
99
|
|
linkml/generators/projectgen.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
import logging
|
2
|
-
import os
|
3
2
|
from collections import defaultdict
|
4
3
|
from dataclasses import dataclass, field
|
5
4
|
from functools import lru_cache
|
@@ -45,7 +44,7 @@ GEN_MAP = {
|
|
45
44
|
"markdown": (
|
46
45
|
MarkdownGenerator,
|
47
46
|
"docs/",
|
48
|
-
{"directory": "{parent}", "index_file": "{name}.md"},
|
47
|
+
{"directory": "{parent}/docs", "index_file": "{name}.md"},
|
49
48
|
),
|
50
49
|
"owl": (OwlSchemaGenerator, "owl/{name}.owl.ttl", {}),
|
51
50
|
"prefixmap": (PrefixGenerator, "prefixmap/{name}.yaml", {}),
|
@@ -64,17 +63,16 @@ GEN_MAP = {
|
|
64
63
|
|
65
64
|
|
66
65
|
@lru_cache
|
67
|
-
def get_local_imports(schema_path:
|
66
|
+
def get_local_imports(schema_path: Path, dir: Path):
|
68
67
|
logger.info(f"GETTING IMPORTS = {schema_path}")
|
69
68
|
all_imports = [schema_path]
|
70
|
-
with open(schema_path) as stream:
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
all_imports += get_local_imports(imp_path, dir)
|
69
|
+
with open(schema_path, encoding="utf-8") as stream:
|
70
|
+
schema = yaml.safe_load(stream)
|
71
|
+
for imp in schema.get("imports", []):
|
72
|
+
imp_path = dir / f"{imp}.yaml"
|
73
|
+
logger.info(f" IMP={imp} // path={imp_path}")
|
74
|
+
if imp_path.is_file():
|
75
|
+
all_imports += get_local_imports(imp_path, dir)
|
78
76
|
return all_imports
|
79
77
|
|
80
78
|
|
@@ -102,11 +100,12 @@ class ProjectGenerator:
|
|
102
100
|
def generate(schema_path: str, config: ProjectConfiguration = ProjectConfiguration()):
|
103
101
|
if config.directory is None:
|
104
102
|
raise Exception("Must pass directory")
|
105
|
-
Path(config.directory)
|
103
|
+
output_dir = Path(config.directory)
|
104
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
106
105
|
if config.mergeimports:
|
107
106
|
all_schemas = [schema_path]
|
108
107
|
else:
|
109
|
-
all_schemas = get_local_imports(schema_path,
|
108
|
+
all_schemas = get_local_imports(schema_path, Path(schema_path).parent)
|
110
109
|
logger.debug(f"ALL_SCHEMAS = {all_schemas}")
|
111
110
|
for gen_name, (gen_cls, gen_path_fmt, default_gen_args) in GEN_MAP.items():
|
112
111
|
if config.includes is not None and config.includes != [] and gen_name not in config.includes:
|
@@ -118,14 +117,12 @@ class ProjectGenerator:
|
|
118
117
|
logger.info(f"Generating: {gen_name}")
|
119
118
|
for local_path in all_schemas:
|
120
119
|
logger.info(f" SCHEMA: {local_path}")
|
121
|
-
name =
|
120
|
+
name = Path(local_path).stem
|
122
121
|
gen_path = gen_path_fmt.format(name=name)
|
123
|
-
gen_path_full =
|
124
|
-
|
125
|
-
parent_dir = "/".join(parts[0:-1])
|
122
|
+
gen_path_full = output_dir / gen_path
|
123
|
+
parent_dir = gen_path_full.parent
|
126
124
|
logger.info(f" PARENT={parent_dir}")
|
127
|
-
|
128
|
-
gen_path_full = "/".join(parts)
|
125
|
+
parent_dir.mkdir(parents=True, exist_ok=True)
|
129
126
|
all_gen_args = {
|
130
127
|
**default_gen_args,
|
131
128
|
**config.generator_args.get(gen_name, {}),
|
@@ -149,7 +146,7 @@ class ProjectGenerator:
|
|
149
146
|
gen_dump = gen.serialize(**serialize_args)
|
150
147
|
|
151
148
|
if gen_name != "excel":
|
152
|
-
if
|
149
|
+
if gen_path_full.suffix != "":
|
153
150
|
# markdowngen does not write to a file
|
154
151
|
logger.info(f" WRITING TO: {gen_path_full}")
|
155
152
|
with open(gen_path_full, "w", encoding="UTF-8") as stream:
|
@@ -6,17 +6,17 @@ LinkMLMeta = """
|
|
6
6
|
class LinkMLMeta(RootModel):
|
7
7
|
root: dict[str, Any] = {}
|
8
8
|
model_config = ConfigDict(frozen=True)
|
9
|
-
|
9
|
+
|
10
10
|
def __getattr__(self, key:str):
|
11
11
|
return getattr(self.root, key)
|
12
|
-
|
12
|
+
|
13
13
|
def __getitem__(self, key:str):
|
14
14
|
return self.root[key]
|
15
|
-
|
15
|
+
|
16
16
|
def __setitem__(self, key:str, value):
|
17
17
|
self.root[key] = value
|
18
|
-
|
18
|
+
|
19
19
|
def __contains__(self, key:str) -> bool:
|
20
20
|
return key in self.root
|
21
|
-
|
21
|
+
|
22
22
|
"""
|
@@ -832,8 +832,7 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
|
|
832
832
|
meta = remove_empty_items(source)
|
833
833
|
else:
|
834
834
|
raise ValueError(
|
835
|
-
f"Unknown metadata mode '{self.metadata_mode}', needs to be one of "
|
836
|
-
f"{[mode for mode in MetadataMode]}"
|
835
|
+
f"Unknown metadata mode '{self.metadata_mode}', needs to be one of {[mode for mode in MetadataMode]}"
|
837
836
|
)
|
838
837
|
|
839
838
|
model.meta = meta
|
@@ -596,7 +596,7 @@ class Imports(PydanticTemplateModel):
|
|
596
596
|
else:
|
597
597
|
return all([obj in an_import.objects for obj in item.objects])
|
598
598
|
else:
|
599
|
-
raise TypeError("Imports only contains single Import objects or other Imports\
|
599
|
+
raise TypeError(f"Imports only contains single Import objects or other Imports\nGot: {type(item)}")
|
600
600
|
|
601
601
|
@field_validator("imports", mode="after")
|
602
602
|
@classmethod
|
@@ -57,7 +57,7 @@ class PythonIfAbsentProcessor(IfAbsentProcessor):
|
|
57
57
|
slot: SlotDefinition,
|
58
58
|
cls: ClassDefinition,
|
59
59
|
):
|
60
|
-
return f"datetime({int(year)}, {int(month)}, {int(day)},
|
60
|
+
return f"datetime({int(year)}, {int(month)}, {int(day)}, {int(hour)}, {int(minutes)}, {int(seconds)})"
|
61
61
|
|
62
62
|
def map_uri_or_curie_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
|
63
63
|
return self._uri_for(default_value)
|
linkml/generators/pythongen.py
CHANGED
@@ -474,7 +474,7 @@ version = {'"' + self.schema.version + '"' if self.schema.version else None}
|
|
474
474
|
def gen_classdef(self, cls: ClassDefinition) -> str:
|
475
475
|
"""Generate python definition for class cls"""
|
476
476
|
|
477
|
-
parentref = f
|
477
|
+
parentref = f"({self.formatted_element_name(cls.is_a, True) if cls.is_a else 'YAMLRoot'})"
|
478
478
|
slotdefs = self.gen_class_variables(cls)
|
479
479
|
postinits = self.gen_postinits(cls)
|
480
480
|
constructor = self.gen_constructor(cls)
|
@@ -833,8 +833,7 @@ version = {'"' + self.schema.version + '"' if self.schema.version else None}
|
|
833
833
|
elif slot_range == "uri":
|
834
834
|
lookup_by_props = ["class_class_uri", "class_model_uri"]
|
835
835
|
td_val_expression = (
|
836
|
-
f"URIRef({td_val_expression}) if "
|
837
|
-
f"isinstance({td_val_expression}, str) else {td_val_expression}"
|
836
|
+
f"URIRef({td_val_expression}) if isinstance({td_val_expression}, str) else {td_val_expression}"
|
838
837
|
)
|
839
838
|
elif slot_range == "uriorcurie":
|
840
839
|
lookup_by_props = ["class_class_curie", "class_class_uri", "class_model_uri"]
|
@@ -917,8 +916,7 @@ version = {'"' + self.schema.version + '"' if self.schema.version else None}
|
|
917
916
|
rlines.append(f"self.{aliased_slot_name} = str(self.{td_value_classvar})")
|
918
917
|
elif (
|
919
918
|
# A really weird case -- a class that has no properties
|
920
|
-
slot.range in self.schema.classes
|
921
|
-
and not self.schema.classes[slot.range].slots
|
919
|
+
slot.range in self.schema.classes and not self.schema.classes[slot.range].slots
|
922
920
|
):
|
923
921
|
rlines.append(f"\tself.{aliased_slot_name} = {base_type_name}()")
|
924
922
|
else:
|
@@ -985,7 +983,7 @@ version = {'"' + self.schema.version + '"' if self.schema.version else None}
|
|
985
983
|
sn = f"self.{aliased_slot_name}"
|
986
984
|
rlines.append(f"if not isinstance({sn}, list):")
|
987
985
|
rlines.append(f"\t{sn} = [{sn}] if {sn} is not None else []")
|
988
|
-
rlines.append(f"{sn} = [v if isinstance(v, {base_type_name})
|
986
|
+
rlines.append(f"{sn} = [v if isinstance(v, {base_type_name}) else {base_type_name}(v) for v in {sn}]")
|
989
987
|
while rlines and copy(rlines[-1]).strip() == "":
|
990
988
|
rlines.pop()
|
991
989
|
rlines.append("")
|
@@ -1213,6 +1211,7 @@ class {enum_name}(EnumDefinitionImpl):
|
|
1213
1211
|
PermissibleValue(text="NAME_ONLY")
|
1214
1212
|
PermissibleValue(
|
1215
1213
|
text="CODE",
|
1214
|
+
title="...",
|
1216
1215
|
description="...",
|
1217
1216
|
meaning="...")
|
1218
1217
|
|
@@ -1223,13 +1222,15 @@ class {enum_name}(EnumDefinitionImpl):
|
|
1223
1222
|
constructor = "PermissibleValue"
|
1224
1223
|
pv_text = pv.text.replace('"', '\\"')
|
1225
1224
|
|
1226
|
-
if not pv.description and not pv.meaning:
|
1225
|
+
if not pv.description and not pv.meaning and not pv.title:
|
1227
1226
|
return f'{constructor}(text="{pv_text}")'
|
1228
1227
|
|
1229
1228
|
indent_str = (4 + indent) * " "
|
1230
1229
|
pv_attrs = [f'{indent_str}text="{pv_text}"']
|
1230
|
+
if pv.title:
|
1231
|
+
pv_attrs.append(f'{indent_str}title="{pv.title}"')
|
1231
1232
|
if pv.description:
|
1232
|
-
pv_attrs.append(f
|
1233
|
+
pv_attrs.append(f"{self.process_multiline_string(pv.description, f'{indent_str}description=')}")
|
1233
1234
|
if pv.meaning:
|
1234
1235
|
pv_meaning = self.namespaces.curie_for(
|
1235
1236
|
self.namespaces.uri_for(pv.meaning), default_ok=False, pythonform=True
|
@@ -6,7 +6,6 @@ from linkml.generators.shacl.shacl_data_type import ShaclDataType
|
|
6
6
|
|
7
7
|
|
8
8
|
class ShaclIfAbsentProcessor(IfAbsentProcessor):
|
9
|
-
|
10
9
|
def map_custom_default_values(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition) -> (bool, str):
|
11
10
|
if default_value == "class_curie":
|
12
11
|
class_uri = self.schema_view.get_uri(cls, expand=True)
|
linkml/generators/shaclgen.py
CHANGED
@@ -141,7 +141,7 @@ class ShaclGenerator(Generator):
|
|
141
141
|
# slot definition, as both are mapped to sh:in in SHACL
|
142
142
|
if s.equals_string or s.equals_string_in:
|
143
143
|
error = "'equals_string'/'equals_string_in' and 'any_of' are mutually exclusive"
|
144
|
-
raise ValueError(f
|
144
|
+
raise ValueError(f"{TypedNode.yaml_loc(str(s), suffix='')} {error}")
|
145
145
|
|
146
146
|
or_node = BNode()
|
147
147
|
prop_pv(SH["or"], or_node)
|