mal-toolbox 0.1.12__py3-none-any.whl → 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.
@@ -3,130 +3,253 @@ import logging
3
3
 
4
4
  import yaml
5
5
 
6
- from ..model import Model, AttackerAttachment
7
- from ..language import LanguageClassesFactory
6
+ import logging
7
+ from ..model import Model
8
+ from ..language import LanguageGraph
9
+ from ..file_utils import load_dict_from_json_file, load_dict_from_yaml_file
8
10
 
9
11
  logger = logging.getLogger(__name__)
10
12
 
11
13
  def load_model_from_older_version(
12
- filename: str,
13
- lang_classes_factory: LanguageClassesFactory,
14
- version: str
14
+ filename: str, lang_graph: LanguageGraph,
15
15
  ) -> Model:
16
- match (version):
17
- case '0.0.39':
18
- return load_model_from_version_0_0_39(filename,
19
- lang_classes_factory)
20
- case _:
21
- msg = ('Unknown version "%s" format. Could not '
22
- 'load model from file "%s"')
23
- logger.error(msg % (version, filename))
24
- raise ValueError(msg % (version, filename))
25
16
 
26
- def load_model_from_version_0_0_39(
27
- filename: str,
28
- lang_classes_factory: LanguageClassesFactory
29
- ) -> Model:
30
- """
31
- Load model from file.
17
+ """ Load an older Model file
32
18
 
