linkml 1.9.2rc1__py3-none-any.whl → 1.9.3__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 (60) hide show
  1. linkml/generators/PythonGenNotes.md +49 -49
  2. linkml/generators/README.md +2 -2
  3. linkml/generators/dbmlgen.py +0 -1
  4. linkml/generators/docgen/class.md.jinja2 +35 -11
  5. linkml/generators/docgen/class_diagram.md.jinja2 +15 -15
  6. linkml/generators/docgen/common_metadata.md.jinja2 +3 -5
  7. linkml/generators/docgen/enum.md.jinja2 +5 -6
  8. linkml/generators/docgen/index.md.jinja2 +5 -5
  9. linkml/generators/docgen/index.tex.jinja2 +1 -1
  10. linkml/generators/docgen/schema.md.jinja2 +1 -3
  11. linkml/generators/docgen/slot.md.jinja2 +6 -8
  12. linkml/generators/docgen/subset.md.jinja2 +4 -7
  13. linkml/generators/docgen/type.md.jinja2 +2 -3
  14. linkml/generators/docgen.py +94 -7
  15. linkml/generators/erdiagramgen.py +1 -1
  16. linkml/generators/graphqlgen.py +1 -1
  17. linkml/generators/javagen/example_template.java.jinja2 +0 -1
  18. linkml/generators/jsonldcontextgen.py +0 -1
  19. linkml/generators/jsonldgen.py +3 -1
  20. linkml/generators/jsonschemagen.py +2 -2
  21. linkml/generators/linkmlgen.py +1 -1
  22. linkml/generators/markdowngen.py +20 -9
  23. linkml/generators/mermaidclassdiagramgen.py +4 -0
  24. linkml/generators/projectgen.py +17 -20
  25. linkml/generators/pydanticgen/includes.py +5 -5
  26. linkml/generators/pydanticgen/pydanticgen.py +2 -3
  27. linkml/generators/pydanticgen/template.py +1 -1
  28. linkml/generators/pydanticgen/templates/base_model.py.jinja +1 -1
  29. linkml/generators/pydanticgen/templates/class.py.jinja +1 -1
  30. linkml/generators/pydanticgen/templates/conditional_import.py.jinja +1 -1
  31. linkml/generators/pydanticgen/templates/footer.py.jinja +1 -1
  32. linkml/generators/pydanticgen/templates/imports.py.jinja +1 -1
  33. linkml/generators/python/python_ifabsent_processor.py +1 -1
  34. linkml/generators/pythongen.py +9 -8
  35. linkml/generators/shacl/shacl_ifabsent_processor.py +0 -1
  36. linkml/generators/shaclgen.py +1 -1
  37. linkml/generators/sparqlgen.py +1 -1
  38. linkml/generators/sqlalchemy/__init__.py +0 -4
  39. linkml/generators/sqlalchemygen.py +9 -15
  40. linkml/generators/sqltablegen.py +70 -4
  41. linkml/generators/string_template.md +3 -4
  42. linkml/generators/terminusdbgen.py +1 -2
  43. linkml/linter/cli.py +3 -4
  44. linkml/linter/config/datamodel/config.py +286 -135
  45. linkml/linter/config/datamodel/config.yaml +26 -11
  46. linkml/linter/config/default.yaml +6 -0
  47. linkml/linter/config/recommended.yaml +6 -0
  48. linkml/linter/linter.py +10 -6
  49. linkml/linter/rules.py +144 -46
  50. linkml/transformers/relmodel_transformer.py +2 -1
  51. linkml/utils/generator.py +3 -0
  52. linkml/utils/logictools.py +5 -5
  53. linkml/utils/schemaloader.py +4 -4
  54. linkml/utils/schemasynopsis.py +11 -7
  55. linkml/workspaces/datamodel/workspaces.yaml +0 -5
  56. {linkml-1.9.2rc1.dist-info → linkml-1.9.3.dist-info}/LICENSE +1 -1
  57. {linkml-1.9.2rc1.dist-info → linkml-1.9.3.dist-info}/METADATA +2 -2
  58. {linkml-1.9.2rc1.dist-info → linkml-1.9.3.dist-info}/RECORD +60 -60
  59. {linkml-1.9.2rc1.dist-info → linkml-1.9.3.dist-info}/WHEEL +0 -0
  60. {linkml-1.9.2rc1.dist-info → linkml-1.9.3.dist-info}/entry_points.txt +0 -0
