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
@@ -1,13 +1,30 @@
1
1
  import sys
2
- from collections.abc import Generator
3
2
  from importlib.util import find_spec
3
+ from keyword import iskeyword
4
4
  from typing import Any, ClassVar, Literal, Optional, Union, get_args
5
5
 
6
+ try:
7
+ from typing import Self
8
+ except ImportError:
9
+ from typing_extensions import Self
10
+
6
11
  from jinja2 import Environment, PackageLoader
7
- from pydantic import BaseModel, Field, field_validator
12
+ from pydantic import BaseModel, Field, field_validator, model_validator
8
13
  from pydantic.version import VERSION as PYDANTIC_VERSION
9
14
 
10
- from linkml.generators.common.template import TemplateModel
15
+ from linkml.generators.common.template import (
16
+ ConditionalImport as ConditionalImport_,
17
+ )
18
+ from linkml.generators.common.template import (
19
+ Import as Import_,
20
+ )
21
+ from linkml.generators.common.template import (
22
+ Imports as Imports_,
23
+ )
24
+ from linkml.generators.common.template import (
25
+ ObjectImport, # noqa: F401
26
+ TemplateModel,
27
+ )
11
28
  from linkml.utils.deprecation import deprecation_warning
12
29
 
13
30
  try:
@@ -113,9 +130,19 @@ class EnumValue(BaseModel):
113
130
  """
114
131
 
115
132
  label: str
133
+ alias: Optional[str] = None
116
134
  value: str
117
135
  description: Optional[str] = None
118
136
 
137
+ @model_validator(mode="after")
138
+ def alias_python_keywords(self) -> Self:
139
+ """Mask Python keywords used for `label` by appending `_`"""
140
+ if iskeyword(self.label):
141
+ if self.alias is None:
142
+ self.alias = self.label
143
+ self.label = self.label + "_"
144
+ return self
145
+
119
146
 
120
147
  class PydanticEnum(PydanticTemplateModel):
121
148
  """
@@ -170,6 +197,7 @@ class PydanticAttribute(PydanticTemplateModel):
170
197
  meta_exclude: ClassVar[list[str]] = ["from_schema", "owner", "range", "inlined", "inlined_as_list"]
171
198
 
172
199
  name: str
200
+ alias: Optional[str] = None
173
201
  required: bool = False
174
202
  identifier: bool = False
175
203
  key: bool = False
@@ -200,8 +228,19 @@ class PydanticAttribute(PydanticTemplateModel):
200
228
  elif self.required or self.identifier or self.key:
201
229
  return "..."
202
230
  else:
231
+ if self.range and self.range.startswith("Optional[list"):
232
+ return "[]"
203
233
  return "None"
204
234
 
235
+ @model_validator(mode="after")
236
+ def alias_python_keywords(self) -> Self:
237
+ """Mask Python keywords used for `name` by appending `_`"""
238
+ if iskeyword(self.name):
239
+ if self.alias is None:
240
+ self.alias = self.name
241
+ self.name = self.name + "_"
242
+ return self
243
+
205
244
 
206
245
  class PydanticValidator(PydanticAttribute):
207
246
  """
@@ -250,18 +289,7 @@ class PydanticClass(PydanticTemplateModel):
250
289
  return self.attributes
251
290
 
252
291
 
253
- class ObjectImport(BaseModel):
254
- """
255
- An object to be imported from within a module.
256
-
257
- See :class:`.Import` for examples
258
- """
259
-
260
- name: str
261
- alias: Optional[str] = None
262
-
263
-
264
- class Import(PydanticTemplateModel):
292
+ class Import(Import_, PydanticTemplateModel):
265
293
  """
266
294
  A python module, or module and classes to be imported.
267
295
 
