mal-toolbox 0.2.0__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=4y4QJcwl6OKw8pCiPaUrOWtXRpEgFrKeoNLajc9p2Iw,2776
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=TFLm32_RLfB4uEsToZ8ypDcRbbdFZMso34mfvqAb1bY,2139
6
- maltoolbox/model.py,sha256=mQqwx0cSLV6E8wGBSDDKkKnFdbcPeBcjqIpFdBjtkYE,31042
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=HLAjF8Qx9W2iv6uSWRULL16ieuy7UM5fLZNwxIr544s,31348
11
- maltoolbox/attackgraph/node.py,sha256=xrnY_YlX9ZFXDRsj92I9PZMGJR6s1LpTnOIppR6TGXo,6074
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=8dRZL22oym5goXBYtYA1ZYXxk8VwQS0RlNCvNCUhCAY,8743
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=WNoPtcYHJE799vR6x269Gx5KPWxBgLOHQoycE1vhOF4,257
18
- maltoolbox/language/classes_factory.py,sha256=-s4xxwcCGgKj1wzrgsrn-ndLhgU4VoEjrrSuGx8qvYE,10217
19
- maltoolbox/language/languagegraph.py,sha256=ctoxS33DM7F4DF50SNDB2lTtokUJKyWMhMhss46yv_g,68123
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.2.0.dist-info/AUTHORS,sha256=zxLrLe8EY39WtRKlAY4Oorx4Z2_LHV2ApRvDGZgY7xY,127
28
- mal_toolbox-0.2.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
29
- mal_toolbox-0.2.0.dist-info/METADATA,sha256=AWtN4435t5ycp5k-dPfRSVRU4rhpyb6e_z1FBE2PYpY,6001
30
- mal_toolbox-0.2.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
31
- mal_toolbox-0.2.0.dist-info/top_level.txt,sha256=phqRVLRKGdSUgRY03mcpi2cmbbDo5YGjkV4gkqHFFcM,11
32
- mal_toolbox-0.2.0.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,259 +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_name, asset in self.lang_graph.assets.items():
32
- logger.debug('Creating %s asset JSON schema entry.', asset.name)
33
- asset_json_entry = {
34
- 'title': 'Asset_' + 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.own_super_asset:
47
- asset_json_entry['allOf'] = [
48
- {'$ref': '#/definitions/LanguageAsset/definitions/Asset_'\
49
- + asset.own_super_asset.name}
50
- ]
51
- for step_name, step in asset.attack_steps.items():
52
- if step.type == 'defense':
53
- if step.ttc and step.ttc['name'] == 'Enabled':
54
- default_defense_value = 1.0
55
- else:
56
- default_defense_value = 0.0
57
- asset_json_entry['properties'][step_name] = \
58
- {
59
- 'type' : 'number',
60
- 'minimum' : 0,
61
- 'maximum' : 1,
62
- 'default': default_defense_value
63
- }
64
- self.json_schema['definitions']['LanguageAsset']['definitions']\
65
- ['Asset_' + asset_name] = asset_json_entry
66
- self.json_schema['definitions']['LanguageAsset']['oneOf'].append(
67
- {'$ref': '#/definitions/LanguageAsset/definitions/Asset_' + asset_name}
68
- )
69
-
70
- def _generate_associations(self) -> None:
71
- """
72
- Generate JSON Schema for association types in the language specification.
73
- """
74
- def create_association_entry(assoc: SchemaGeneratedClass):
75
- logger.debug('Creating %s association JSON schema entry.', assoc.name)
76
- assoc_json_entry = {
77
- 'title': 'Association_' + assoc.full_name,
78
- 'type': 'object',
79
- 'properties': {}
80
- }
81
- assoc_json_entry['properties']['type'] = \
82
- {
83
- 'type' : 'string',
84
- 'default': assoc.name
85
- }
86
-
87
- create_association_field(assoc, assoc_json_entry, 'left')
88
- create_association_field(assoc, assoc_json_entry, 'right')
89
- return assoc_json_entry
90
-
91
- def create_association_field(
92
- assoc: SchemaGeneratedClass,
93
- assoc_json_entry: dict,
94
- position: Literal['left', 'right']
95
- ) -> None:
96
- field = getattr(assoc, position + "_field")
97
- assoc_json_entry['properties'][field.fieldname] = \
98
- {
99
- 'type' : 'array',
100
- 'items' :
101
- {
102
- '$ref':
103
- '#/definitions/LanguageAsset/definitions/Asset_' +
104
- field.asset.name
105
- }
106
- }
107
- if field.maximum:
108
- assoc_json_entry['properties'][field.fieldname]\
109
- ['maxItems'] = field.maximum
110
-
111
- for asset_name, asset in self.lang_graph.assets.items():
112
- for assoc_name, assoc in asset.associations.items():
113
- if assoc_name not in self.json_schema['definitions']\
114
- ['LanguageAssociation']['definitions']:
115
- assoc_json_entry = create_association_entry(assoc)
116
- self.json_schema['definitions']['LanguageAssociation']\
117
- ['definitions']['Association_' + assoc_name] = \
118
- assoc_json_entry
119
- self.json_schema['definitions']['LanguageAssociation']['oneOf'].\
120
- append({'$ref': '#/definitions/LanguageAssociation/' +
121
- 'definitions/Association_' + assoc_name})
122
-
123
- def _create_classes(self) -> None:
124
- """
125
- Create classes based on the language specification.
126
- """
127
-
128
- # First, we have to translate the language specification into a JSON
129
- # schema. Initialize the overall JSON schema structure.
130
- self.json_schema = {
131
- '$schema': 'http://json-schema.org/draft-04/schema#',
132
- 'id': f"urn:mal:{__name__.replace('.', ':')}",
133
- 'title': 'LanguageObject',
134
- 'type': 'object',
135
- 'oneOf':[
136
- {'$ref': '#/definitions/LanguageAsset'},
137
- {'$ref': '#/definitions/LanguageAssociation'}
138
- ],
139
- 'definitions': {}}
140
- self.json_schema['definitions']['LanguageAsset'] = {
141
- 'title': 'LanguageAsset',
142
- 'type': 'object',
143
- 'oneOf': [],
144
- 'definitions': {}}
145
- self.json_schema['definitions']['LanguageAssociation'] = {
146
- 'title': 'LanguageAssociation',
147
- 'type': 'object',
148
- 'oneOf': [],
149
- 'definitions': {}}
150
-
151
- self._generate_assets()
152
- self._generate_associations()
153
-
154
- if logger.isEnabledFor(logging.DEBUG):
155
- # Avoid running json.dumps when not in debug
156
- logger.debug(json.dumps(self.json_schema, indent = 2))
157
-
158
- # Once we have the JSON schema we create the actual classes.
159
- builder = pjs.ObjectBuilder(self.json_schema)
160
- self.ns = builder.build_classes(standardize_names=False)
161
-
162
- def get_association_by_signature(
163
- self,
164
- assoc_name: str,
165
- left_asset: str,
166
- right_asset: str
167
- ) -> Optional[str]:
168
- """
169
- Get association name based on its signature. This is primarily
170
- relevant for getting the exact association full name when multiple
171
- associations with the same name exist.
172
-
173
- Arguments:
174
- assoc_name - the association name
175
- left_asset - the name of the left asset type
176
- right_asset - the name of the right asset type
177
-
178
- Return: The matching association name if a match is found.
179
- None if there is no match.
180
- """
181
- lang_assocs_entries = self.json_schema['definitions']\
182
- ['LanguageAssociation']['definitions']
183
- if not assoc_name in lang_assocs_entries:
184
- raise LookupError(
185
- 'Failed to find "%s" association in the language json '
186
- 'schema.' % assoc_name
187
- )
188
- assoc_entry = lang_assocs_entries[assoc_name]
189
- # If the association has a oneOf property it should always have more
190
- # than just one alternative, but check just in case
191
- if 'definitions' in assoc_entry and \
192
- len(assoc_entry['definitions']) > 1:
193
- full_name = '%s_%s_%s' % (
194
- assoc_name,
195
- left_asset,
196
- right_asset
197
- )
198
- full_name_flipped = '%s_%s_%s' % (
199
- assoc_name,
200
- right_asset,
201
- left_asset
202
- )
203
- if not full_name in assoc_entry['definitions']:
204
- if not full_name_flipped in assoc_entry['definitions']:
205
- raise LookupError(
206
- 'Failed to find "%s" or "%s" association in the '
207
- 'language json schema.'
208
- % (full_name,
209
- full_name_flipped)
210
- )
211
- else:
212
- return full_name_flipped
213
- else:
214
- return full_name
215
- else:
216
- return assoc_name
217
-
218
- def get_asset_class(self,
219
- asset_name: str
220
- ) -> Optional[SchemaGeneratedClass]:
221
- class_name = 'Asset_' + asset_name
222
- if hasattr(self.ns, class_name):
223
- class_obj = getattr(self.ns, class_name)
224
- class_obj.__hash__ = lambda self: hash(self.name)
225
- return class_obj
226
- else:
227
- logger.warning('Could not find Asset "%s" in classes factory.' %
228
- asset_name)
229
- return None
230
-
231
- def get_association_class(self,
232
- assoc_name: str
233
- ) -> Optional[SchemaGeneratedClass]:
234
- class_name = 'Association_' + assoc_name
235
- if hasattr(self.ns, class_name):
236
- return getattr(self.ns, class_name)
237
- else:
238
- logger.warning('Could not find Association "%s" in classes factory.' %
239
- assoc_name)
240
- return None
241
-
242
- def get_association_class_by_fieldnames(self,
243
- assoc_name: str,
244
- fieldname1: str,
245
- fieldname2: str
246
- ) -> Optional[SchemaGeneratedClass]:
247
- class_name = 'Association_%s_%s_%s' % (assoc_name,
248
- fieldname1, fieldname2)
249
- class_name_alt = 'Association_%s_%s_%s' % (assoc_name,
250
- fieldname2, fieldname1)
251
-
252
- if hasattr(self.ns, class_name):
253
- return getattr(self.ns, class_name)
254
- elif hasattr(self.ns, class_name_alt):
255
- return getattr(self.ns, class_name_alt)
256
- else:
257
- logger.warning('Could not find Association "%s" or "%s" in '
258
- 'classes factory.' % (class_name, class_name_alt))
259
- return None