linkml 1.7.5__py3-none-any.whl → 1.7.6__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 (41) hide show
  1. linkml/generators/__init__.py +2 -0
  2. linkml/generators/docgen/class.md.jinja2 +15 -2
  3. linkml/generators/docgen/slot.md.jinja2 +14 -1
  4. linkml/generators/docgen.py +17 -3
  5. linkml/generators/jsonldcontextgen.py +9 -6
  6. linkml/generators/jsonldgen.py +3 -1
  7. linkml/generators/owlgen.py +14 -0
  8. linkml/generators/prefixmapgen.py +5 -4
  9. linkml/generators/projectgen.py +14 -2
  10. linkml/generators/pydanticgen/__init__.py +29 -0
  11. linkml/generators/{pydanticgen.py → pydanticgen/pydanticgen.py} +192 -299
  12. linkml/generators/pydanticgen/template.py +504 -0
  13. linkml/generators/pydanticgen/templates/attribute.py.jinja +10 -0
  14. linkml/generators/pydanticgen/templates/base_model.py.jinja +27 -0
  15. linkml/generators/pydanticgen/templates/class.py.jinja +21 -0
  16. linkml/generators/pydanticgen/templates/conditional_import.py.jinja +9 -0
  17. linkml/generators/pydanticgen/templates/enum.py.jinja +16 -0
  18. linkml/generators/pydanticgen/templates/footer.py.jinja +13 -0
  19. linkml/generators/pydanticgen/templates/imports.py.jinja +31 -0
  20. linkml/generators/pydanticgen/templates/module.py.jinja +27 -0
  21. linkml/generators/pydanticgen/templates/validator.py.jinja +15 -0
  22. linkml/generators/pythongen.py +13 -7
  23. linkml/generators/shacl/__init__.py +3 -0
  24. linkml/generators/shacl/ifabsent_processor.py +41 -0
  25. linkml/generators/shacl/shacl_data_type.py +40 -0
  26. linkml/generators/shaclgen.py +105 -82
  27. linkml/generators/shexgen.py +1 -1
  28. linkml/generators/sqlalchemygen.py +1 -1
  29. linkml/generators/sqltablegen.py +32 -22
  30. linkml/generators/terminusdbgen.py +7 -1
  31. linkml/linter/config/datamodel/config.py +8 -0
  32. linkml/linter/rules.py +11 -2
  33. linkml/utils/generator.py +7 -6
  34. linkml/utils/ifabsent_functions.py +7 -9
  35. linkml/utils/schemaloader.py +1 -9
  36. linkml/utils/sqlutils.py +39 -25
  37. {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/METADATA +4 -1
  38. {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/RECORD +41 -27
  39. {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/LICENSE +0 -0
  40. {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/WHEEL +0 -0
  41. {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/entry_points.txt +0 -0
@@ -36,11 +36,7 @@ from rdflib import URIRef
36
36
  import linkml
37
37
  from linkml._version import __version__
38
38
  from linkml.utils.generator import Generator, shared_arguments
39
- from linkml.utils.ifabsent_functions import (
40
- default_curie_or_uri,
41
- ifabsent_postinit_declaration,
42
- ifabsent_value_declaration,
43
- )
39
+ from linkml.utils.ifabsent_functions import ifabsent_postinit_declaration, ifabsent_value_declaration
44
40
 
45
41
 
46
42
  @dataclass
@@ -67,7 +63,7 @@ class PythonGenerator(Generator):
67
63
 
68
64
  def __post_init__(self) -> None:
69
65
  self.sourcefile = self.schema
70
- self.schemaview = SchemaView(self.schema)
66
+ self.schemaview = SchemaView(self.schema, base_dir=self.base_dir)
71
67
  super().__post_init__()
72
68
  if self.format is None:
73
69
  self.format = self.valid_formats[0]
@@ -151,6 +147,7 @@ import re
151
147
  from jsonasobj2 import JsonObj, as_dict
152
148
  from typing import Optional, List, Union, Dict, ClassVar, Any
153
149
  from dataclasses import dataclass
150
+ from datetime import date, datetime
154
151
  {enumimports}
155
152
  from linkml_runtime.utils.slot import Slot
156
153
  from linkml_runtime.utils.metamodelcore import empty_list, empty_dict, bnode
@@ -294,7 +291,7 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
294
291
  return rval.values()
295
292
 
296
293
  def gen_namespaces(self) -> str:
297
- dflt_prefix = default_curie_or_uri(self)
294
+ dflt_prefix = self._default_curie_or_uri()
298
295
  dflt = f"CurieNamespace('', '{sfx(dflt_prefix)}')" if ":/" in dflt_prefix else dflt_prefix.upper()
299
296
  curienamespace_defs = [
300
297
  {
@@ -641,6 +638,7 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
641
638
  for slot in self.domain_slots(cls):
642
639
  if slot.ifabsent:
643
640
  dflt = ifabsent_postinit_declaration(slot.ifabsent, self, cls, slot)
641
+
644
642
  if dflt and dflt != "None":
645
643
  post_inits_pre_super.append(f"if self.{self.slot_name(slot.name)} is None:")
646
644
  post_inits_pre_super.append(f"\tself.{self.slot_name(slot.name)} = {dflt}")
@@ -1170,6 +1168,14 @@ class {enum_name}(EnumDefinitionImpl):
1170
1168
 
1171
1169
  return f'{prefix_string}"""{string}"""'
1172
1170
 
1171
+ def _default_curie_or_uri(self) -> str:
1172
+ dflt = self.schema.default_prefix if self.schema.default_prefix else sfx(self.schema.id)
1173
+ if ":/" in dflt:
1174
+ prefix = self.namespaces.prefix_for(self.schema.default_prefix)
1175
+ if prefix:
1176
+ dflt = prefix
1177
+ return dflt
1178
+
1173
1179
 
1174
1180
  @shared_arguments(PythonGenerator)
1175
1181
  @click.command()
@@ -0,0 +1,3 @@
1
+ from linkml.generators.shacl import ifabsent_processor
2
+
3
+ __all__ = ["shacl_data_type", "ifabsent_processor"]
@@ -0,0 +1,41 @@
1
+ import re
2
+ from typing import Any, Callable
3
+
4
+ from linkml_runtime import SchemaView
5
+ from linkml_runtime.linkml_model import SlotDefinition
6
+ from rdflib import SH, Literal, URIRef
7
+ from rdflib.term import Identifier
8
+
9
+ from linkml.generators.shacl.shacl_data_type import ShaclDataType
10
+
11
+
12
+ class IfAbsentProcessor:
13
+
14
+ ifabsent_regex = re.compile("""(?:(?P<type>\w+)\()?[\"\']?(?P<default_value>[^\(\)\"\')]*)[\"\']?\)?""")
15
+
16
+ def __init__(self, schema_view: SchemaView):
17
+ self.schema_view = schema_view
18
+
19
+ def process_slot(self, add_prop: Callable[[URIRef, Identifier], None], slot: SlotDefinition):
20
+ if slot.ifabsent:
21
+ ifabsent_match = self.ifabsent_regex.search(slot.ifabsent)
22
+ ifabsent_default_value = ifabsent_match.group("default_value")
23
+
24
+ self._map_to_default_value(slot, add_prop, ifabsent_default_value)
25
+
26
+ def _map_to_default_value(
27
+ self, slot: SlotDefinition, add_prop: Callable[[URIRef, Identifier], None], ifabsent_default_value: Any
28
+ ) -> None:
29
+ for datatype in list(ShaclDataType):
30
+ if datatype.linkml_type == slot.range:
31
+ add_prop(SH.defaultValue, Literal(ifabsent_default_value, datatype=datatype.uri_ref))
32
+ return
33
+
34
+ for enum_name, enum in self.schema_view.all_enums().items():
35
+ if enum_name == slot.range:
36
+ for permissible_value_name, permissible_value in enum.permissible_values.items():
37
+ if permissible_value_name == ifabsent_default_value:
38
+ add_prop(SH.defaultValue, Literal(ifabsent_default_value))
39
+ return
40
+
41
+ raise ValueError(f"The ifabsent value `{slot.ifabsent}` of the `{slot.name}` slot could not be processed")
@@ -0,0 +1,40 @@
1
+ from enum import Enum
2
+
3
+ from rdflib import URIRef
4
+
5
+
6
+ class DataType:
7
+ linkml_type: str
8
+ uri_ref: URIRef
9
+
10
+
11
+ class ShaclDataType(DataType, Enum):
12
+ STRING = ("string", URIRef("http://www.w3.org/2001/XMLSchema#string"))
13
+ BOOLEAN = ("boolean", URIRef("http://www.w3.org/2001/XMLSchema#boolean"))
14
+ DURATION = ("duration", URIRef("http://www.w3.org/2001/XMLSchema#duration"))
15
+ DATETIME = ("datetime", URIRef("http://www.w3.org/2001/XMLSchema#dateTime"))
16
+ DATE = ("date", URIRef("http://www.w3.org/2001/XMLSchema#date"))
17
+ TIME = ("time", URIRef("http://www.w3.org/2001/XMLSchema#time"))
18
+ DECIMAL = ("decimal", URIRef("http://www.w3.org/2001/XMLSchema#decimal"))
19
+ INTEGER = ("integer", URIRef("http://www.w3.org/2001/XMLSchema#integer"))
20
+ FLOAT = ("float", URIRef("http://www.w3.org/2001/XMLSchema#float"))
21
+ DOUBLE = ("double", URIRef("http://www.w3.org/2001/XMLSchema#double"))
22
+ URI = ("uri", URIRef("http://www.w3.org/2001/XMLSchema#anyURI"))
23
+ CURI = ("curi", URIRef("http://www.w3.org/2001/XMLSchema#string"))
24
+ NCNAME = ("ncname", URIRef("http://www.w3.org/2001/XMLSchema#string"))
25
+ OBJECT_IDENTIFIER = ("objectidentifier", URIRef("http://www.w3.org/ns/shex#iri"))
26
+ NODE_IDENTIFIER = ("nodeidentifier", URIRef("http://www.w3.org/ns/shex#nonLiteral"))
27
+ JSON_POINTER = ("jsonpointer", URIRef("http://www.w3.org/2001/XMLSchema#string"))
28
+ JSON_PATH = ("jsonpath", URIRef("http://www.w3.org/2001/XMLSchema#string"))
29
+ SPARQL_PATH = ("sparqlpath", URIRef("http://www.w3.org/2001/XMLSchema#string"))
30
+
31
+ def __new__(cls, linkml_type, uri_ref):
32
+ obj = object.__new__(cls)
33
+ obj.linkml_type = linkml_type
34
+ obj.uri_ref = uri_ref
35
+
36
+ return obj
37
+
38
+ def __init__(self, linkml_type, uri_ref):
39
+ self.linkml_type = linkml_type
40
+ self.uri_ref = uri_ref
@@ -1,8 +1,10 @@
1
1
  import logging
2
2
  import os
3
3
  from dataclasses import dataclass
4
+ from typing import Callable
4
5
 
5
6
  import click
7
+ from linkml_runtime.linkml_model.meta import ElementName
6
8
  from linkml_runtime.utils.formatutils import underscore
7
9
  from linkml_runtime.utils.schemaview import SchemaView
8
10
  from rdflib import BNode, Graph, Literal, URIRef
@@ -10,23 +12,10 @@ from rdflib.collection import Collection
10
12
  from rdflib.namespace import RDF, SH
11
13
 
12
14
  from linkml._version import __version__
15
+ from linkml.generators.shacl.ifabsent_processor import IfAbsentProcessor
16
+ from linkml.generators.shacl.shacl_data_type import ShaclDataType
13
17
  from linkml.utils.generator import Generator, shared_arguments
14
18
 
15
- LINK_ML_TYPES_STRING = URIRef("http://www.w3.org/2001/XMLSchema#string")
16
- LINK_ML_TYPES_BOOL = URIRef("http://www.w3.org/2001/XMLSchema#boolean")
17
- LINK_ML_TYPES_DECIMAL = URIRef("http://www.w3.org/2001/XMLSchema#decimal")
18
- LINK_ML_TYPES_INTEGER = URIRef("http://www.w3.org/2001/XMLSchema#integer")
19
- LINK_ML_TYPES_FLOAT = URIRef("http://www.w3.org/2001/XMLSchema#float")
20
- LINK_ML_TYPES_DOUBLE = URIRef("http://www.w3.org/2001/XMLSchema#double")
21
- LINK_ML_TYPES_DURATION = URIRef("http://www.w3.org/2001/XMLSchema#duration")
22
- LINK_ML_TYPES_DATETIME = URIRef("http://www.w3.org/2001/XMLSchema#dateTime")
23
- LINK_ML_TYPES_DATE = URIRef("http://www.w3.org/2001/XMLSchema#date")
24
- LINK_ML_TYPES_TIME = URIRef("http://www.w3.org/2001/XMLSchema#time")
25
- LINK_ML_TYPES_DATE_TIME = URIRef("http://www.w3.org/2001/XMLSchema#datetime")
26
- LINK_ML_TYPES_URI = URIRef("http://www.w3.org/2001/XMLSchema#anyURI")
27
- LINK_ML_TYPES_OBJECT_ID = URIRef("http://www.w3.org/ns/shex#iri")
28
- LINK_ML_TYPES_NODE_ID = URIRef("http://www.w3.org/ns/shex#nonLiteral")
29
-
30
19
 
31
20
  @dataclass
32
21
  class ShaclGenerator(Generator):
@@ -60,6 +49,9 @@ class ShaclGenerator(Generator):
60
49
  sv = self.schemaview
61
50
  g = Graph()
62
51
  g.bind("sh", SH)
52
+
53
+ ifabsent_processor = IfAbsentProcessor(sv)
54
+
63
55
  for pfx in self.schema.prefixes.values():
64
56
  g.bind(str(pfx.prefix_prefix), pfx.prefix_reference)
65
57
 
@@ -116,76 +108,107 @@ class ShaclGenerator(Generator):
116
108
  prop_pv_literal(SH.minCount, 1)
117
109
  prop_pv_literal(SH.minInclusive, s.minimum_value)
118
110
  prop_pv_literal(SH.maxInclusive, s.maximum_value)
119
- prop_pv_literal(SH.hasValue, s.equals_number)
120
- r = s.range
121
- if r in sv.all_classes():
122
- range_ref = sv.get_uri(r, expand=True)
123
- prop_pv(SH["class"], URIRef(range_ref))
124
- if sv.get_identifier_slot(r) is not None:
125
- prop_pv(SH.nodeKind, SH.IRI)
126
- else:
127
- prop_pv(SH.nodeKind, SH.BlankNode)
128
- elif r in sv.all_types().values():
129
- rt = sv.get_type(r)
130
- if rt.uri:
131
- prop_pv(SH.datatype, rt.uri)
132
- else:
133
- logging.error(f"No URI for type {rt.name}")
134
- elif r in sv.all_enums():
135
- e = sv.get_enum(r)
136
- pv_node = BNode()
137
- Collection(
138
- g,
139
- pv_node,
140
- [
141
- URIRef(sv.expand_curie(pv.meaning)) if pv.meaning else Literal(pv_name)
142
- for pv_name, pv in e.permissible_values.items()
143
- ],
144
- )
145
- prop_pv(SH["in"], pv_node)
111
+
112
+ all_classes = sv.all_classes()
113
+ if s.any_of:
114
+ or_node = BNode()
115
+ prop_pv(SH["or"], or_node)
116
+ range_list = []
117
+ for any in s.any_of:
118
+ r = any.range
119
+ if r in all_classes:
120
+ class_node = BNode()
121
+
122
+ def cl_node_pv(p, v):
123
+ if v is not None:
124
+ g.add((class_node, p, v))
125
+
126
+ self._add_class(cl_node_pv, r)
127
+ range_list.append(class_node)
128
+ elif r in sv.all_types().values():
129
+ t_node = BNode()
130
+
131
+ def t_node_pv(p, v):
132
+ if v is not None:
133
+ g.add((t_node, p, v))
134
+
135
+ self._add_type(t_node_pv, r)
136
+ range_list.append(t_node)
137
+ elif r in sv.all_enums():
138
+ en_node = BNode()
139
+
140
+ def en_node_pv(p, v):
141
+ if v is not None:
142
+ g.add((en_node, p, v))
143
+
144
+ self._add_enum(g, en_node_pv, r)
145
+ range_list.append(en_node)
146
+ else:
147
+ st_node = BNode()
148
+
149
+ def st_node_pv(p, v):
150
+ if v is not None:
151
+ g.add((st_node, p, v))
152
+
153
+ add_simple_data_type(st_node_pv, r)
154
+ range_list.append(st_node)
155
+ Collection(g, or_node, range_list)
156
+
146
157
  else:
147
- if r == "string":
148
- prop_pv(SH.datatype, LINK_ML_TYPES_STRING)
149
- elif r == "boolean":
150
- prop_pv(SH.datatype, LINK_ML_TYPES_BOOL)
151
- elif r == "duration":
152
- prop_pv(SH.datatype, LINK_ML_TYPES_DURATION)
153
- elif r == "datetime":
154
- prop_pv(SH.datatype, LINK_ML_TYPES_DATETIME)
155
- elif r == "date":
156
- prop_pv(SH.datatype, LINK_ML_TYPES_DATE)
157
- elif r == "time":
158
- prop_pv(SH.datatype, LINK_ML_TYPES_TIME)
159
- elif r == "datetime":
160
- prop_pv(SH.datatype, LINK_ML_TYPES_DATE_TIME)
161
- elif r == "decimal":
162
- prop_pv(SH.datatype, LINK_ML_TYPES_DECIMAL)
163
- elif r == "integer":
164
- prop_pv(SH.datatype, LINK_ML_TYPES_INTEGER)
165
- elif r == "float":
166
- prop_pv(SH.datatype, LINK_ML_TYPES_FLOAT)
167
- elif r == "double":
168
- prop_pv(SH.datatype, LINK_ML_TYPES_DOUBLE)
169
- elif r == "uri":
170
- prop_pv(SH.datatype, LINK_ML_TYPES_URI)
171
- elif r == "curi":
172
- prop_pv(SH.datatype, LINK_ML_TYPES_STRING)
173
- elif r == "ncname":
174
- prop_pv(SH.datatype, LINK_ML_TYPES_STRING)
175
- elif r == "objectidentifier":
176
- prop_pv(SH.datatype, LINK_ML_TYPES_OBJECT_ID)
177
- elif r == "nodeidentifier":
178
- prop_pv(SH.datatype, LINK_ML_TYPES_NODE_ID)
179
- elif r == "jsonpointer":
180
- prop_pv(SH.datatype, LINK_ML_TYPES_STRING)
181
- elif r == "jsonpath":
182
- prop_pv(SH.datatype, LINK_ML_TYPES_STRING)
183
- elif r == "sparqlpath":
184
- prop_pv(SH.datatype, LINK_ML_TYPES_STRING)
185
- if s.pattern:
186
- prop_pv(SH.pattern, Literal(s.pattern))
158
+ prop_pv_literal(SH.hasValue, s.equals_number)
159
+ r = s.range
160
+ if r in all_classes:
161
+ self._add_class(prop_pv, r)
162
+ if sv.get_identifier_slot(r) is not None:
163
+ prop_pv(SH.nodeKind, SH.IRI)
164
+ else:
165
+ prop_pv(SH.nodeKind, SH.BlankNode)
166
+ elif r in sv.all_types().values():
167
+ self._add_type(prop_pv, r)
168
+ elif r in sv.all_enums():
169
+ self._add_enum(g, prop_pv, r)
170
+ else:
171
+ add_simple_data_type(prop_pv, r)
172
+ if s.pattern:
173
+ prop_pv(SH.pattern, Literal(s.pattern))
174
+
175
+ ifabsent_processor.process_slot(prop_pv, s)
176
+
187
177
  return g
188
178
 
179
+ def _add_class(self, func: Callable, r: ElementName) -> None:
180
+ sv = self.schemaview
181
+ range_ref = sv.get_uri(r, expand=True)
182
+ func(SH["class"], URIRef(range_ref))
183
+
184
+ def _add_enum(self, g: Graph, func: Callable, r: ElementName) -> None:
185
+ sv = self.schemaview
186
+ enum = sv.get_enum(r)
187
+ pv_node = BNode()
188
+ Collection(
189
+ g,
190
+ pv_node,
191
+ [
192
+ URIRef(sv.expand_curie(pv.meaning)) if pv.meaning else Literal(pv_name)
193
+ for pv_name, pv in enum.permissible_values.items()
194
+ ],
195
+ )
196
+ func(SH["in"], pv_node)
197
+
198
+ def _add_type(self, func: Callable, r: ElementName) -> None:
199
+ sv = self.schemaview
200
+ rt = sv.get_type(r)
201
+ if rt.uri:
202
+ func(SH.datatype, rt.uri)
203
+ else:
204
+ logging.error(f"No URI for type {rt.name}")
205
+
206
+
207
+ def add_simple_data_type(func: Callable, r: ElementName) -> None:
208
+ for datatype in list(ShaclDataType):
209
+ if datatype.linkml_type == r:
210
+ func(SH.datatype, datatype.uri_ref)
211
+
189
212
 
190
213
  @shared_arguments(ShaclGenerator)
191
214
  @click.option(
@@ -165,7 +165,7 @@ class ShExGenerator(Generator):
165
165
  shex = as_json_1(self.shex)
166
166
  if self.format == "rdf":
167
167
  g = Graph()
168
- g.parse(data=shex, format="json-ld")
168
+ g.parse(data=shex, format="json-ld", version="1.1")
169
169
  g.bind("owl", OWL)
170
170
  shex = g.serialize(format="turtle")
171
171
  elif self.format == "shex":
@@ -40,7 +40,7 @@ class SQLAlchemyGenerator(Generator):
40
40
  """
41
41
  Generates SQL Alchemy classes
42
42
 
43
- See also: :ref:`SQLTableGenerator`
43
+ See also: :class:`~linkml.generators.sqltablegen.SQLTableGenerator`
44
44
  """
45
45
 
46
46
  # ClassVars
@@ -61,19 +61,19 @@ RANGEMAP = {
61
61
  @dataclass
62
62
  class SQLTableGenerator(Generator):
63
63
  """
64
- A :ref:`Generator` for creating SQL DDL
64
+ A :class:`~linkml.utils.generator.Generator` for creating SQL DDL
65
65
 
66
66
  The basic algorithm for mapping a linkml schema S is as follows:
67
67
 
68
- - Each schema S corresponds to one database schema D (see SQLSchema)
69
- - Each Class C in S is mapped to a table T (see SQLTable)
70
- - Each slot S in each C is mapped to a column Col (see SQLColumn)
68
+ - Each schema S corresponds to one database schema D (see SQLSchema)
69
+ - Each Class C in S is mapped to a table T (see SQLTable)
70
+ - Each slot S in each C is mapped to a column Col (see SQLColumn)
71
71
 
72
72
  if the direct_mapping attribute is set to true, then no further transformations
73
73
  are applied. Note that this means:
74
74
 
75
- - inline objects are modeled as Text strings
76
- - multivalued fields are modeled as single Text strings
75
+ - inline objects are modeled as Text strings
76
+ - multivalued fields are modeled as single Text strings
77
77
 
78
78
  this direct mapping is useful for simple spreadsheet/denormalized representations of complex data.
79
79
  however, for other applications, additional transformations should occur. these are:
@@ -95,22 +95,30 @@ class SQLTableGenerator(Generator):
95
95
  E.g. if a class User has a multivalues slot alias whose range is a string,
96
96
  then create a table user_aliases, with two columns (1) alias [a string] and (2) a backref to user
97
97
 
98
- Each mapped slot C.S has a range R
99
-
100
- ranges that are types (literals):
101
- - If R is a type, and the slot is NOT multivalued, do a direct type conversion
102
- - If R is a type, and the slot is multivalued:
103
- * do not include the mapped column
104
- * create a new table T_S, with 2 columns: S, and a backref to T
105
- ranges that are classes:
106
- Ref = map_class_to_table(R)
107
- - if R is a class, and the slot is NOT multivalued, and Ref has a singular primary key:
108
- * Col.type = ForeignKey(Ref.PK)
109
- - if R is a class, and the slot is NOT multivalued, and Ref has NO singular primary key:
110
- * add a foreign key C.pk to Ref
111
- * add a backref C.S => Ref, C.pk
112
- * remove Col from T
113
- - If R is a class, and the slot IS multivalued
98
+ Each mapped slot C.S has a range R
99
+
100
+ ranges that are types (literals):
101
+
102
+ * If R is a type, and the slot is NOT multivalued, do a direct type conversion
103
+ * If R is a type, and the slot is multivalued:
104
+
105
+ * do not include the mapped column
106
+ * create a new table T_S, with 2 columns: S, and a backref to T
107
+
108
+ ranges that are classes:
109
+
110
+ * Ref = map_class_to_table(R)
111
+ * if R is a class, and the slot is NOT multivalued, and Ref has a singular primary key:
112
+
113
+ * Col.type = ForeignKey(Ref.PK)
114
+
115
+ * if R is a class, and the slot is NOT multivalued, and Ref has NO singular primary key:
116
+
117
+ * add a foreign key C.pk to Ref
118
+ * add a backref C.S => Ref, C.pk
119
+ * remove Col from T
120
+
121
+ * If R is a class, and the slot IS multivalued
114
122
 
115
123
  """
116
124
 
@@ -236,6 +244,8 @@ class SQLTableGenerator(Generator):
236
244
  range_base = METAMODEL_TYPE_TO_BASE[range]
237
245
  elif range in schema.types:
238
246
  range_base = schema.types[range].base
247
+ elif range is None:
248
+ return Text()
239
249
  else:
240
250
  logging.error(f"Unknown range: {range} for {slot.name} = {slot.range}")
241
251
  return Text()
@@ -6,7 +6,13 @@ from typing import List
6
6
  import click
7
7
  from linkml_runtime.linkml_model.meta import ClassDefinition, SlotDefinition
8
8
  from linkml_runtime.utils.formatutils import be, camelcase, underscore
9
- from terminusdb_client.woqlquery import WOQLQuery as WQ
9
+
10
+ try:
11
+ from terminusdb_client.woqlquery import WOQLQuery as WQ
12
+ except ImportError:
13
+ import warnings
14
+
15
+ warnings.warn("terminusdb_client is not a requirement of this package, please install it separately")
10
16
 
11
17
  from linkml._version import __version__
12
18
  from linkml.utils.generator import Generator, shared_arguments
@@ -232,11 +232,19 @@ class StandardNamingConfig(RuleConfig):
232
232
 
233
233
  level: Union[str, "RuleLevel"] = None
234
234
  permissible_values_upper_case: Optional[Union[bool, Bool]] = None
235
+ slot_pattern: Optional[str] = None
236
+ class_pattern: Optional[str] = None
235
237
 
236
238
  def __post_init__(self, *_: List[str], **kwargs: Dict[str, Any]):
237
239
  if self.permissible_values_upper_case is not None and not isinstance(self.permissible_values_upper_case, Bool):
238
240
  self.permissible_values_upper_case = Bool(self.permissible_values_upper_case)
239
241
 
242
+ if self.class_pattern is not None and not isinstance(self.class_pattern, str):
243
+ self.class_pattern = str(self.class_pattern)
244
+
245
+ if self.slot_pattern is not None and not isinstance(self.slot_pattern, str):
246
+ self.slot_pattern = str(self.slot_pattern)
247
+
240
248
  super().__post_init__(**kwargs)
241
249
 
242
250
 
linkml/linter/rules.py CHANGED
@@ -219,8 +219,17 @@ class StandardNamingRule(LinterRule):
219
219
  self.config = config
220
220
 
221
221
  def check(self, schema_view: SchemaView, fix: bool = False) -> Iterable[LinterProblem]:
222
- class_pattern = self.PATTERNS["uppercamel"]
223
- slot_pattern = self.PATTERNS["snake"]
222
+ class_pattern = (
223
+ self.PATTERNS["uppercamel"]
224
+ if not self.config.class_pattern
225
+ else self.PATTERNS.get(self.config.class_pattern, re.compile(self.config.class_pattern))
226
+ )
227
+ slot_pattern = (
228
+ self.PATTERNS["snake"]
229
+ if not self.config.slot_pattern
230
+ else self.PATTERNS.get(self.config.slot_pattern, re.compile(self.config.slot_pattern))
231
+ )
232
+
224
233
  enum_pattern = self.PATTERNS["uppercamel"]
225
234
  permissible_value_pattern = (
226
235
  self.PATTERNS["uppersnake"] if self.config.permissible_values_upper_case else self.PATTERNS["snake"]
linkml/utils/generator.py CHANGED
@@ -83,11 +83,11 @@ class Generator(metaclass=abc.ABCMeta):
83
83
  For usage `Generator Docs <https://linkml.io/linkml/generators/>`_
84
84
  """
85
85
 
86
- # ClassVars
87
86
  schema: Union[str, TextIO, SchemaDefinition, "Generator"]
88
87
  """metamodel compliant schema. Can be URI, file name, actual schema, another generator, an
89
88
  open file or a pre-parsed schema"""
90
89
 
90
+ # ClassVars
91
91
  generatorname: ClassVar[str] = None
92
92
  """ Name of the generator. Override with os.path.basename(__file__)"""
93
93
 
@@ -159,7 +159,8 @@ class Generator(metaclass=abc.ABCMeta):
159
159
  """True means output is to a directory, False is to stdout"""
160
160
 
161
161
  base_dir: str = None # Base directory of schema
162
- """Working directory or base URL of sources"""
162
+ """Working directory or base URL of sources.
163
+ Setting this is necessary for correct retrieval of relative imports."""
163
164
 
164
165
  metamodel_name_map: Dict[str, str] = None
165
166
  """Allows mapping of names of metamodel elements such as slot, etc"""
@@ -196,8 +197,8 @@ class Generator(metaclass=abc.ABCMeta):
196
197
  if self.uses_schemaloader:
197
198
  self._initialize_using_schemaloader(schema)
198
199
  else:
199
- logging.info(f"Using SchemaView with im={self.importmap}")
200
- self.schemaview = SchemaView(schema, importmap=self.importmap)
200
+ logging.info(f"Using SchemaView with im={self.importmap} // base_dir={self.base_dir}")
201
+ self.schemaview = SchemaView(schema, importmap=self.importmap, base_dir=self.base_dir)
201
202
  self.schema = self.schemaview.schema
202
203
  self._init_namespaces()
203
204
 
@@ -683,7 +684,7 @@ class Generator(metaclass=abc.ABCMeta):
683
684
 
684
685
  def slot_name(self, name: str) -> str:
685
686
  """
686
- Return the underscored version of the aliased slot name if name is a slot. Prepend "unknown_" if the name
687
+ Return the underscored version of the aliased slot name if name is a slot. Prepend "unknown\_" if the name
687
688
  isn't valid.
688
689
  """
689
690
  slot = self.slot_for(name)
@@ -705,7 +706,7 @@ class Generator(metaclass=abc.ABCMeta):
705
706
 
706
707
  :param el_or_elname: element or name to map
707
708
  :param is_range_name: True means that we're looking for a class or type. False means Slot or Subset. Only
708
- applies if el_or_elname is an ElementName (otherwise we know what we've got
709
+ applies if el_or_elname is an ElementName (otherwise we know what we've got
709
710
  :return: Formatted name if type can be known else None
710
711
  """
711
712
  if isinstance(el_or_elname, str):
@@ -1,5 +1,5 @@
1
1
  import re
2
- from typing import Callable, List, Match, Optional, Text, Tuple
2
+ from typing import Callable, List, Match, Optional, Text, Tuple, Union
3
3
 
4
4
  from linkml_runtime.linkml_model.meta import ClassDefinition, SlotDefinition
5
5
  from linkml_runtime.utils.formatutils import sfx
@@ -77,12 +77,13 @@ default_library: List[
77
77
  (
78
78
  r"date\((\d{4})-(\d{2})-(\d{2})\)",
79
79
  False,
80
- lambda m, __, ___, ____: f"datetime.date({m[1]}, {m[2]}, {m[3]})",
80
+ lambda m, __, ___, ____: f"date({int(m[1])}, {int(m[2])}, {int(m[3])})",
81
81
  ),
82
82
  (
83
83
  r"datetime\((\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z\)",
84
84
  False,
85
- lambda m, __, ___, ____: f"datetime.datetime({m[1]}, {m[2]}, {m[3]}, {m[4]}, {m[5]}, {m[6]})",
85
+ lambda m, __, ___, ____: f"datetime({int(m[1])}, {int(m[2])}, {int(m[3])}, "
86
+ f"{int(m[4])}, {int(m[5])}, {int(m[6])})",
86
87
  ),
87
88
  # TODO: We have to make the real URI available before any of these can work
88
89
  # ("class_uri", True,
@@ -113,12 +114,9 @@ default_library: List[
113
114
 
114
115
  def isabsent_match(
115
116
  txt: Text,
116
- ) -> Optional[
117
- Tuple[
118
- Match[str],
119
- bool,
120
- Callable[[Match[str], SchemaLoader, ClassDefinition, SlotDefinition], str],
121
- ]
117
+ ) -> Union[
118
+ Tuple[Match[str], bool, Callable[[Match[str], SchemaLoader, ClassDefinition, SlotDefinition], str]],
119
+ Tuple[None, None, None],
122
120
  ]:
123
121
  txt = str(txt)
124
122
  for pattern, postinit, f in default_library:
@@ -118,7 +118,7 @@ class SchemaLoader:
118
118
  sname = self.importmap.get(str(sname), sname) # It may also use URI or other forms
119
119
  import_schemadefinition = load_raw_schema(
120
120
  sname + ".yaml",
121
- base_dir=os.path.dirname(self.schema.source_file) if self.schema.source_file else None,
121
+ base_dir=os.path.dirname(self.schema.source_file) if self.schema.source_file else self.base_dir,
122
122
  merge_modules=self.merge_modules,
123
123
  emit_metadata=self.emit_metadata,
124
124
  )
@@ -351,14 +351,6 @@ class SchemaLoader:
351
351
  f"slot: {slot.name} - unrecognized domain ({slot.domain})",
352
352
  slot.domain,
353
353
  )
354
- if slot.ifabsent:
355
- from linkml.utils.ifabsent_functions import isabsent_match
356
-
357
- if isabsent_match(slot.ifabsent) is None:
358
- self.raise_value_error(
359
- f"Unrecognized ifabsent action for slot '{slot.name}': '{slot.ifabsent}'",
360
- slot.ifabsent,
361
- )
362
354
 
363
355
  # Keys and identifiers must be present
364
356
  if bool(slot.key or slot.identifier):