33
- Arguments:
34
- filename - the name of the input file
35
- lang_classes_factory - the language classes factory that defines the
36
- classes needed to build the model
19
+ Load an older model from given `filename` (yml/json)
20
+ convert the model to the new format and return a Model object.
37
21
  """
38
22
 
39
- def _process_model(model_dict, lang_classes_factory) -> Model:
40
- model = Model(model_dict['metadata']['name'], lang_classes_factory)
23
+ model_dict = load_model_dict_from_file(filename)
41
24
 
42
- # Reconstruct the assets
43
- for asset_id, asset_object in model_dict['assets'].items():
44
- logger.debug(f"Loading asset:\n{json.dumps(asset_object, indent=2)}")
25
+ # Get the version of the model, default to 0.0
26
+ # since metadata was not given back then
27
+ version = model_dict.get(
28
+ 'metadata', {}
29
+ ).get('MAL-Toolbox Version', '0.0.')
45
30
 
46
- # Allow defining an asset via the metaconcept only.
47
- asset_object = (
48
- asset_object
49
- if isinstance(asset_object, dict)
50
- else {'metaconcept': asset_object, 'name': f"{asset_object}:{asset_id}"}
31
+ match (version):
32
+ case x if '0.0.' in x:
33
+ model_dict = convert_model_dict_from_version_0_0(model_dict)
34
+ model_dict = convert_model_dict_from_version_0_1(model_dict)
35
+ model_dict = convert_model_dict_from_version_0_2(model_dict)
36
+ case x if '0.1.' in x:
37
+ model_dict = convert_model_dict_from_version_0_1(model_dict)
38
+ model_dict = convert_model_dict_from_version_0_2(model_dict)
39
+ case x if '0.2.' in x:
40
+ model_dict = convert_model_dict_from_version_0_2(model_dict)
41
+ case _:
42
+ msg = (
43
+ 'Unknown version "%s" format.'
44
+ 'Could not load model from file "%s"'
51
45
  )
46
+ logger.error(msg, version, filename)
47
+ raise ValueError(msg % (version, filename))
48
+
49
+ # TODO: _from_dict should be public
50
+ return Model._from_dict(model_dict, lang_graph)
51
+
52
52
 
53
- asset = getattr(model.lang_classes_factory.ns,
54
- asset_object['metaconcept'])(name = asset_object['name'])
55
-
56
- for defense in (defenses:=asset_object.get('defenses', [])):
57
- setattr(asset, defense, float(defenses[defense]))
58
-
59
- model.add_asset(asset, asset_id = int(asset_id))
60
-
61
- # Reconstruct the associations
62
- for assoc_dict in model_dict.get('associations', []):
63
- association = getattr(model.lang_classes_factory.ns, assoc_dict.pop('metaconcept'))()
64
-
65
- # compatibility with old format
66
- assoc_dict = assoc_dict.get('association', assoc_dict)
67
-
68
- for field, targets in assoc_dict.items():
69
- targets = targets if isinstance(targets, list) else [targets]
70
- setattr(
71
- association,
72
- field,
73
- [model.get_asset_by_id(int(id)) for id in targets]
74
- )
75
- model.add_association(association)
76
-
77
- # Reconstruct the attackers
78
- if 'attackers' in model_dict:
79
- attackers_info = model_dict['attackers']
80
- for attacker_id in attackers_info:
81
- attacker = AttackerAttachment(
82
- name = attackers_info[attacker_id]['name']
83
- )
84
- attacker.entry_points = []
85
- for asset_id in attackers_info[attacker_id]['entry_points']:
86
- attacker.entry_points.append(
87
- (model.get_asset_by_id(int(asset_id)),
88
- attackers_info[attacker_id]['entry_points']\
89
- [asset_id]['attack_steps']))
90
- model.add_attacker(attacker, attacker_id = int(attacker_id))
91
- return model
92
-
93
- def load_from_json(
94
- filename: str,
95
- lang_classes_factory: LanguageClassesFactory
96
- ) -> Model:
97
- """
98
- Load model from a json file.
99
-
100
- Arguments:
101
- filename - the name of the input file
102
- """
103
- with open(filename, 'r', encoding='utf-8') as model_file:
104
- model_dict = json.loads(model_file.read())
105
-
106
- return _process_model(model_dict, lang_classes_factory)
107
-
108
- def load_from_yaml(
109
- filename: str,
110
- lang_classes_factory: LanguageClassesFactory
111
- ) -> Model:
112
- """
113
- Load model from a yaml file.
114
-
115
- Arguments:
116
- filename - the name of the input file
117
- """
118
- with open(filename, 'r', encoding='utf-8') as model_file:
119
- model_dict = yaml.safe_load(model_file)
120
-
121
- return _process_model(model_dict, lang_classes_factory)
122
-
123
- logger.info(f'Loading model from {filename} file.')
53
+ def load_model_dict_from_file(
54
+ filename: str,
55
+ ) -> dict:
56
+ """Load a json or yaml file to dict"""
57
+
58
+ model_dict = {}
124
59
  if filename.endswith('.yml') or filename.endswith('.yaml'):
125
- return load_from_yaml(filename, lang_classes_factory)
60
+ model_dict = load_dict_from_yaml_file(filename)
126
61
  elif filename.endswith('.json'):
127
- return load_from_json(filename, lang_classes_factory)
62
+ model_dict = load_dict_from_json_file(filename)
128
63
  else:
129
64
  msg = 'Unknown file extension for model file to load from.'
130
65
  logger.error(msg)
131
66
  raise ValueError(msg)
132
- return None
67
+ return model_dict
68
+
69
+
70
+ def convert_model_dict_from_version_0_0(model_dict: dict) -> dict:
71
+ """
72
+ Convert model dict version 0.0 to 0.1
73
+
74
+ Arguments:
75
+ model_dict - the dictionary containing the serialized model
76
+
77
+ Returns:
78
+ A dictionary containing the version 0.1 equivalent serialized model
79
+ """
80
+
81
+ new_model_dict = {}
82
+
83
+ # Meta data and attackers did not change
84
+ new_model_dict['metadata'] = model_dict['metadata']
85
+ new_model_dict['attackers'] = model_dict['attackers']
86
+
87
+ new_model_dict['assets'] = {}
88
+
89
+ # Reconstruct the assets
90
+ new_assets_dict = {}
91
+ for asset_id, asset_info in model_dict['assets'].items():
92
+ # Make sure asset ids are ints for json compatibility
93
+ asset_id = int(asset_id)
94
+ new_assets_dict[asset_id] = asset_info
95
+
96
+ # Metaconcept renamed to type
97
+ new_assets_dict[asset_id]["type"] = (
98
+ new_assets_dict[asset_id]["metaconcept"]
99
+ )
100
+ del new_assets_dict[asset_id]["metaconcept"]
101
+
102
+ new_model_dict['assets'] = new_assets_dict
103
+
104
+ # Reconstruct the associations dict in new format
105
+ new_assoc_list = []
106
+ for assoc_dict in model_dict.get('associations', []):
107
+ new_assoc_dict: dict[str, dict] = {}
108
+
109
+ # Assocs are not mapped from association names
110
+ # metaconcept field removed
111
+ assoc_name = assoc_dict['metaconcept']
112
+ new_assoc_dict[assoc_name] = {}
113
+
114
+ for field, targets in assoc_dict['association'].items():
115
+ # Targets are now intes
116
+ new_targets = [int(asset_id) for asset_id in targets]
117
+ new_assoc_dict[assoc_name][field] = new_targets
118
+
119
+ new_assoc_list.append(new_assoc_dict)
120
+
121
+ # Add new assoc dict to new model dict
122
+ new_model_dict['associations'] = new_assoc_list
123
+
124
+ # Reconstruct the attackers
125
+ if 'attackers' in model_dict:
126
+ attackers_info = model_dict['attackers']
127
+
128
+ return new_model_dict
129
+
130
+
131
+ def convert_model_dict_from_version_0_1(model_dict: dict) -> dict:
132
+ """
133
+ Convert model dict version 0.1 to 0.2
134
+
135
+ Arguments:
136
+ model_dict - the dictionary containing the serialized model
137
+
138
+ Returns:
139
+ A dictionary containing the version 0.2 equivalent serialized model
140
+ """
141
+
142
+ new_model_dict = {}
143
+
144
+ # Meta data and assets format did not change from version 0.1
145
+ new_model_dict['metadata'] = model_dict['metadata']
146
+
147
+ new_assets_dict = {}
148
+ for asset_id, asset_info in model_dict['assets'].items():
149
+ # Make sure asset ids are ints for json compatibility
150
+ asset_id = int(asset_id)
151
+ new_assets_dict[asset_id] = asset_info
152
+
153
+ new_model_dict['assets'] = new_assets_dict
154
+
155
+ # Reconstruct the associations dict in new format
156
+ new_assoc_list = []
157
+ for assoc_dict in model_dict.get('associations', []):
158
+ new_assoc_dict: dict[str, dict] = {}
159
+
160
+ assert len(assoc_dict.keys()) == 1, (
161
+ "Only one key per association in model file allowed"
162
+ )
163
+
164
+ assoc_name = list(assoc_dict.keys())[0]
165
+ new_assoc_name = assoc_name.split("_")[0]
166
+ new_assoc_dict[new_assoc_name] = {}
167
+ for field, targets in assoc_dict[assoc_name].items():
168
+ new_assoc_dict[new_assoc_name][field] = targets
169
+
170
+ new_assoc_list.append(new_assoc_dict)
171
+
172
+ # Add new assoc dict to new model dict
173
+ new_model_dict['associations'] = new_assoc_list
174
+
175
+ # Reconstruct attackers dict for new format
176
+ new_attackers_dict: dict[int, dict] = {}
177
+ attackers_dict: dict = model_dict.get('attackers', {})
178
+ for attacker_id, attacker_dict in attackers_dict.items():
179
+ attacker_id = int(attacker_id) # JSON compatibility
180
+ new_attackers_dict[attacker_id] = {}
181
+ new_attackers_dict[attacker_id]['name'] = attacker_dict['name']
182
+ new_entry_points_dict = {}
183
+
184
+ entry_points_dict = attacker_dict['entry_points']
185
+ for asset_id, attack_steps in entry_points_dict.items():
186
+ asset_id = int(asset_id) # JSON compatibility
187
+ asset_name = new_assets_dict[asset_id]['name']
188
+ new_entry_points_dict[asset_name] = {
189
+ 'asset_id': asset_id,
190
+ 'attack_steps': attack_steps['attack_steps']
191
+ }
192
+
193
+ new_attackers_dict[attacker_id]['entry_points'] = new_entry_points_dict
194
+
195
+ # Add new attackers dict to new model dict
196
+ new_model_dict['attackers'] = new_attackers_dict
197
+ return new_model_dict
198
+
199
+
200
+ def convert_model_dict_from_version_0_2(model_dict: dict) -> dict:
201
+ """
202
+ Convert model dict version 0.2 to 0.3
203
+
204
+ Arguments:
205
+ model_dict - the dictionary containing the serialized model
206
+
207
+ Returns:
208
+ A dictionary containing the version 0.3 equivalent serialized model
209
+ """
210
+
211
+ new_model_dict = {}
212
+
213
+ # Meta data and assets format did not change from version 0.1
214
+ new_model_dict['metadata'] = model_dict['metadata']
215
+ new_model_dict['attackers'] = model_dict['attackers']
216
+
217
+ new_assets_dict: dict[int, dict] = {}
218
+ for asset_id, asset_info in model_dict['assets'].items():
219
+ # Make sure asset ids are ints for json compatibility
220
+ asset_id = int(asset_id)
221
+ new_assets_dict[asset_id] = asset_info
222
+ new_assets_dict[asset_id]['associated_assets'] = {}
223
+
224
+ new_model_dict['assets'] = new_assets_dict
225
+
226
+ # Reconstruct the associations dict in new format
227
+ for assocs_dict in model_dict.get('associations', []):
228
+
229
+ assert len(assocs_dict.keys()) == 1, (
230
+ "Only one key per association in model file allowed"
231
+ )
232
+
233
+ assoc_name = list(assocs_dict.keys())[0]
234
+ assoc_dict = assocs_dict[assoc_name]
235
+ left_field_name = list(assoc_dict.keys())[0]
236
+ right_field_name = list(assoc_dict.keys())[1]
237
+
238
+ for l_asset_id in assoc_dict[left_field_name]:
239
+ l_asset_id = int(l_asset_id) # json compatibility
240
+ for r_asset_id in assoc_dict[right_field_name]:
241
+ r_asset_id = int(r_asset_id) # json compatibility
242
+ l_asset_dict = new_model_dict['assets'][l_asset_id]
243
+ r_asset_dict = new_model_dict['assets'][r_asset_id]
244
+
245
+ # Add connections from left to right
246
+ l_asset_dict['associated_assets'].setdefault(
247
+ right_field_name, {}
248
+ )[r_asset_id] = r_asset_dict['name']
249
+
250
+ # And from right to left
251
+ r_asset_dict['associated_assets'].setdefault(
252
+ left_field_name, {}
253
+ )[l_asset_id] = l_asset_dict['name']
254
+
255
+ return new_model_dict
@@ -1,32 +0,0 @@
1
- maltoolbox/__init__.py,sha256=Gkuzd3_D5OscXXfTzYQf5Vdcmooe67qcYyScGAeFpcQ,2778
2
- maltoolbox/__main__.py,sha256=1lOOOme_y56VWrEE1jkarTt-WoUo9yilCo8sUrivyns,2680
3
- maltoolbox/default.conf,sha256=YLGBSJh2q8hn3RzRRBbib9F6E6pcvquoHeALMRtA0wU,295
4
- maltoolbox/exceptions.py,sha256=0YjPx2v1yYumZ2o7pVZ1s_jS-GAb3Ng979KEFhROSNY,1399
5
- maltoolbox/file_utils.py,sha256=6KFEEZvf9x8yfNAq7hadF7lUGlLimNFMJ0W_DK2rh6Q,2024
6
- maltoolbox/model.py,sha256=gYGFdjkHPYpBuEngBRsj_Ki6fFSJtfBG9p8oCARz7gk,30556
7
- maltoolbox/wrappers.py,sha256=BYYNcIdTlyumADQCPcy1xmPEabfmi0P1l9RcbdVWm9w,2002
8
- maltoolbox/attackgraph/__init__.py,sha256=Oqqj5iCwnrzjDoJEFZnVI_kebjJPVbPXK-mWHy0lf-8,209
9
- maltoolbox/attackgraph/attacker.py,sha256=OaBNDYZF8shbFuQctzuNYVkOrpNb_KhxxV19k0SRa50,3541
10
- maltoolbox/attackgraph/attackgraph.py,sha256=T9snTC8kzgN017leI29CYj2YdlrU8IDxYiV69yDgz7o,30060
11
- maltoolbox/attackgraph/node.py,sha256=oFaGCz4QPvDcS7xM5lxaG_-GUR-PKT2xtQ1ryzYRWaU,5869
12
- maltoolbox/attackgraph/query.py,sha256=JnoNTUEIlLv2VIk3u5Rq3GpleOn9TZVGBVijniRY_44,6802
13
- maltoolbox/attackgraph/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- maltoolbox/attackgraph/analyzers/apriori.py,sha256=Af4NOSiE6Z0UnI_fuhxBA6YtSkDUj1kMie1rj09I0qM,8548
15
- maltoolbox/ingestors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- maltoolbox/ingestors/neo4j.py,sha256=jdulYsQ2eZT2r0Af_yYjyGkmVx4l5h8viu1Z70NjVAM,8811
17
- maltoolbox/language/__init__.py,sha256=0tvCJDayrLwpqKRny7LBkMOrvcDE6JfJj7U7Jd64Okg,140
18
- maltoolbox/language/classes_factory.py,sha256=0QFF5Z9e4UeWwavvH1jM4BkeDZqKKZ4Ij7leGmSfXn4,10063
19
- maltoolbox/language/languagegraph.py,sha256=f79ovmrGQb6tvY9ze-zqP031N6rApB9WsbZd5LRyhk8,47031
20
- maltoolbox/language/compiler/__init__.py,sha256=fJ22-FlXfr907WCPkqlr_eBTzPqsrg6m3i7J_ZWpuAo,840
21
- maltoolbox/language/compiler/mal_lexer.py,sha256=wocRzBkLbqYefpGvq2W77x7439-AdZKVgPWhRiRubXg,10776
22
- maltoolbox/language/compiler/mal_parser.py,sha256=M1EVZFV73TNfQHz2KJ8-iloqOD4KUhHyajszD8UrNow,91349
23
- maltoolbox/language/compiler/mal_visitor.py,sha256=9gG06D7GZKlBY-62SmbIkRYkGBUBIC6fl1GOg7v2IuM,13223
24
- maltoolbox/translators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- maltoolbox/translators/securicad.py,sha256=FAIHnoqFTmNYbCGxLsK6pX5g1oiNFfPTqkT_3qq3GG8,6692
26
- maltoolbox/translators/updater.py,sha256=Ap08-AsU_7or5ESQvZL2i4nWz3B5pvgfftZyc_-Gd8M,4766
27
- mal_toolbox-0.1.12.dist-info/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
28
- mal_toolbox-0.1.12.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
29
- mal_toolbox-0.1.12.dist-info/METADATA,sha256=XNkHzeC0F3E4Mtp4GhxAm2d2SWqDteCPNVZ1Px0q2rw,6002
30
- mal_toolbox-0.1.12.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
31
- mal_toolbox-0.1.12.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
32
- mal_toolbox-0.1.12.dist-info/RECORD,,
maltoolbox/default.conf DELETED
@@ -1,17 +0,0 @@
1
- [logging]
2
- output_dir = tmp
3
- log_level =
4
- log_file = %(output_dir)s/log.txt
5
- attackgraph_file = %(output_dir)s/attackgraph.yml
6
- model_file = %(output_dir)s/model.yml
7
- langspec_file = %(output_dir)s/langspec_file.yml
8
-
9
- [input]
10
- model_file =
11
- lang_spec_file =
12
-
13
- [neo4j]
14
- uri =
15
- username =
16
- password =
17
- dbname =
@@ -1,243 +0,0 @@
1
- """
2
- MAL-Toolbox Language Classes Factory Module
3
- Uses python_jsonschema_objects to generate python classes from a MAL language
4
- """
5
- from __future__ import annotations
6
- import json
7
- import logging
8
- from typing import TYPE_CHECKING
9
-
10
- import python_jsonschema_objects as pjs
11
-
12
- if TYPE_CHECKING:
13
- from typing import Literal, Optional, TypeAlias
14
- from maltoolbox.language import LanguageGraph
15
- from python_jsonschema_objects.classbuilder import ProtocolBase
16
-
17
- SchemaGeneratedClass: TypeAlias = ProtocolBase
18
-
19
- logger = logging.getLogger(__name__)
20
-
21
- class LanguageClassesFactory:
22
- def __init__(self, lang_graph: LanguageGraph):
23
- self.lang_graph: LanguageGraph = lang_graph
24
- self.json_schema: dict = {}
25
- self._create_classes()
26
-
27
- def _generate_assets(self) -> None:
28
- """
29
- Generate JSON Schema for asset types in the language specification.
30
- """
31
- for asset in self.lang_graph.assets:
32
- logger.debug('Creating %s asset JSON schema entry.', asset.name)
33
- asset_json_entry = {
34
- 'title': asset.name,
35
- 'type': 'object',
36
- 'properties': {},
37
- }
38
- asset_json_entry['properties']['id'] = {
39
- 'type' : 'integer',
40
- }
41
- asset_json_entry['properties']['type'] = \
42
- {
43
- 'type' : 'string',
44
- 'default': asset.name
45
- }
46
- if asset.super_assets:
47
- asset_json_entry['allOf'] = [
48
- {'$ref': '#/definitions/LanguageAsset/definitions/' + superasset.name}
49
- for superasset in asset.super_assets
50
- ]
51
- for defense in filter(lambda step: step.type == 'defense', asset.attack_steps):
52
- if defense.ttc and defense.ttc['name'] == 'Enabled':
53
- default_defense_value = 1.0
54
- else:
55
- default_defense_value = 0.0
56
- asset_json_entry['properties'][defense.name] = \
57
- {
58
- 'type' : 'number',
59
- 'minimum' : 0,
60
- 'maximum' : 1,
61
- 'default': default_defense_value
62
- }
63
- self.json_schema['definitions']['LanguageAsset']['definitions']\
64
- [asset.name] = asset_json_entry
65
- self.json_schema['definitions']['LanguageAsset']['oneOf'].append(
66
- {'$ref': '#/definitions/LanguageAsset/definitions/' + asset.name}
67
- )
68
-
69
- def _generate_associations(self) -> None:
70
- """
71
- Generate JSON Schema for association types in the language specification.
72
- """
73
- def create_association_entry(assoc: SchemaGeneratedClass):
74
- logger.debug('Creating %s association JSON schema entry.', assoc.name)
75
- assoc_json_entry = {'title': assoc.name, 'type': 'object', 'properties': {}}
76
-
77
- create_association_field(assoc, assoc_json_entry, 'left')
78
- create_association_field(assoc, assoc_json_entry, 'right')
79
- return assoc_json_entry
80
-
81
- def create_association_with_subentries(assoc: SchemaGeneratedClass):
82
- if (assoc.name not in self.json_schema['definitions']\
83
- ['LanguageAssociation']['definitions']):
84
- logger.info(
85
- 'Multiple associations with name %s exist. '
86
- 'Creating subentries for each one.', assoc.name
87
- )
88
- self.json_schema['definitions']['LanguageAssociation']\
89
- ['definitions'][assoc.name] =\
90
- {
91
- 'title': assoc.name,
92
- 'type': 'object',
93
- 'oneOf': [],
94
- 'definitions': {}
95
- }
96
- self.json_schema['definitions']['LanguageAssociation']['oneOf'].\
97
- append({'$ref': '#/definitions/LanguageAssociation/definitions/'
98
- + assoc.name})
99
-
100
- assoc_json_subentry = create_association_entry(assoc)
101
- subentry_name = assoc.name + '_' + assoc.left_field.asset.name + '_' \
102
- + assoc.right_field.asset.name
103
-
104
- logger.info('Creating %s subentry association.', subentry_name)
105
- assoc_json_subentry['title'] = subentry_name
106
- self.json_schema['definitions']['LanguageAssociation']\
107
- ['definitions'][assoc.name]['definitions'][subentry_name] = assoc_json_subentry
108
- self.json_schema['definitions']['LanguageAssociation']\
109
- ['definitions'][assoc.name]['oneOf'].append(
110
- {'$ref': '#/definitions/LanguageAssociation/definitions/' \
111
- + assoc.name + '/definitions/' + subentry_name})
112
-
113
- def create_association_field(
114
- assoc: SchemaGeneratedClass,
115
- assoc_json_entry: dict,
116
- position: Literal['left', 'right']
117
- ) -> None:
118
- field = getattr(assoc, position + "_field")
119
- assoc_json_entry['properties'][field.fieldname] = \
120
- {
121
- 'type' : 'array',
122
- 'items' :
123
- {
124
- '$ref':
125
- '#/definitions/LanguageAsset/definitions/' +
126
- field.asset.name
127
- }
128
- }
129
- if field.maximum:
130
- assoc_json_entry['properties'][field.fieldname]\
131
- ['maxItems'] = field.maximum
132
-
133
-
134
- for assoc in self.lang_graph.associations:
135
- count = len(list(filter(lambda temp_assoc: temp_assoc.name ==
136
- assoc.name, self.lang_graph.associations)))
137
- if count > 1:
138
- # If there are multiple associations with the same name we
139
- # will need to create separate entries for each using their
140
- # fieldnames.
141
- create_association_with_subentries(assoc)
142
- else:
143
- assoc_json_entry = create_association_entry(assoc)
144
- self.json_schema['definitions']['LanguageAssociation']\
145
- ['definitions'][assoc.name] = assoc_json_entry
146
- self.json_schema['definitions']['LanguageAssociation']['oneOf'].\
147
- append({'$ref': '#/definitions/LanguageAssociation/' +
148
- 'definitions/' + assoc.name})
149
-
150
- def _create_classes(self) -> None:
151
- """
152
- Create classes based on the language specification.
153
- """
154
-
155
- # First, we have to translate the language specification into a JSON
156
- # schema. Initialize the overall JSON schema structure.
157
- self.json_schema = {
158
- '$schema': 'http://json-schema.org/draft-04/schema#',
159
- 'id': f"urn:mal:{__name__.replace('.', ':')}",
160
- 'title': 'LanguageObject',
161
- 'type': 'object',
162
- 'oneOf':[
163
- {'$ref': '#/definitions/LanguageAsset'},
164
- {'$ref': '#/definitions/LanguageAssociation'}
165
- ],
166
- 'definitions': {}}
167
- self.json_schema['definitions']['LanguageAsset'] = {
168
- 'title': 'LanguageAsset',
169
- 'type': 'object',
170
- 'oneOf': [],
171
- 'definitions': {}}
172
- self.json_schema['definitions']['LanguageAssociation'] = {
173
- 'title': 'LanguageAssociation',
174
- 'type': 'object',
175
- 'oneOf': [],
176
- 'definitions': {}}
177
-
178
- self._generate_assets()
179
- self._generate_associations()
180
-
181
- if logger.isEnabledFor(logging.DEBUG):
182
- # Avoid running json.dumps when not in debug
183
- logger.debug(json.dumps(self.json_schema, indent = 2))
184
-
185
- # Once we have the JSON schema we create the actual classes.
186
- builder = pjs.ObjectBuilder(self.json_schema)
187
- self.ns = builder.build_classes(standardize_names=False)
188
-
189
- def get_association_by_signature(
190
- self,
191
- assoc_name: str,
192
- left_asset: str,
193
- right_asset: str
194
- ) -> Optional[str]:
195
- """
196
- Get association name based on its signature. This is primarily
197
- relevant for getting the exact association full name when multiple
198
- associations with the same name exist.
199
-
200
- Arguments:
201
- assoc_name - the association name
202
- left_asset - the name of the left asset type
203
- right_asset - the name of the right asset type
204
-
205
- Return: The matching association name if a match is found.
206
- None if there is no match.
207
- """
208
- lang_assocs_entries = self.json_schema['definitions']\
209
- ['LanguageAssociation']['definitions']
210
- if not assoc_name in lang_assocs_entries:
211
- raise LookupError(
212
- 'Failed to find "%s" association in the language json '
213
- 'schema.' % assoc_name
214
- )
215
- assoc_entry = lang_assocs_entries[assoc_name]
216
- # If the association has a oneOf property it should always have more
217
- # than just one alternative, but check just in case
218
- if 'definitions' in assoc_entry and \
219
- len(assoc_entry['definitions']) > 1:
220
- full_name = '%s_%s_%s' % (
221
- assoc_name,
222
- left_asset,
223
- right_asset
224
- )
225
- full_name_flipped = '%s_%s_%s' % (
226
- assoc_name,
227
- right_asset,
228
- left_asset
229
- )
230
- if not full_name in assoc_entry['definitions']:
231
- if not full_name_flipped in assoc_entry['definitions']:
232
- raise LookupError(
233
- 'Failed to find "%s" or "%s" association in the '
234
- 'language json schema.'
235
- % (full_name,
236
- full_name_flipped)
237
- )
238
- else:
239
- return full_name_flipped
240
- else:
241
- return full_name
242
- else:
243
- return assoc_name