flamapy-fm 2.1.0.dev0__tar.gz → 2.1.0.dev1__tar.gz

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 (62) hide show
  1. flamapy_fm-2.1.0.dev1/PKG-INFO +43 -0
  2. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/models/__init__.py +4 -2
  3. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/models/feature_model.py +57 -5
  4. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/uvl_reader.py +33 -1
  5. flamapy_fm-2.1.0.dev1/flamapy_fm.egg-info/PKG-INFO +43 -0
  6. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy_fm.egg-info/requires.txt +1 -1
  7. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/setup.py +1 -1
  8. flamapy-fm-2.1.0.dev0/PKG-INFO +0 -28
  9. flamapy-fm-2.1.0.dev0/flamapy_fm.egg-info/PKG-INFO +0 -28
  10. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/README.md +0 -0
  11. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/__init__.py +0 -0
  12. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/__init__.py +0 -0
  13. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/fm_atomic_sets.py +0 -0
  14. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/fm_average_branching_factor.py +0 -0
  15. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/fm_core_features.py +0 -0
  16. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/fm_count_leafs.py +0 -0
  17. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/fm_estimated_configurations_number.py +0 -0
  18. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/fm_feature_ancestors.py +0 -0
  19. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/fm_generate_random_attribute.py +0 -0
  20. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/fm_language_level.py +0 -0
  21. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +0 -0
  22. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/fm_max_depth_tree.py +0 -0
  23. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/fm_metrics.py +0 -0
  24. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/fm_variation_points.py +0 -0
  25. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/interfaces/__init__.py +0 -0
  26. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/operations/interfaces/variation_points.py +0 -0
  27. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/__init__.py +0 -0
  28. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/afm_reader.py +0 -0
  29. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/afm_writer.py +0 -0
  30. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/clafer_writer.py +0 -0
  31. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/featureide_reader.py +0 -0
  32. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/featureide_writer.py +0 -0
  33. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/flat_fm.py +0 -0
  34. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/fm_secure_features_names.py +0 -0
  35. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/glencoe_reader.py +0 -0
  36. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/glencoe_writer.py +0 -0
  37. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/json_reader.py +0 -0
  38. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/json_writer.py +0 -0
  39. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/pl_writer.py +0 -0
  40. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/pysat_to_fm.py +0 -0
  41. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/__init__.py +0 -0
  42. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/cardinality_group_refactoring.py +0 -0
  43. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/commitment_feature.py +0 -0
  44. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/deletion_feature.py +0 -0
  45. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring.py +0 -0
  46. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/feature_cardinality_refactoring_paper.py +0 -0
  47. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/multiple_group_decomposition_refactoring.py +0 -0
  48. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/mutex_group_refactoring.py +0 -0
  49. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/or_mandatory_refactoring.py +0 -0
  50. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/pseudocomplex_constraint_refactoring.py +0 -0
  51. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_exception.py +0 -0
  52. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/refactoring_interface.py +0 -0
  53. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/strictcomplex_constraint_refactoring.py +0 -0
  54. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/refactorings/xor_mandatory_refactoring.py +0 -0
  55. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/splot_writer.py +0 -0
  56. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/uvl_writer.py +0 -0
  57. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy/metamodels/fm_metamodel/transformations/xml_reader.py +0 -0
  58. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy_fm.egg-info/SOURCES.txt +0 -0
  59. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy_fm.egg-info/dependency_links.txt +0 -0
  60. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/flamapy_fm.egg-info/top_level.txt +0 -0
  61. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/pyproject.toml +0 -0
  62. {flamapy-fm-2.1.0.dev0 → flamapy_fm-2.1.0.dev1}/setup.cfg +0 -0
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: flamapy-fm
3
+ Version: 2.1.0.dev1
4
+ Summary: flamapy-fm is a plugin to Flamapy module
5
+ Home-page: https://github.com/flamapy/fm_metamodel
6
+ Author: Flamapy
7
+ Author-email: flamapy@us.es
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: flamapy-fw~=2.1.0.dev1
14
+ Requires-Dist: uvlparser~=2.0.1
15
+ Requires-Dist: afmparser~=1.0.3
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest; extra == "dev"
18
+ Requires-Dist: pytest-mock; extra == "dev"
19
+ Requires-Dist: prospector; extra == "dev"
20
+ Requires-Dist: mypy; extra == "dev"
21
+ Requires-Dist: coverage; extra == "dev"
22
+ Requires-Dist: antlr4-tools; extra == "dev"
23
+ Dynamic: author
24
+ Dynamic: author-email
25
+ Dynamic: classifier
26
+ Dynamic: description
27
+ Dynamic: description-content-type
28
+ Dynamic: home-page
29
+ Dynamic: provides-extra
30
+ Dynamic: requires-dist
31
+ Dynamic: requires-python
32
+ Dynamic: summary
33
+
34
+ # fm_metamodel
35
+
36
+ This repo host the feature model concrete classes
37
+
38
+
39
+ ## Install for development
40
+
41
+ ```
42
+ pip install -e .
43
+ ```
@@ -7,11 +7,13 @@ from .feature_model import (
7
7
  Range,
8
8
  Attribute,
9
9
  Cardinality,
10
- FeatureType
10
+ FeatureType,
11
+ AttributeType
11
12
  )