@@ -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'({self.formatted_element_name(cls.is_a, True) if cls.is_a else "YAMLRoot"})'
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}) " f"else {base_type_name}(v) for v in {sn}]")
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'{self.process_multiline_string(pv.description, f"{indent_str}description=")}')
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)
@@ -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'{TypedNode.yaml_loc(str(s), suffix="")} {error}')
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)
@@ -151,7 +151,7 @@ class SparqlGenerator(Generator):
151
151
  template_obj = Template(template)
152
152
  extra = ""
153
153
  if named_graphs is not None:
154
- extra += f'FILTER( ?graph in ( {",".join(named_graphs)} ))'
154
+ extra += f"FILTER( ?graph in ( {','.join(named_graphs)} ))"
155
155
  logger.info(f"Named Graphs = {named_graphs} // extra={extra}")
156
156
  if limit is not None and isinstance(limit, int):
157
157
  limit = f"LIMIT {limit}"
@@ -1,4 +0,0 @@
1
- from .sqlalchemy_declarative_template import sqlalchemy_declarative_template_str
2
- from .sqlalchemy_imperative_template import sqlalchemy_imperative_template_str
3
-
4
- __all__ = ["sqlalchemy_declarative_template_str", "sqlalchemy_imperative_template_str"]
@@ -1,9 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  import os
3
5
  from collections import defaultdict
4
6
  from dataclasses import dataclass
5
7
  from types import ModuleType
6
- from typing import Optional, Union
7
8
 
8
9
  import click
9
10
  from jinja2 import Template
@@ -16,7 +17,8 @@ from sqlalchemy import Enum
16
17
  from linkml._version import __version__
17
18
  from linkml.generators.pydanticgen import PydanticGenerator
18
19
  from linkml.generators.pythongen import PythonGenerator
19
- from linkml.generators.sqlalchemy import sqlalchemy_declarative_template_str, sqlalchemy_imperative_template_str
20
+ from linkml.generators.sqlalchemy.sqlalchemy_declarative_template import sqlalchemy_declarative_template_str
21
+ from linkml.generators.sqlalchemy.sqlalchemy_imperative_template import sqlalchemy_imperative_template_str
20
22
  from linkml.generators.sqltablegen import SQLTableGenerator
21
23
  from linkml.transformers.relmodel_transformer import ForeignKeyPolicy, RelationalModelTransformer
22
24
  from linkml.utils.generator import Generator, shared_arguments
@@ -43,10 +45,10 @@ class SQLAlchemyGenerator(Generator):
43
45
  valid_formats = ["sqla"]
44
46
  file_extension = "py"
45
47
  uses_schemaloader = False
46
- template: Optional[TemplateEnum] = None
48
+ template: TemplateEnum | None = None
47
49
 
48
50
  # ObjectVars
49
- original_schema: Union[SchemaDefinition, str] = None
51
+ original_schema: SchemaDefinition | str = None
50
52
 
51
53
  def __post_init__(self) -> None:
52
54
  self.original_schema = self.schema
@@ -55,14 +57,12 @@ class SQLAlchemyGenerator(Generator):
55
57
 
56
58
  def generate_sqla(
57
59
  self,
58
- model_path: Optional[str] = None,
60
+ model_path: str | None = None,
59
61
  no_model_import: bool = False,
60
- template: Optional[TemplateEnum] = None,
61
- foreign_key_policy: Optional[ForeignKeyPolicy] = None,
62
+ template: TemplateEnum | None = None,
63
+ foreign_key_policy: ForeignKeyPolicy | None = None,
62
64
  **kwargs,
63
65
  ) -> str:
64
- # src_sv = SchemaView(self.schema)
65
- # self.schema = src_sv.schema
66
66
  template = template or self.template or TemplateEnum.IMPERATIVE