@@ -292,15 +320,6 @@ class Import(PydanticTemplateModel):
292
320
  """
293
321
 
294
322
  template: ClassVar[str] = "imports.py.jinja"
295
- module: str
296
- alias: Optional[str] = None
297
- objects: Optional[list[ObjectImport]] = None
298
- is_schema: bool = False
299
- """
300
- Whether or not this ``Import`` is importing another schema imported by the main schema --
301
- ie. that it is not expected to be provided by the environment, but imported locally from within the package.
302
- Used primarily in split schema generation, see :func:`.pydanticgen.generate_split` for example usage.
303
- """
304
323
 
305
324
  @computed_field
306
325
  def group(self) -> IMPORT_GROUPS:
@@ -324,72 +343,8 @@ class Import(PydanticTemplateModel):
324
343
  else:
325
344
  return "thirdparty"
326
345
 
327
- def merge(self, other: "Import") -> list["Import"]:
328
- """
329
- Merge one import with another, see :meth:`.Imports` for an example.
330
-
331
- * If module don't match, return both
332
- * If one or the other are a :class:`.ConditionalImport`, return both
333
- * If modules match, neither contain objects, but the other has an alias, return the other
334
- * If modules match, one contains objects but the other doesn't, return both
335
- * If modules match, both contain objects, merge the object lists, preferring objects with aliases
336
- """
337
- # return both if we are orthogonal
338
- if self.module != other.module:
339
- return [self, other]
340
-
341
- # handle conditionals
342
- if isinstance(self, ConditionalImport) and isinstance(other, ConditionalImport):
343
- # If our condition is the same, return the newer version
344
- if self.condition == other.condition:
345
- return [other]
346
- if isinstance(self, ConditionalImport) or isinstance(other, ConditionalImport):
347
- # we don't have a good way of combining conditionals, just return both
348
- return [self, other]
349
-
350
- # handle module vs. object imports
351
- elif other.objects is None and self.objects is None:
352
- # both are modules, return the other only if it updates the alias
353
- if other.alias:
354
- return [other]
355
- else:
356
- return [self]
357
- elif other.objects is not None and self.objects is not None:
358
- # both are object imports, merge and return
359
- alias = self.alias if other.alias is None else other.alias
360
- # FIXME: super awkward implementation
361
- # keep ours if it has an alias and the other doesn't,
362
- # otherwise take the other's version
363
- self_objs = {obj.name: obj for obj in self.objects}
364
- other_objs = {
365
- obj.name: obj for obj in other.objects if obj.name not in self_objs or self_objs[obj.name].alias is None
366
- }
367
- self_objs.update(other_objs)
368
-
369
- return [
370
- Import(
371
- module=self.module,
372
- alias=alias,
373
- objects=list(self_objs.values()),
374
- is_schema=self.is_schema or other.is_schema,
375
- )
376
- ]
377
- else:
378
- # one is a module, the other imports objects, keep both
379
- return [self, other]
380
-
381
- def sort(self) -> None:
382
- """
383
- Sort imported objects
384
-
385
- * First by whether the first letter is capitalized or not,
386
- * Then alphabetically (by object name rather than alias)
387
- """
388
- if self.objects:
389
- self.objects = sorted(self.objects, key=lambda obj: (obj.name[0].islower(), obj.name))
390
-
391
346
 
392
- class ConditionalImport(Import):
347
+ class ConditionalImport(ConditionalImport_, PydanticTemplateModel):
393
348
  """
394
349
  Import that depends on some condition in the environment, common when
395
350
  using backported features or straddling dependency versions.
@@ -430,22 +385,13 @@ class ConditionalImport(Import):
430
385
  """
431
386
 
432
387
  template: ClassVar[str] = "conditional_import.py.jinja"
433
- condition: str
434
- alternative: Import
435
388
 
436
389
  @computed_field
437
390
  def group(self) -> Literal["conditional"]:
438
391
  return "conditional"
439
392
 
440
- def sort(self) -> None:
441
- """
442
- :meth:`.Import.sort` called for self and :attr:`.alternative`
443
- """
444
- super().sort()
445
- self.alternative.sort()
446
-
447
393
 
448
- class Imports(PydanticTemplateModel):
394
+ class Imports(Imports_, PydanticTemplateModel):
449
395
  """