12
13
 
13
14
  __all__ = [
14
15
  'Attribute',
16
+ 'AttributeType',
15
17
  'Cardinality',
16
18
  'Constraint',
17
19
  'Domain',
@@ -19,5 +21,5 @@ __all__ = [
19
21
  'FeatureModel',
20
22
  'FeatureType',
21
23
  'Range',
22
- 'Relation',
24
+ 'Relation'
23
25
  ]
@@ -99,6 +99,15 @@ class FeatureType(Enum):
99
99
  STRING = 'String'
100
100
 
101
101
 
102
+ class AttributeType(Enum):
103
+ BOOLEAN = 'Boolean'
104
+ INTEGER = 'Integer'
105
+ REAL = 'Real'
106
+ STRING = 'String'
107
+ VECTOR = 'Vector' # Vector (list of values)
108
+ NESTED = 'Nested' # Nested attribute (i.e., list of attributes)
109
+
110
+
102
111
  class Cardinality:
103
112
 
104
113
  def __init__(self, card_min: int = 1, card_max: int = 1):
@@ -401,6 +410,20 @@ class FeatureModel(VariabilityModel):
401
410
  features.extend(relation.children)
402
411
  return features
403
412
 
413
+ def get_attributes(self) -> list["Attribute"]:
414
+ attributes_dict: dict[str, "Attribute"] = {}
415
+ for feature in self.get_features():
416
+ attributes_dict.update({attr.name: attr for attr in feature.get_attributes()})
417
+ attributes = []
418
+ for attr in attributes_dict.values():
419
+ attribute = Attribute(attr.name, attr.domain, None, attr.null_value)
420
+ attribute.attribute_type = attr.attribute_type
421
+ attributes.append(attribute)
422
+ return attributes
423
+
424
+ def get_attribute_by_name(self, attribute_name: str) -> Optional["Attribute"]:
425
+ return next((a for a in self.get_attributes() if a.name == attribute_name), None)
426
+
404
427
  def get_boolean_features(self) -> list["Feature"]:
405
428
  return [f for f in self.get_features() if f.is_boolean()]
406
429
 
@@ -468,14 +491,23 @@ class FeatureModel(VariabilityModel):
468
491
  res += "Relations:\r\n"
469
492
  for i, relation in enumerate(self.get_relations()):
470
493
  res += f"R{i}: {relation}\r\n"
494
+ res += "Constraints:\r\n"
471
495
  for i, ctc in enumerate(self.ctcs):
472
496
  res += f"CTC{i}: {ctc}\r\n"
473
- attributes_res = ""
497
+ res += "Attributes:\r\n"
498
+ attr_counter = 0
474
499
  for feature in self.get_features():
475
500
  for attribute in feature.get_attributes():
476
- attributes_res += f"{attribute}" + "\r\n"
477
- if attributes_res != "":
478
- res += "Attributes:\r\n" + attributes_res
501
+ res += f"ATTR{attr_counter}: {attribute}" + "\r\n"
502
+ attr_counter += 1
503
+ res += "Imports:\r\n"
504
+ for i, (name, imp) in enumerate(self.imports.items()):
505
+ alias = next((alias for alias, namespace in self.alias_namespace.items()
506
+ if namespace == name), None)
507
+ if alias is None:
508
+ res += f"IMP{i}: {name} -> {imp}\r\n"
509
+ else:
510
+ res += f"IMP{i}: {name} ({alias}) -> {imp}\r\n"
479
511
  return res
480
512
 
481
513
  def __hash__(self) -> int:
@@ -560,6 +592,22 @@ class Attribute:
560
592
  self.domain: Optional["Domain"] = domain
561
593
  self.default_value: "Any" = default_value
562
594
  self.null_value: Optional[Any] = null_value
595
+ self.attribute_type: Optional["AttributeType"] = self._infer_attribute_type(default_value)
596
+
597
+ def _infer_attribute_type(self, value: Any) -> Optional["AttributeType"]:
598
+ if isinstance(value, bool):
599
+ attr_type = AttributeType.BOOLEAN
600
+ elif isinstance(value, int):
601
+ attr_type = AttributeType.INTEGER
602
+ elif isinstance(value, float):
603
+ attr_type = AttributeType.REAL
604
+ elif isinstance(value, str):
605
+ attr_type = AttributeType.STRING
606
+ elif isinstance(value, list):
607
+ attr_type = AttributeType.VECTOR
608
+ else:
609
+ attr_type = AttributeType.NESTED
610
+ return attr_type
563
611
 
564
612
  def get_name(self) -> str:
565
613
  return self.name
@@ -601,7 +649,11 @@ class Attribute:
601
649
  if self.domain is not None:
602
650
  result = result + "Domain: " + str(self.domain)
603
651
  if self.default_value is not None:
604
- result = result + "Default value: " + str(self.default_value)
652
+ if isinstance(self.default_value, list):
653
+ default_value_str = '[' + ', '.join(str(v) for v in self.default_value) + ']'
654
+ else:
655
+ default_value_str = str(self.default_value)
656
+ result = result + "Default value: " + default_value_str
605
657
  if self.null_value is not None:
606
658
  result = result + "Null value: " + str(self.null_value)
607
659
 
@@ -182,9 +182,41 @@ class UVLReader(TextToModel):
182
182
  if key == "abstract" and (value is None or value):
183
183
  feature.is_abstract = True
184
184
  else:
185
- feature.add_attribute(Attribute(name=str(key), default_value=value))
185
+ # Handle attributes
186
+ if value is None: # for boolean values the value may be not provided
187
+ default_value = True
188
+ elif isinstance(value, dict): # it represents nested attributes
189
+ attributes_list = self._process_nested_attribute(feature, str(key), value)
190
+ for attr in attributes_list:
191
+ feature.add_attribute(attr)
192
+ default_value = None
193
+ else:
194
+ default_value = value
195
+ feature.add_attribute(Attribute(name=str(key), default_value=default_value))
186
196
  feature.constraints_attributes = self.constraints_attributes[feature]
187
197
 
198
+ def _process_nested_attribute(self,
199
+ parent: Feature,
200
+ parent_attribute_name: str,
201
+ nested_values: dict[Any, Any]) -> list[Attribute]:
202
+ attributes = []
203
+ for key, value in nested_values.items():
204
+ if value is None: # for boolean values the value may be not provided
205
+ default_value = True
206
+ elif isinstance(value, dict):
207
+ attributes_list = self._process_nested_attribute(parent,
208
+ f'{parent_attribute_name}.{key}',
209
+ value)
210
+ for attr in attributes_list:
211
+ attributes.append(attr)
212
+ default_value = None
213
+ else:
214
+ default_value = value
215
+ attribute = Attribute(name=f'{parent_attribute_name}.{key}',
216
+ default_value=default_value)
217
+ attribute.parent = parent
218
+ attributes.append(attribute)
219
+ return attributes
188
220
 
189
221
  def _process_imported_feature(self, feature: Feature) -> None:
190
222
  feature_reference = feature.name.split('.')
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: flamapy-fm
3
+ Version: 2.1.0.dev1
4
+ Summary: flamapy-fm is a plugin to Flamapy module
5
+ Home-page: https://github.com/flamapy/fm_metamodel
6
+ Author: Flamapy
7
+ Author-email: flamapy@us.es
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: flamapy-fw~=2.1.0.dev1
14
+ Requires-Dist: uvlparser~=2.0.1
15
+ Requires-Dist: afmparser~=1.0.3
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest; extra == "dev"
18
+ Requires-Dist: pytest-mock; extra == "dev"
19
+ Requires-Dist: prospector; extra == "dev"
20
+ Requires-Dist: mypy; extra == "dev"
21
+ Requires-Dist: coverage; extra == "dev"
22
+ Requires-Dist: antlr4-tools; extra == "dev"
23
+ Dynamic: author
24
+ Dynamic: author-email
25
+ Dynamic: classifier
26
+ Dynamic: description
27
+ Dynamic: description-content-type
28
+ Dynamic: home-page
29
+ Dynamic: provides-extra
30
+ Dynamic: requires-dist
31
+ Dynamic: requires-python
32
+ Dynamic: summary
33
+
34
+ # fm_metamodel
35
+
36
+ This repo host the feature model concrete classes
37
+
38
+
39
+ ## Install for development
40
+
41
+ ```
42
+ pip install -e .
43
+ ```
@@ -1,4 +1,4 @@
1
- flamapy-fw~=2.1.0.dev0
1
+ flamapy-fw~=2.1.0.dev1
2
2
  uvlparser~=2.0.1
3
3
  afmparser~=1.0.3
4
4
 
@@ -16,7 +16,7 @@ dev_requirements = read_requirements("requirements-dev.txt")
16
16
 
17
17
  setuptools.setup(
18
18
  name="flamapy-fm",
19
- version="2.1.0.dev0",
19
+ version="2.1.0.dev1",
20
20
  author="Flamapy",
21
21
  author_email="flamapy@us.es",
22
22
  description="flamapy-fm is a plugin to Flamapy module",
@@ -1,28 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: flamapy-fm
3
- Version: 2.1.0.dev0
4
- Summary: flamapy-fm is a plugin to Flamapy module
5
- Home-page: https://github.com/flamapy/fm_metamodel
6
- Author: Flamapy
7
- Author-email: flamapy@us.es
8
- License: UNKNOWN
9
- Platform: UNKNOWN
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
12
- Classifier: Operating System :: OS Independent
13
- Requires-Python: >=3.9
14
- Description-Content-Type: text/markdown
15
- Provides-Extra: dev
16
-
17
- # fm_metamodel
18
-
19
- This repo host the feature model concrete classes
20
-
21
-
22
- ## Install for development
23
-
24
- ```
25
- pip install -e .
26
- ```
27
-
28
-
@@ -1,28 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: flamapy-fm
3
- Version: 2.1.0.dev0
4
- Summary: flamapy-fm is a plugin to Flamapy module
5
- Home-page: https://github.com/flamapy/fm_metamodel
6
- Author: Flamapy
7
- Author-email: flamapy@us.es
8
- License: UNKNOWN
9
- Platform: UNKNOWN
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
12
- Classifier: Operating System :: OS Independent
13
- Requires-Python: >=3.9
14
- Description-Content-Type: text/markdown
15
- Provides-Extra: dev
16
-
17
- # fm_metamodel
18
-
19
- This repo host the feature model concrete classes
20
-
21
-
22
- ## Install for development
23
-
24
- ```
25
- pip install -e .
26
- ```
27
-
28
-