67
67
  sqltr = RelationalModelTransformer(self.schemaview)
68
68
  if foreign_key_policy:
@@ -76,7 +76,6 @@ class SQLAlchemyGenerator(Generator):
76
76
  sql_type = sql_range.__repr__()
77
77
  ann = Annotation("sql_type", sql_type)
78
78
  a.annotations[ann.tag] = ann
79
- # a.sql_type = sql_type
80
79
  if template == TemplateEnum.IMPERATIVE:
81
80
  template_str = sqlalchemy_imperative_template_str
82
81
  elif template == TemplateEnum.DECLARATIVE:
@@ -90,10 +89,6 @@ class SQLAlchemyGenerator(Generator):
90
89
  backrefs = defaultdict(list)
91
90
  for m in tr_result.mappings:
92
91
  backrefs[m.source_class].append(m)
93
- # for c in tr_schema.classes.values():
94
- # if len(c.attributes) == 0:
95
- # skip[c.name] = True
96
- # #raise ValueError(f'Class must have attrs: {c.name}')
97
92
  self.add_safe_aliases(tr_schema)
98
93
  tr_sv = SchemaView(tr_schema)
99
94
  rel_schema_classes_ordered = [tr_sv.get_class(cn, strict=True) for cn in self.order_classes_by_hierarchy(tr_sv)]
@@ -167,7 +162,6 @@ class SQLAlchemyGenerator(Generator):
167
162
  @staticmethod
168
163
  def add_safe_aliases(schema: SchemaDefinition) -> None:
169
164
  for c in schema.classes.values():
170
- # c.alias = underscore(c.name)
171
165
  for a in c.attributes.values():
172
166
  a.alias = underscore(a.name)
173
167
 
@@ -11,7 +11,7 @@ from linkml_runtime.dumpers import yaml_dumper
11
11
  from linkml_runtime.linkml_model import SchemaDefinition, SlotDefinition
12
12
  from linkml_runtime.utils.formatutils import camelcase, underscore
13
13
  from linkml_runtime.utils.schemaview import SchemaView
14
- from sqlalchemy import Column, ForeignKey, MetaData, Table, UniqueConstraint, create_mock_engine
14
+ from sqlalchemy import Column, ForeignKey, Index, MetaData, Table, UniqueConstraint, create_mock_engine
15
15
  from sqlalchemy.dialects.oracle import VARCHAR2
16
16
  from sqlalchemy.types import Boolean, Date, DateTime, Enum, Float, Integer, Text, Time
17
17
 
@@ -149,6 +149,8 @@ class SQLTableGenerator(Generator):
149
149
  relative_slot_num: bool = False
150
150
  default_length_oracle: int = ORACLE_MAX_VARCHAR_LENGTH
151
151
  generate_abstract_class_ddl: bool = True
152
+ autogenerate_pk_index: bool = True
153
+ autogenerate_fk_index: bool = True
152
154
 
153
155
  def serialize(self, **kwargs: dict[str, Any]) -> str:
154
156
  return self.generate_ddl(**kwargs)
@@ -191,21 +193,34 @@ class SQLTableGenerator(Generator):
191
193
  return ""
192
194
  return txt.replace("\n", "")
193
195
 
196
+ def strip_dangling_whitespace(txt: str) -> str:
197
+ return "\n".join(line.rstrip() for line in txt.splitlines())
198
+
194
199
  # Currently SQLite dialect in SQLA does not generate comments; see
195
200
  # https://github.com/sqlalchemy/sqlalchemy/issues/1546#issuecomment-1067389172
196
201
  # As a workaround we add these as "--" comments via direct string manipulation
197
202
  include_comments = self.dialect == "sqlite"
198
203
  sv = SchemaView(schema)
199
204
 
205
+ # This is a safeguard for checking duplicate item names within a table
206
+ autogenerated_item_names = []
207
+
200
208
  # Iterate through the attributes in each class, creating Column objects.
201
209
  # This includes generating the appropriate column name, converting the range
202
210
  # into an SQL type, and adding a foreign key notation if appropriate.
203
211
  for cn, c in schema.classes.items():