450
396
  Container class for imports that can handle merging!
451
397
 
@@ -482,135 +428,6 @@ class Imports(PydanticTemplateModel):
482
428
  imports: list[Union[Import, ConditionalImport]] = Field(default_factory=list)
483
429
  group_order: tuple[str, ...] = get_args(IMPORT_GROUPS)
484
430
  """Order in which to sort imports by their :attr:`.Import.group`"""
485
- render_sorted: bool = True
486
- """When rendering, render in sorted groups"""
487
-
488
- @classmethod
489
- def _merge(
490
- cls, imports: list[Union[Import, ConditionalImport]], other: Union[Import, "Imports", list[Import]]
491
- ) -> list[Union[Import, ConditionalImport]]:
492
- """
493
- Add a new import to an existing imports list, handling deduplication and flattening.
494
-
495
- Mutates and returns ``imports``
496
-
497
- Generally will prefer the imports in ``other`` , updating those in ``imports``.
498
- If ``other`` ...
499
- - doesn't match any ``module`` in ``imports``, add it!
500
- - matches a single ``module`` in imports, :meth:`.Import.merge` the object imports
501
- - matches multiple ``module``s in imports, then there must have been another
502
- :class:`.ConditionalImport` already present, so we :meth:`.Import.merge` the existing
503
- :class:`.Import` if it is one, and if it's a :class:`.ConditionalImport` just YOLO
504
- and append it since there isn't a principled way to merge them from strings.
505
- - is :class:`.Imports` or a list of :class:`.Import` s, call this recursively for each
506
- item.
507
-
508
- Since imports can be merged in an undefined order depending on the generator configuration,
509
- default behavior for imports with matching ``module`` is to remove them and append to the
510
- end of the imports list (rather than keeping it in the position of the existing
511
- :class:`.Import` ). :class:`.ConditionalImports` make it possible to have namespace
512
- conflicts, so in imperative import style we assume the most recently added :class:`.Import`
513
- is the one that should prevail.
514
- """
515
- #
516
- if isinstance(other, Imports) or (isinstance(other, list) and all([isinstance(i, Import) for i in other])):
517
- for i in other:
518
- imports = cls._merge(imports, i)
519
- return imports
520
-
521
- existing = [i for i in imports if i.module == other.module]
522
-
523
- # if we have nothing importing from this module yet, add it!
524
- if len(existing) == 0:
525
- imports.append(other)
526
- elif len(existing) == 1:
527
- imports.remove(existing[0])
528
- imports.extend(existing[0].merge(other))
529
- else:
530
- # we have both a conditional and at least one nonconditional already.
531
- # If this is another conditional, we just add it, otherwise, we merge it
532
- # with the single nonconditional
533
- if isinstance(other, ConditionalImport):
534
- imports.append(other)
535
- else:
536
- for e in existing:
537
- if isinstance(e, Import):
538
- imports.remove(e)
539
- merged = e.merge(other)
540
- imports.extend(merged)
541
- break
542
-
543
- # SPECIAL CASE - __future__ annotations must happen at the top of a file
544
- # sort here outside of sort method because our imports are invalid without it,
545
- # where calling ``sort`` should be optional.
546
- imports = sorted(imports, key=lambda i: i.module == "__future__", reverse=True)
547
- return imports
548
-
549
- def __add__(self, other: Union[Import, "Imports", list[Import]]) -> "Imports":
550
- imports = self.imports.copy()
551
- imports = self._merge(imports, other)
552
- return Imports.model_construct(
553
- imports=imports, **{k: getattr(self, k, None) for k in self.model_fields if k != "imports"}
554
- )
555
-
556
- def __len__(self) -> int:
557
- return len(self.imports)
558
-
559
- def __iter__(self) -> Generator[Import, None, None]:
560
- yield from self.imports
561
-
562
- def __getitem__(self, item: Union[int, str]) -> Import:
563
- if isinstance(item, int):
564
- return self.imports[item]
565
- elif isinstance(item, str):
566
- # the name of the module
567
- an_import = [i for i in self.imports if i.module == item]
568
- if len(an_import) == 0:
569
- raise KeyError(f"No import with module {item} was found.\nWe have: {self.imports}")
570
- return an_import[0]
571
- else:
572
- raise TypeError(f"Can only index with an int or a string as the name of the module,\nGot: {type(item)}")
573
-
574
- def __contains__(self, item: Union[Import, "Imports", list[Import]]) -> bool:
575
- """
576
- Check if all the objects are imported from the given module(s)
577
-
578
- If the import is a bare module import (ie its :attr:`~.Import.objects` is ``None`` )
579
- then we must also have a bare module import in this Imports (because even if
580
- we import from the module, unless we import it specifically its name won't be
581
- available in the namespace.
582
-
583
- :attr:`.Import.alias` must always match for the same reason.
584
- """
585
- if isinstance(item, Imports):
586
- return all([i in self for i in item.imports])
587
- elif isinstance(item, list):
588
- return all([i in self for i in item])
589
- elif isinstance(item, Import):
590
- try:
591
- an_import = self[item.module]
592
- except KeyError:
593
- return False
594
- if item.objects is None:
595
- return an_import == item
596
- else:
597
- return all([obj in an_import.objects for obj in item.objects])
598
- else:
599
- raise TypeError(f"Imports only contains single Import objects or other Imports\nGot: {type(item)}")
600
-
601
- @field_validator("imports", mode="after")
602
- @classmethod
603
- def imports_are_merged(
604
- cls, imports: list[Union[Import, ConditionalImport]]
605
- ) -> list[Union[Import, ConditionalImport]]:
606
- """
607
- When creating from a list of imports, construct model as if we have done so by iteratively
608
- constructing with __add__ calls
609
- """
610
- merged_imports = []
611
- for i in imports:
612
- merged_imports = cls._merge(merged_imports, i)
613
- return merged_imports
614
431
 
