rdf-construct 0.3.0__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 (110) hide show
  1. rdf_construct/__init__.py +12 -0
  2. rdf_construct/__main__.py +0 -0
  3. rdf_construct/cli.py +3429 -0
  4. rdf_construct/core/__init__.py +33 -0
  5. rdf_construct/core/config.py +116 -0
  6. rdf_construct/core/ordering.py +219 -0
  7. rdf_construct/core/predicate_order.py +212 -0
  8. rdf_construct/core/profile.py +157 -0
  9. rdf_construct/core/selector.py +64 -0
  10. rdf_construct/core/serialiser.py +232 -0
  11. rdf_construct/core/utils.py +89 -0
  12. rdf_construct/cq/__init__.py +77 -0
  13. rdf_construct/cq/expectations.py +365 -0
  14. rdf_construct/cq/formatters/__init__.py +45 -0
  15. rdf_construct/cq/formatters/json.py +104 -0
  16. rdf_construct/cq/formatters/junit.py +104 -0
  17. rdf_construct/cq/formatters/text.py +146 -0
  18. rdf_construct/cq/loader.py +300 -0
  19. rdf_construct/cq/runner.py +321 -0
  20. rdf_construct/diff/__init__.py +59 -0
  21. rdf_construct/diff/change_types.py +214 -0
  22. rdf_construct/diff/comparator.py +338 -0
  23. rdf_construct/diff/filters.py +133 -0
  24. rdf_construct/diff/formatters/__init__.py +71 -0
  25. rdf_construct/diff/formatters/json.py +192 -0
  26. rdf_construct/diff/formatters/markdown.py +210 -0
  27. rdf_construct/diff/formatters/text.py +195 -0
  28. rdf_construct/docs/__init__.py +60 -0
  29. rdf_construct/docs/config.py +238 -0
  30. rdf_construct/docs/extractors.py +603 -0
  31. rdf_construct/docs/generator.py +360 -0
  32. rdf_construct/docs/renderers/__init__.py +7 -0
  33. rdf_construct/docs/renderers/html.py +803 -0
  34. rdf_construct/docs/renderers/json.py +390 -0
  35. rdf_construct/docs/renderers/markdown.py +628 -0
  36. rdf_construct/docs/search.py +278 -0
  37. rdf_construct/docs/templates/html/base.html.jinja +44 -0
  38. rdf_construct/docs/templates/html/class.html.jinja +152 -0
  39. rdf_construct/docs/templates/html/hierarchy.html.jinja +28 -0
  40. rdf_construct/docs/templates/html/index.html.jinja +110 -0
  41. rdf_construct/docs/templates/html/instance.html.jinja +90 -0
  42. rdf_construct/docs/templates/html/namespaces.html.jinja +37 -0
  43. rdf_construct/docs/templates/html/property.html.jinja +124 -0
  44. rdf_construct/docs/templates/html/single_page.html.jinja +169 -0
  45. rdf_construct/lint/__init__.py +75 -0
  46. rdf_construct/lint/config.py +214 -0
  47. rdf_construct/lint/engine.py +396 -0
  48. rdf_construct/lint/formatters.py +327 -0
  49. rdf_construct/lint/rules.py +692 -0
  50. rdf_construct/localise/__init__.py +114 -0
  51. rdf_construct/localise/config.py +508 -0
  52. rdf_construct/localise/extractor.py +427 -0
  53. rdf_construct/localise/formatters/__init__.py +36 -0
  54. rdf_construct/localise/formatters/markdown.py +229 -0
  55. rdf_construct/localise/formatters/text.py +224 -0
  56. rdf_construct/localise/merger.py +346 -0
  57. rdf_construct/localise/reporter.py +356 -0
  58. rdf_construct/main.py +6 -0
  59. rdf_construct/merge/__init__.py +165 -0
  60. rdf_construct/merge/config.py +354 -0
  61. rdf_construct/merge/conflicts.py +281 -0
  62. rdf_construct/merge/formatters.py +426 -0
  63. rdf_construct/merge/merger.py +425 -0
  64. rdf_construct/merge/migrator.py +339 -0
  65. rdf_construct/merge/rules.py +377 -0
  66. rdf_construct/merge/splitter.py +1102 -0
  67. rdf_construct/puml2rdf/__init__.py +103 -0
  68. rdf_construct/puml2rdf/config.py +230 -0
  69. rdf_construct/puml2rdf/converter.py +420 -0
  70. rdf_construct/puml2rdf/merger.py +200 -0
  71. rdf_construct/puml2rdf/model.py +202 -0
  72. rdf_construct/puml2rdf/parser.py +565 -0
  73. rdf_construct/puml2rdf/validators.py +451 -0
  74. rdf_construct/refactor/__init__.py +72 -0
  75. rdf_construct/refactor/config.py +362 -0
  76. rdf_construct/refactor/deprecator.py +328 -0
  77. rdf_construct/refactor/formatters/__init__.py +8 -0
  78. rdf_construct/refactor/formatters/text.py +311 -0
  79. rdf_construct/refactor/renamer.py +294 -0
  80. rdf_construct/shacl/__init__.py +56 -0
  81. rdf_construct/shacl/config.py +166 -0
  82. rdf_construct/shacl/converters.py +520 -0
  83. rdf_construct/shacl/generator.py +364 -0
  84. rdf_construct/shacl/namespaces.py +93 -0
  85. rdf_construct/stats/__init__.py +29 -0
  86. rdf_construct/stats/collector.py +178 -0
  87. rdf_construct/stats/comparator.py +298 -0
  88. rdf_construct/stats/formatters/__init__.py +83 -0
  89. rdf_construct/stats/formatters/json.py +38 -0
  90. rdf_construct/stats/formatters/markdown.py +153 -0
  91. rdf_construct/stats/formatters/text.py +186 -0
  92. rdf_construct/stats/metrics/__init__.py +26 -0
  93. rdf_construct/stats/metrics/basic.py +147 -0
  94. rdf_construct/stats/metrics/complexity.py +137 -0
  95. rdf_construct/stats/metrics/connectivity.py +130 -0
  96. rdf_construct/stats/metrics/documentation.py +128 -0
  97. rdf_construct/stats/metrics/hierarchy.py +207 -0
  98. rdf_construct/stats/metrics/properties.py +88 -0
  99. rdf_construct/uml/__init__.py +22 -0
  100. rdf_construct/uml/context.py +194 -0
  101. rdf_construct/uml/mapper.py +371 -0
  102. rdf_construct/uml/odm_renderer.py +789 -0
  103. rdf_construct/uml/renderer.py +684 -0
  104. rdf_construct/uml/uml_layout.py +393 -0
  105. rdf_construct/uml/uml_style.py +613 -0
  106. rdf_construct-0.3.0.dist-info/METADATA +496 -0
  107. rdf_construct-0.3.0.dist-info/RECORD +110 -0
  108. rdf_construct-0.3.0.dist-info/WHEEL +4 -0
  109. rdf_construct-0.3.0.dist-info/entry_points.txt +3 -0
  110. rdf_construct-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,451 @@