212
+ desc = strip_newlines(c.description)
204
213
  if include_comments:
205
214
  if c.abstract:
206
- ddl_str += f'-- # Abstract Class: "{cn}" Description: "{strip_newlines(c.description)}"\n'
215
+ if desc:
216
+ ddl_str += f"-- # Abstract Class: {cn} Description: {desc}\n"
217
+ else:
218
+ ddl_str += f"-- # Abstract Class: {cn}\n"
207
219
  else:
208
- ddl_str += f'-- # Class: "{cn}" Description: "{strip_newlines(c.description)}"\n'
220
+ if desc:
221
+ ddl_str += f"-- # Class: {cn} Description: {desc}\n"
222
+ else:
223
+ ddl_str += f"-- # Class: {cn}\n"
209
224
  pk_slot = sv.get_identifier_slot(cn)
210
225
  if c.attributes:
211
226
  cols = []
@@ -220,15 +235,26 @@ class SQLTableGenerator(Generator):
220
235
  fk = sql_name(self.get_id_or_key(s.range, sv))
221
236
  args = [ForeignKey(fk)]
222
237
  field_type = self.get_sql_range(s, schema)
238
+ fk_index_cond = (s.key or s.identifier) and self.autogenerate_fk_index
239
+ pk_index_cond = is_pk and self.autogenerate_pk_index
240
+ is_index = fk_index_cond or pk_index_cond
223
241
  col = Column(
224
242
  sql_name(sn),
225
243
  field_type,
226
244
  *args,
227
245
  primary_key=is_pk,
228
246
  nullable=not s.required,
247
+ index=is_index,
229
248
  )
249
+ if is_index:
250
+ # this is what SQLAlchemy generates, TODO: perhaps we need to expand this
251
+ autogenerated_item_names.append(sql_name("ix_" + sql_name(cn) + "_" + sql_name(sn)))
230
252
  if include_comments:
231
- ddl_str += f"-- * Slot: {sn} Description: {strip_newlines(s.description)}\n"
253
+ desc = strip_newlines(s.description)
254
+ comment_str = f"-- * Slot: {sn}"
255
+ if desc:
256
+ comment_str += f" Description: {desc}"
257
+ ddl_str += comment_str + "\n"
232
258
  if s.description:
233
259
  col.comment = s.description
234
260
  cols.append(col)
@@ -239,9 +265,34 @@ class SQLTableGenerator(Generator):
239
265
  continue
240
266
  sql_uc = UniqueConstraint(*sql_names)
241
267
  cols.append(sql_uc)
268
+ # Anything that has a unique constraint should have an associated index with it
269
+ uc_index_name = sql_name(cn)
270
+ for name in sql_names:
271
+ uc_index_name = uc_index_name + "_" + name
272
+ uc_index_name = uc_index_name + "_idx"
273
+ is_duplicate = self.check_duplicate_entry_names(autogenerated_item_names, sql_name(uc_index_name))
274
+ if not is_duplicate:
275
+ sql_names = [sql_name(uc_index_name)] + sql_names
276
+ uc_index = Index(*sql_names)
277
+ cols.append(uc_index)
278
+ autogenerated_item_names.append(sql_name(uc_index_name))
242
279
  if not c.abstract or (c.abstract and self.generate_abstract_class_ddl):
280
+ for tag, annotation in c.annotations.items():
281
+ if tag == "index":
282
+ value_dict = {k: annotation for k, annotation in annotation.value._items()}
283
+ for key, value in value_dict.items():
284
+ test_name = sql_name(key)
285
+ name_exists = self.check_duplicate_entry_names(autogenerated_item_names, test_name)
286
+ if not name_exists:
287
+ value_sql_name = [sql_name(col_name) for col_name in value]
288
+ annotation_ind = Index(test_name, *value_sql_name)
289
+ autogenerated_item_names.append(test_name)
290
+ cols.append(annotation_ind)
243
291
  Table(sql_name(cn), schema_metadata, *cols, comment=str(c.description))
244
292
  schema_metadata.create_all(engine)