615
432
  @computed_field
616
433
  def import_groups(self) -> list[IMPORT_GROUPS]:
@@ -637,11 +454,6 @@ class Imports(PydanticTemplateModel):
637
454
  i.sort()
638
455
  self.imports = imports
639
456
 
640
- def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
641
- if self.render_sorted:
642
- self.sort()
643
- return super().render(environment=environment, black=black)
644
-
645
457
 
646
458
  class PydanticModule(PydanticTemplateModel):
647
459
  """
@@ -1,4 +1,5 @@
1
1
  {{name}}: {{ range }} = Field(default={{ field }}
2
+ {%- if alias %}, alias="{{alias}}"{% endif -%}
2
3
  {%- if title != None %}, title="{{title}}"{% endif -%}
3
4
  {%- if description %}, description="""{{description}}"""{% endif -%}
4
5
  {%- if equals_number != None %}
@@ -1,5 +1,7 @@
1
1
  class {{ name }}(BaseModel):
2
2
  model_config = ConfigDict(
3
+ serialize_by_alias = True,
4
+ validate_by_name = True,
3
5
  validate_assignment = True,
4
6
  validate_default = True,
5
7
  extra = "{{ extra_fields }}",
@@ -11,6 +13,18 @@ class {{ name }}(BaseModel):
11
13
  {% for field in fields %}
12
14
  {{ field }}
13
15
  {% endfor %}
14
- {% else %}
15
- {{ "pass" }}
16
16
  {% endif %}
17
+
18
+ @model_serializer(mode='wrap', when_used='unless-none')
19
+ def treat_empty_lists_as_none(
20
+ self, handler: SerializerFunctionWrapHandler,
21
+ info: SerializationInfo) -> dict[str, Any]:
22
+ if info.exclude_none:
23
+ _instance = self.model_copy()
24
+ for field, field_info in type(_instance).model_fields.items():
25
+ if getattr(_instance, field) == [] and not(
26
+ field_info.is_required()):
27
+ setattr(_instance, field, None)
28
+ else:
29
+ _instance = self
30
+ return handler(_instance, info)
@@ -5,7 +5,7 @@ import {{ module }}
5
5
  import {{ module }} as {{ alias }}
6
6
  {%- else %}
7
7
  {% if objects | length == 1 %}
8
- from {{ module }} import {{ objects[0]['name'] }} {% if objects[0]['alias'] is not none %} as {{ objects[0]['alias'] }} {% endif %}
8
+ from {{ module }} import {{ objects[0]['name'] }}{% if objects[0]['alias'] is not none %} as {{ objects[0]['alias'] }}{% endif %}
9
9
  {%- else %}
10
10
  from {{ module }} import (
11
11
  {% for object in objects %}
@@ -67,12 +67,21 @@ class RDFGenerator(Generator):
67
67
  base=str(self.namespaces._base),
68
68
  prefix=True,
69
69
  )
70
- out = self._data(graph)
71
70
  if output:
71
+ # Binary-safe when -o/--output is used:
72
+ # delegate to RDFLib (Graph.serialize(destination=..., format=...)).
73
+ # Serializers that produce bytes write directly to the file; stdout stays empty.
74
+ fmt = "turtle" if self.format == "ttl" else self.format
75
+ try:
76
+ out = graph.serialize(format=fmt)
77
+ except UnicodeDecodeError:
78
+ graph.serialize(destination=output, format=fmt)
79
+ return ""
72
80
  with open(output, "w", encoding="UTF-8") as outf:
73
81
  outf.write(out)
82
+ return out
74
83
 
75
- return out
84
+ return self._data(graph)
76
85
 
77
86
 
78
87
  @shared_arguments(RDFGenerator)
@@ -0,0 +1,3 @@
1
+ from linkml.generators.rustgen.rustgen import RUST_MODES, RustGenerator
2
+
3
+ __all__ = ["RUST_MODES", "RustGenerator"]
@@ -0,0 +1,97 @@
1
+ from pydantic import Field
2
+
3
+ from linkml.generators.common.build import (
4
+ BuildResult,
5
+ SchemaResult,
6
+ )
7
+ from linkml.generators.common.build import (
8
+ ClassResult as ClassResult_,
9
+ )
10
+ from linkml.generators.common.build import EnumResult as EnumResult_
11
+ from linkml.generators.common.build import (
12
+ SlotResult as SlotResult_,
13
+ )
14
+ from linkml.generators.common.build import TypeResult as TypeResult_
15
+ from linkml.generators.rustgen.template import (
16
+ Imports,
17
+ RustCargo,
18
+ RustEnum,
19
+ RustFile,
20
+ RustProperty,
21
+ RustPyProject,
22
+ RustStruct,
23
+ RustTemplateModel,
24
+ RustTypeAlias,
25
+ )
26
+
27
+
28
+ class RustBuildResult(BuildResult):
29
+ """
30
+ BuildResult parent class for rustgen
31
+ """
32
+
33
+ imports: Imports = Imports()
34
+
35
+ def merge(self, other: "RustBuildResult") -> "RustBuildResult":
36
+ self.imports += other.imports
37
+ return self
38
+
39
+
40
+ class TypeResult(RustBuildResult, TypeResult_):
41
+ """
42
+ A linkml type as a type alias
43
+ """
44
+
45
+ type_: RustTypeAlias
46
+
47
+
48
+ class ClassResult(RustBuildResult, ClassResult_):
49
+ """
50
+ A single built rust struct
51
+ """
52
+
53
+ cls: RustStruct
54
+
55
+
56
+ class SlotResult(RustBuildResult, SlotResult_):
57
+ """
58
+ A type alias
59
+ """
60
+
61
+ slot: RustTypeAlias
62
+
63
+
64
+ class AttributeResult(RustBuildResult, SlotResult_):
65
+ """
66
+ A field within a rust struct
67
+ """
68
+
69
+ attribute: RustProperty
70
+
71
+
72
+ class EnumResult(RustBuildResult, EnumResult_):
73
+ """
74
+ A rust enum!
75
+ """
76
+
77
+ enum: RustEnum
78
+
79
+
80
+ class CrateResult(RustBuildResult, SchemaResult):
81
+ """
82
+ A schema built into a rust crate
83
+ """
84
+
85
+ cargo: RustCargo
86
+ file: RustFile
87
+ extra_files: dict[str, RustTemplateModel]
88
+ pyproject: RustPyProject
89
+ bin_files: dict[str, RustTemplateModel] = Field(default_factory=dict)
90
+
91
+
92
+ class FileResult(RustBuildResult, SchemaResult):
93
+ """
94
+ A schema built into a single rust file
95
+ """
96
+
97
+ file: RustFile
@@ -0,0 +1,83 @@
1
+ from pathlib import Path
2
+ from typing import Optional, get_args
3
+
4
+ import click
5
+
6
+ from linkml._version import __version__
7
+ from linkml.generators.rustgen import RUST_MODES, RustGenerator
8
+ from linkml.utils.generator import shared_arguments
9
+
10
+
11
+ @shared_arguments(RustGenerator)
12
+ @click.option(
13
+ "-m",
14
+ "--mode",
15
+ type=click.Choice([a for a in get_args(RUST_MODES)]),
16
+ default="crate",
17
+ help="Generation mode: 'crate' (Cargo package) or 'file' (single .rs)",
18
+ )
19
+ @click.option(
20
+ "-f",
21
+ "--force",
22
+ is_flag=True,
23
+ help="Overwrite output if it already exists",
24
+ )
25
+ @click.option(
26
+ "-p",
27
+ "--pyo3",
28
+ is_flag=True,
29
+ help=(
30
+ "Add 'pyo3' to Cargo.toml default features and emit Python module glue (cdylib + #[pymodule]). "
31
+ 'Source always includes #[cfg(feature="pyo3")] gates; this flag only enables the crate feature by default.'
32
+ ),
33
+ )
34
+ @click.option(
35
+ "-s",
36
+ "--serde",
37
+ is_flag=True,
38
+ help=(
39
+ "Add 'serde' to Cargo.toml default features. Source always includes #[cfg(feature=\"serde\")] derives/attrs; "
40
+ "this flag only enables the crate feature by default."
41
+ ),
42
+ )
43
+ @click.option(
44
+ "--handwritten-lib/--no-handwritten-lib",
45
+ default=False,
46
+ help=(
47
+ "When enabled, place generated sources under src/generated and create a shim lib.rs for handwritten code. "
48
+ "The shim is only created on first run and left untouched on subsequent regenerations."
49
+ ),
50
+ )
51
+ @click.option("-n", "--crate-name", type=str, default=None, help="Name of the generated crate/module")
52
+ @click.option(
53
+ "-o",
54
+ "--output",
55
+ type=click.Path(dir_okay=True),
56
+ help="Output directory (crate mode) or .rs file (file mode)",
57
+ )
58
+ @click.version_option(__version__, "-V", "--version")
59
+ @click.command(name="rust")
60
+ def cli(
61
+ yamlfile: Path,
62
+ mode: RUST_MODES = "crate",
63
+ force: bool = False,
64
+ pyo3: bool = False,
65
+ serde: bool = False,
66
+ crate_name: Optional[str] = None,
67
+ handwritten_lib: bool = False,
68
+ output: Optional[Path] = None,
69
+ **kwargs,
70
+ ):
71
+ gen = RustGenerator(
72
+ yamlfile,
73
+ mode=mode,
74
+ pyo3=pyo3,
75
+ serde=serde,
76
+ output=output,
77
+ crate_name=crate_name,
78
+ handwritten_lib=handwritten_lib,
79
+ **kwargs,
80
+ )
81
+ serialized = gen.serialize(force=force)
82
+ if output is None:
83
+ print(serialized)