1
+ """Validation for PlantUML import.
2
+
3
+ This module provides validation of parsed PlantUML models and
4
+ generated RDF, ensuring consistency and flagging potential issues.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import Optional
10
+
11
+ from rdflib import Graph, URIRef, RDF, RDFS
12
+ from rdflib.namespace import OWL
13
+
14
+ from rdf_construct.puml2rdf.model import PumlModel, PumlRelationship, RelationshipType
15
+
16
+
17
+ class Severity(Enum):
18
+ """Severity level for validation issues."""
19
+
20
+ ERROR = "error" # Must be fixed
21
+ WARNING = "warning" # Should be reviewed
22
+ INFO = "info" # Informational
23
+
24
+
25
+ @dataclass
26
+ class ValidationIssue:
27
+ """A validation issue found during checking.
28
+
29
+ Attributes:
30
+ severity: How serious the issue is
31
+ code: Machine-readable issue code
32
+ message: Human-readable description
33
+ entity: The entity this issue relates to
34
+ suggestion: Optional fix suggestion
35
+ """
36
+
37
+ severity: Severity
38
+ code: str
39
+ message: str
40
+ entity: Optional[str] = None
41
+ suggestion: Optional[str] = None
42
+
43
+ def __str__(self) -> str:
44
+ prefix = f"[{self.severity.value.upper()}]"
45
+ entity_str = f" ({self.entity})" if self.entity else ""
46
+ return f"{prefix} {self.code}: {self.message}{entity_str}"
47
+
48
+
49
+ @dataclass
50
+ class ValidationResult:
51
+ """Result of validation.
52
+
53
+ Attributes:
54
+ issues: List of validation issues found
55
+ """
56
+
57
+ issues: list[ValidationIssue]
58
+
59
+ @property
60
+ def has_errors(self) -> bool:
61
+ """Return True if any errors were found."""
62
+ return any(i.severity == Severity.ERROR for i in self.issues)
63
+
64
+ @property
65
+ def has_warnings(self) -> bool:
66
+ """Return True if any warnings were found."""
67
+ return any(i.severity == Severity.WARNING for i in self.issues)
68
+
69
+ @property
70
+ def error_count(self) -> int:
71
+ """Count of errors."""
72
+ return sum(1 for i in self.issues if i.severity == Severity.ERROR)
73
+
74
+ @property
75
+ def warning_count(self) -> int:
76
+ """Count of warnings."""
77
+ return sum(1 for i in self.issues if i.severity == Severity.WARNING)
78
+
79
+ def errors(self) -> list[ValidationIssue]:
80
+ """Return only error-level issues."""
81
+ return [i for i in self.issues if i.severity == Severity.ERROR]
82
+
83
+ def warnings(self) -> list[ValidationIssue]:
84
+ """Return only warning-level issues."""
85
+ return [i for i in self.issues if i.severity == Severity.WARNING]
86
+
87
+
88
+ class PumlModelValidator:
89
+ """Validates parsed PlantUML models for consistency.
90
+
91
+ Checks include:
92
+ - Relationships reference existing classes
93
+ - No duplicate class names
94
+ - Attributes have valid types
95
+ - Inheritance doesn't create cycles
96
+ """
97
+
98
+ def validate(self, model: PumlModel) -> ValidationResult:
99
+ """Validate a parsed PlantUML model.
100
+
101
+ Args:
102
+ model: The parsed model to validate
103
+
104
+ Returns:
105
+ ValidationResult with any issues found
106
+ """
107
+ issues: list[ValidationIssue] = []
108
+
109
+ # Check for duplicate class names
110
+ issues.extend(self._check_duplicate_classes(model))
111
+
112
+ # Check relationship references
113
+ issues.extend(self._check_relationship_references(model))
114
+
115
+ # Check for inheritance cycles
116
+ issues.extend(self._check_inheritance_cycles(model))
117
+
118
+ # Check attribute types
119
+ issues.extend(self._check_attribute_types(model))
120
+
121
+ # Check for classes without any relationships
122
+ issues.extend(self._check_orphan_classes(model))
123
+
124
+ return ValidationResult(issues=issues)
125
+
126
+ def _check_duplicate_classes(self, model: PumlModel) -> list[ValidationIssue]:
127
+ """Check for duplicate class names."""
128
+ issues = []
129
+ seen = set()
130
+
131
+ for cls in model.classes:
132
+ if cls.name in seen:
133
+ issues.append(
134
+ ValidationIssue(
135
+ severity=Severity.ERROR,
136
+ code="DUPLICATE_CLASS",
137
+ message=f"Duplicate class name: {cls.name}",
138
+ entity=cls.name,
139
+ suggestion="Rename one of the classes or use packages to distinguish them",
140
+ )
141
+ )
142
+ seen.add(cls.name)
143
+
144
+ return issues
145
+
146
+ def _check_relationship_references(self, model: PumlModel) -> list[ValidationIssue]:
147
+ """Check that relationships reference existing classes."""
148
+ issues = []
149
+
150
+ # Include both local and qualified names for lookup
151
+ class_names = set()
152
+ for cls in model.classes:
153
+ class_names.add(cls.name) # Local name: "Building"
154
+ class_names.add(cls.qualified_name) # Qualified: "building.Building"
155
+
156
+ for rel in model.relationships:
157
+ if rel.source not in class_names:
158
+ issues.append(
159
+ ValidationIssue(
160
+ severity=Severity.ERROR,
161
+ code="UNKNOWN_CLASS",
162
+ message=f"Relationship references unknown source class: {rel.source}",
163
+ entity=rel.source,
164
+ suggestion=f"Add class declaration for '{rel.source}'",
165
+ )
166
+ )
167
+ if rel.target not in class_names:
168
+ issues.append(
169
+ ValidationIssue(
170
+ severity=Severity.ERROR,
171
+ code="UNKNOWN_CLASS",
172
+ message=f"Relationship references unknown target class: {rel.target}",
173
+ entity=rel.target,
174
+ suggestion=f"Add class declaration for '{rel.target}'",
175
+ )
176
+ )
177
+
178
+ return issues
179
+
180
+ def _check_inheritance_cycles(self, model: PumlModel) -> list[ValidationIssue]:
181
+ """Check for cycles in inheritance hierarchy."""
182
+ issues = []
183
+
184
+ # Build inheritance graph
185
+ inheritance: dict[str, set[str]] = {}
186
+ for rel in model.inheritance_relationships():
187
+ if rel.source not in inheritance:
188
+ inheritance[rel.source] = set()
189
+ inheritance[rel.source].add(rel.target)
190
+
191
+ # Check for cycles using DFS
192
+ def has_cycle(start: str, visited: set[str], path: list[str]) -> Optional[list[str]]:
193
+ if start in path:
194
+ cycle_start = path.index(start)
195
+ return path[cycle_start:] + [start]
196
+
197
+ if start in visited:
198
+ return None
199
+
200
+ visited.add(start)
201
+ path.append(start)
202
+
203
+ for parent in inheritance.get(start, set()):
204
+ cycle = has_cycle(parent, visited, path)
205
+ if cycle:
206
+ return cycle
207
+
208
+ path.pop()
209
+ return None
210
+
211
+ for cls in model.classes:
212
+ cycle = has_cycle(cls.name, set(), [])
213
+ if cycle:
214
+ issues.append(
215
+ ValidationIssue(
216
+ severity=Severity.ERROR,
217
+ code="INHERITANCE_CYCLE",
218
+ message=f"Inheritance cycle detected: {' -> '.join(cycle)}",
219
+ entity=cls.name,
220
+ suggestion="Break the cycle by removing one of the inheritance relationships",
221
+ )
222
+ )
223
+ break # Only report once
224
+
225
+ return issues
226
+
227
+ def _check_attribute_types(self, model: PumlModel) -> list[ValidationIssue]:
228
+ """Check that attribute types are recognized."""
229
+ issues = []
230
+
231
+ known_types = {
232
+ "string", "str", "text",
233
+ "integer", "int",
234
+ "decimal", "float", "double", "number",
235
+ "boolean", "bool",
236
+ "date", "datetime", "time",
237
+ "gYear", "gyear", "gYearMonth",
238
+ "duration",
239
+ "uri", "anyURI", "anyuri", "url",
240
+ "base64", "hexBinary",
241
+ "language", "token",
242
+ }
243
+
244
+ for cls in model.classes:
245
+ for attr in cls.attributes:
246
+ if attr.datatype and attr.datatype.lower() not in known_types:
247
+ if not attr.datatype.startswith("xsd:"):
248
+ issues.append(
249
+ ValidationIssue(
250
+ severity=Severity.WARNING,
251
+ code="UNKNOWN_DATATYPE",
252
+ message=f"Unknown datatype '{attr.datatype}' for attribute '{attr.name}'",
253
+ entity=f"{cls.name}.{attr.name}",
254
+ suggestion="Use standard XSD type or add custom mapping in config",
255
+ )
256
+ )
257
+
258
+ return issues
259
+
260
+ def _check_orphan_classes(self, model: PumlModel) -> list[ValidationIssue]:
261
+ """Check for classes with no relationships."""
262
+ issues = []
263
+
264
+ # Get all classes involved in relationships
265
+ related_classes = set()
266
+ for rel in model.relationships:
267
+ related_classes.add(rel.source)
268
+ related_classes.add(rel.target)
269
+
270
+ for cls in model.classes:
271
+ if cls.name not in related_classes and not cls.attributes:
272
+ issues.append(
273
+ ValidationIssue(
274
+ severity=Severity.INFO,
275
+ code="ISOLATED_CLASS",
276
+ message=f"Class '{cls.name}' has no relationships or attributes",
277
+ entity=cls.name,
278
+ suggestion="Consider adding relationships or attributes",
279
+ )
280
+ )
281
+
282
+ return issues
283
+
284
+
285
+ class RdfValidator:
286
+ """Validates generated RDF for OWL/RDFS consistency.
287
+
288
+ Checks include:
289
+ - Classes are typed as owl:Class
290
+ - Properties have domain and range
291
+ - No dangling references
292
+ """
293
+
294
+ def validate(self, graph: Graph) -> ValidationResult:
295
+ """Validate an RDF graph.
296
+
297
+ Args:
298
+ graph: The graph to validate
299
+
300
+ Returns:
301
+ ValidationResult with any issues found
302
+ """
303
+ issues: list[ValidationIssue] = []
304
+
305
+ # Check class typing
306
+ issues.extend(self._check_class_typing(graph))
307
+
308
+ # Check property completeness
309
+ issues.extend(self._check_property_completeness(graph))
310
+
311
+ # Check for dangling references
312
+ issues.extend(self._check_dangling_references(graph))
313
+
314
+ return ValidationResult(issues=issues)
315
+
316
+ def _check_class_typing(self, graph: Graph) -> list[ValidationIssue]:
317
+ """Check that classes are properly typed."""
318
+ issues = []
319
+
320
+ # Find subjects of rdfs:subClassOf that aren't typed as classes
321
+ for s in graph.subjects(RDFS.subClassOf, None):
322
+ if not isinstance(s, URIRef):
323
+ continue
324
+
325
+ is_class = (
326
+ (s, RDF.type, OWL.Class) in graph
327
+ or (s, RDF.type, RDFS.Class) in graph
328
+ )
329
+ if not is_class:
330
+ issues.append(
331
+ ValidationIssue(
332
+ severity=Severity.WARNING,
333
+ code="UNTYPED_CLASS",
334
+ message=f"Subject of rdfs:subClassOf not typed as class",
335
+ entity=str(s),
336
+ suggestion="Add rdf:type owl:Class triple",
337
+ )
338
+ )
339
+
340
+ return issues
341
+
342
+ def _check_property_completeness(self, graph: Graph) -> list[ValidationIssue]:
343
+ """Check that properties have domain and range."""
344
+ issues = []
345
+
346
+ for prop in graph.subjects(RDF.type, OWL.ObjectProperty):
347
+ if not isinstance(prop, URIRef):
348
+ continue
349
+
350
+ has_domain = any(graph.objects(prop, RDFS.domain))
351
+ has_range = any(graph.objects(prop, RDFS.range))
352
+
353
+ if not has_domain:
354
+ issues.append(
355
+ ValidationIssue(
356
+ severity=Severity.INFO,
357
+ code="MISSING_DOMAIN",
358
+ message="Object property has no domain",
359
+ entity=str(prop),
360
+ )
361
+ )
362
+ if not has_range:
363
+ issues.append(
364
+ ValidationIssue(
365
+ severity=Severity.INFO,
366
+ code="MISSING_RANGE",
367
+ message="Object property has no range",
368
+ entity=str(prop),
369
+ )
370
+ )
371
+
372
+ for prop in graph.subjects(RDF.type, OWL.DatatypeProperty):
373
+ if not isinstance(prop, URIRef):
374
+ continue
375
+
376
+ has_range = any(graph.objects(prop, RDFS.range))
377
+ if not has_range:
378
+ issues.append(
379
+ ValidationIssue(
380
+ severity=Severity.INFO,
381
+ code="MISSING_RANGE",
382
+ message="Datatype property has no range (XSD type)",
383
+ entity=str(prop),
384
+ )
385
+ )
386
+
387
+ return issues
388
+
389
+ def _check_dangling_references(self, graph: Graph) -> list[ValidationIssue]:
390
+ """Check for references to undefined entities."""
391
+ issues = []
392
+
393
+ # Get all defined classes
394
+ defined_classes = set()
395
+ for cls in graph.subjects(RDF.type, OWL.Class):
396
+ defined_classes.add(cls)
397
+ for cls in graph.subjects(RDF.type, RDFS.Class):
398
+ defined_classes.add(cls)
399
+
400
+ # Check domain and range references
401
+ for prop in graph.subjects(RDF.type, OWL.ObjectProperty):
402
+ for domain in graph.objects(prop, RDFS.domain):
403
+ if isinstance(domain, URIRef) and domain not in defined_classes:
404
+ issues.append(
405
+ ValidationIssue(
406
+ severity=Severity.WARNING,
407
+ code="UNDEFINED_DOMAIN",
408
+ message=f"Property domain references undefined class",
409
+ entity=f"{prop} -> {domain}",
410
+ )
411
+ )
412
+
413
+ for rng in graph.objects(prop, RDFS.range):
414
+ if isinstance(rng, URIRef) and rng not in defined_classes:
415
+ # Could be external class - just info
416
+ issues.append(
417
+ ValidationIssue(
418
+ severity=Severity.INFO,
419
+ code="EXTERNAL_RANGE",
420
+ message=f"Property range references class not in this graph",
421
+ entity=f"{prop} -> {rng}",
422
+ )
423
+ )
424
+
425
+ return issues
426
+
427
+
428
+ def validate_puml(model: PumlModel) -> ValidationResult:
429
+ """Convenience function to validate a PlantUML model.
430
+
431
+ Args:
432
+ model: The parsed model to validate
433
+
434
+ Returns:
435
+ ValidationResult with any issues found
436
+ """
437
+ validator = PumlModelValidator()
438
+ return validator.validate(model)
439
+
440
+
441
+ def validate_rdf(graph: Graph) -> ValidationResult:
442
+ """Convenience function to validate an RDF graph.
443
+
444
+ Args:
445
+ graph: The graph to validate
446
+
447
+ Returns:
448
+ ValidationResult with any issues found
449
+ """
450
+ validator = RdfValidator()
451
+ return validator.validate(graph)
@@ -0,0 +1,72 @@
1
+ """Refactor module for URI renaming and deprecation.
2
+
3
+ This module provides tools for common ontology maintenance tasks:
4
+ - Renaming URIs (single entities or bulk namespace changes)
5
+ - Deprecating entities with proper OWL annotations
6
+ - Data migration for instance graphs
7
+
8
+ Example usage:
9
+ from rdf_construct.refactor import OntologyRenamer, RenameConfig
10
+
11
+ renamer = OntologyRenamer()
12
+ config = RenameConfig(entities={
13
+ "http://example.org/Buiding": "http://example.org/Building"
14
+ })
15
+ result = renamer.rename(graph, config)
16
+ """
17
+
18
+ from rdf_construct.refactor.config import (
19
+ RenameConfig,
20
+ RenameMapping,
21
+ DeprecationSpec,
22
+ DeprecationConfig,
23
+ RefactorConfig,
24
+ DataMigrationSpec,
25
+ load_refactor_config,
26
+ create_default_rename_config,
27
+ create_default_deprecation_config,
28
+ )
29
+ from rdf_construct.refactor.renamer import (
30
+ OntologyRenamer,
31
+ RenameResult,
32
+ RenameStats,
33
+ rename_file,
34
+ rename_files,
35
+ )
36
+ from rdf_construct.refactor.deprecator import (
37
+ OntologyDeprecator,
38
+ DeprecationResult,
39
+ DeprecationStats,
40
+ EntityDeprecationInfo,
41
+ deprecate_file,
42
+ generate_deprecation_message,
43
+ )
44
+ from rdf_construct.refactor.formatters import TextFormatter
45
+
46
+ __all__ = [
47
+ # Config
48
+ "RenameConfig",
49
+ "RenameMapping",
50
+ "DeprecationSpec",
51
+ "DeprecationConfig",
52
+ "RefactorConfig",
53
+ "DataMigrationSpec",
54
+ "load_refactor_config",
55
+ "create_default_rename_config",
56
+ "create_default_deprecation_config",
57
+ # Renamer
58
+ "OntologyRenamer",
59
+ "RenameResult",
60
+ "RenameStats",
61
+ "rename_file",
62
+ "rename_files",
63
+ # Deprecator
64
+ "OntologyDeprecator",
65
+ "DeprecationResult",
66
+ "DeprecationStats",
67
+ "EntityDeprecationInfo",
68
+ "deprecate_file",
69
+ "generate_deprecation_message",
70
+ # Formatters
71
+ "TextFormatter",
72
+ ]