293
+ ddl_str = strip_dangling_whitespace(ddl_str)
294
+ if ddl_str[-1:] != "\n":
295
+ ddl_str += "\n"
245
296
  return ddl_str
246
297
 
247
298
  def get_oracle_sql_range(self, slot: SlotDefinition) -> Text | VARCHAR2 | None:
@@ -335,6 +386,15 @@ class SQLTableGenerator(Generator):
335
386
  pk_name = pk.alias if pk.alias else pk.name
336
387
  return f"{cn}.{pk_name}"
337
388
 
389
+ # Scans the schema for duplicate names that exist as a best practice
390
+ @staticmethod
391
+ def check_duplicate_entry_names(autogen: list, item_name: str) -> bool:
392
+ if item_name in autogen:
393
+ msg = f"Warning: {item_name} already exists, please generate a new name"
394
+ logger.warning(msg)
395
+ return True
396
+ return False
397
+
338
398
 
339
399
  @shared_arguments(SQLTableGenerator)
340
400
  @click.command(name="sqltables")
@@ -362,6 +422,12 @@ class SQLTableGenerator(Generator):
362
422
  show_default=True,
363
423
  help="Default length of varchar based arguments for oracle dialects",
364
424
  )
425
+ @click.option(
426
+ "--autogenerate_index",
427
+ default=False,
428
+ show_default=True,
429
+ help="Enable the creation of indexes on all columns generated",
430
+ )
365
431
  @click.option(
366
432
  "--generate_abstract_class_ddl",
367
433
  default=True,
@@ -1,6 +1,6 @@
1
1
  # String Template proposal
2
2
  The `string_template` attribute allows a Python [format string](https://docs.python.org/3/library/string.html#formatstrings)
3
- to be associated with a LinkML model `Element`.
3
+ to be associated with a LinkML model `Element`.
4
4
 
5
5
  ## Syntax
6
6
  ```yaml
@@ -29,7 +29,7 @@ class FirstClass(YAMLRoot):
29
29
  name: str = None
30
30
  age: Optional[int] = None
31
31
  gender: Optional[str] = None
32
-
32
+
33
33
  ...
34
34
 
35
35
  def __str__(self):
@@ -54,7 +54,7 @@ inst2 = FirstClass.parse("Jillian Johnson - a 93 year old female")
54
54
  print(str(inst2))
55
55
  > 'Jillian Johnson - a 93 year old female'
56
56
 
57
- # repr gives you the non-templated
57
+ # repr gives you the non-templated
58
58
  print(repr(inst2))
59
59
  > "FirstClass(name='Jillian Johnson', age=93, gender='female')"
60
60
 
@@ -71,4 +71,3 @@ name: Freddy Buster Jones
71
71
  age: 11
72
72
  gender: Undetermined
73
73
  ```
74
-
@@ -114,8 +114,7 @@ class TerminusdbGenerator(Generator):
114
114
 
115
115
  if rng not in XSD_Ok and slot.range not in self.schema.classes:
116
116
  raise Exception(
117
- f"slot range for {name} must be schema class or supported xsd type. "
118
- f"Range {rng} is of type {type(rng)}."
117
+ f"slot range for {name} must be schema class or supported xsd type. Range {rng} is of type {type(rng)}."
119
118
  )
120
119
 
121
120
  self.clswq.property(underscore(name), rng, label=name, description=slot.description)
linkml/linter/cli.py CHANGED
@@ -7,10 +7,9 @@ import click
7
7
  import yaml
8
8
 
9
9
  from linkml._version import __version__
10
-
11
- from .config.datamodel.config import RuleLevel
12
- from .formatters import JsonFormatter, MarkdownFormatter, TerminalFormatter, TsvFormatter
13
- from .linter import Linter
10
+ from linkml.linter.config.datamodel.config import RuleLevel
11
+ from linkml.linter.formatters import JsonFormatter, MarkdownFormatter, TerminalFormatter, TsvFormatter
12
+ from linkml.linter.linter import Linter
14
13
 
15
14
  YAML_SUFFIXES = [".yml", ".yaml"]
16
15
  DEFAULT_CONFIG_FILES = [".linkmllint.yaml", ".linkmllint.yml"]