dsw-tdk 3.13.0__py3-none-any.whl → 4.27.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.
dsw_tdk/consts.py DELETED
@@ -1,19 +0,0 @@
1
- import pathspec # type: ignore
2
- import re
3
-
4
- APP = 'dsw-tdk'
5
- VERSION = '3.13.0'
6
- METAMODEL_VERSION = 10
7
-
8
- REGEX_SEMVER = re.compile(r'^[0-9]+\.[0-9]+\.[0-9]+$')
9
- REGEX_ORGANIZATION_ID = re.compile(r'^(?![.])(?!.*[.]$)[a-zA-Z0-9.]+$')
10
- REGEX_TEMPLATE_ID = re.compile(r'^(?![-])(?!.*[-]$)[a-zA-Z0-9-]+$')
11
- REGEX_KM_ID = REGEX_TEMPLATE_ID
12
- REGEX_MIME_TYPE = re.compile(r'^(?![-])(?!.*[-]$)[-\w.]+/[-\w.]+$')
13
-
14
- DEFAULT_LIST_FORMAT = '{template.id:<40} {template.name:<30}'
15
- DEFAULT_ENCODING = 'utf-8'
16
- DEFAULT_README = 'README.md'
17
-
18
- TEMPLATE_FILE = 'template.json'
19
- PATHSPEC_FACTORY = pathspec.patterns.GitWildMatchPattern
dsw_tdk/core.py DELETED
@@ -1,385 +0,0 @@
1
- import asyncio
2
- import datetime
3
- import json
4
- import logging
5
- import pathlib
6
- import re
7
- import watchgod # type: ignore
8
- import zipfile
9
-
10
- from typing import List, Optional, Tuple
11
-
12
- from dsw_tdk.api_client import DSWAPIClient, DSWCommunicationError
13
- from dsw_tdk.consts import DEFAULT_ENCODING, REGEX_SEMVER
14
- from dsw_tdk.model import TemplateProject, Template, TemplateFile, TemplateFileType
15
- from dsw_tdk.utils import UUIDGen
16
- from dsw_tdk.validation import ValidationError, TemplateValidator
17
-
18
-
19
- ChangeItem = Tuple[watchgod.Change, pathlib.Path]
20
-
21
-
22
- class TDKProcessingError(RuntimeError):
23
-
24
- def __init__(self, message: str, hint: str):
25
- self.message = message
26
- self.hint = hint
27
-
28
-
29
- METAMODEL_VERSION_SUPPORT = {
30
- 1: (2, 5, 0),
31
- 2: (2, 6, 0),
32
- 3: (2, 12, 0),
33
- 4: (3, 2, 0),
34
- 5: (3, 5, 0),
35
- 6: (3, 6, 0),
36
- 7: (3, 7, 0),
37
- 8: (3, 8, 0),
38
- 9: (3, 10, 0),
39
- 10: (3, 12, 0),
40
- }
41
-
42
-
43
- class TDKCore:
44
-
45
- def _check_metamodel_version(self):
46
- mm_ver = self.safe_template.metamodel_version
47
- api_version = self.remote_version.split('~', maxsplit=1)[0]
48
- if '-' in api_version:
49
- api_version = api_version.split('-', maxsplit=1)[0]
50
- if 'v' == api_version[0]:
51
- api_version = api_version[1:]
52
- if not re.match(REGEX_SEMVER, api_version):
53
- self.logger.warning(f'Using non-stable release of API: {self.remote_version}')
54
- return
55
- parts = api_version.split('.')
56
- ver = (int(parts[0]), int(parts[1]), int(parts[2]))
57
- vtag = f'v{ver[0]}.{ver[1]}.{ver[2]}'
58
- hint = 'Fix your metamodelVersion in template.json and/or visit docs'
59
- if mm_ver not in METAMODEL_VERSION_SUPPORT.keys():
60
- raise TDKProcessingError(f'Unknown metamodel version: {mm_ver}', hint)
61
- min_version = METAMODEL_VERSION_SUPPORT[mm_ver]
62
- if min_version > ver:
63
- raise TDKProcessingError(f'Unsupported metamodel version for API {vtag}', hint)
64
- if mm_ver + 1 in METAMODEL_VERSION_SUPPORT.keys():
65
- max_version = METAMODEL_VERSION_SUPPORT[mm_ver + 1]
66
- if ver >= max_version:
67
- raise TDKProcessingError(f'Unsupported metamodel version for API {vtag}', hint)
68
-
69
- def __init__(self, template: Optional[Template] = None, project: Optional[TemplateProject] = None,
70
- client: Optional[DSWAPIClient] = None, logger: Optional[logging.Logger] = None):
71
- self.template = template
72
- self.project = project
73
- self.client = client
74
- self.remote_version = 'unknown~??????'
75
- self.logger = logger or logging.getLogger()
76
- self.loop = asyncio.get_event_loop()
77
- self.changes_processor = ChangesProcessor(self)
78
-
79
- @property
80
- def safe_template(self) -> Template:
81
- if self.template is None:
82
- raise RuntimeError('No template is loaded')
83
- return self.template
84
-
85
- @property
86
- def safe_project(self) -> TemplateProject:
87
- if self.project is None:
88
- raise RuntimeError('No template is loaded')
89
- return self.project
90
-
91
- @property
92
- def safe_client(self) -> DSWAPIClient:
93
- if self.client is None:
94
- raise RuntimeError('No DSW API client specified')
95
- return self.client
96
-
97
- async def init_client(self, api_url: str, username: str, password: str):
98
- self.logger.info(f'Connecting to {api_url}')
99
- self.client = DSWAPIClient(api_url=api_url)
100
- await self.client.login(email=username, password=password)
101
- self.remote_version = await self.client.get_api_version()
102
- self.logger.info(f'Successfully authenticated as {username}')
103
- self.logger.debug(f'Connected to API version {self.remote_version}')
104
-
105
- def prepare_local(self, template_dir):
106
- self.logger.debug('Preparing local template project')
107
- self.project = TemplateProject(template_dir=template_dir, logger=self.logger)
108
-
109
- def load_local(self, template_dir):
110
- self.prepare_local(template_dir=template_dir)
111
- self.logger.info('Loading local template project')
112
- self.project.load()
113
-
114
- async def load_remote(self, template_id: str):
115
- self.logger.info(f'Retrieving template {template_id}')
116
- self.template = await self.safe_client.get_template(template_id=template_id)
117
- self.logger.debug('Retrieving template files')
118
- files = await self.safe_client.get_template_files(template_id=template_id)
119
- self.logger.info(f'Retrieved {len(files)} file(s)')
120
- for tfile in files:
121
- self.safe_template.files[tfile.filename.as_posix()] = tfile
122
- self.logger.debug('Retrieving template assets')
123
- assets = await self.safe_client.get_template_assets(template_id=template_id)
124
- self.logger.info(f'Retrieved {len(assets)} asset(s)')
125
- for tfile in assets:
126
- self.safe_template.files[tfile.filename.as_posix()] = tfile
127
-
128
- async def list_remote(self) -> List[Template]:
129
- self.logger.info('Listing remote templates')
130
- return await self.safe_client.get_templates()
131
-
132
- def verify(self) -> List[ValidationError]:
133
- template = self.template or self.safe_project.template
134
- if template is None:
135
- raise RuntimeError('No template is loaded')
136
- return TemplateValidator.collect_errors(template)
137
-
138
- def store_local(self, force: bool):
139
- if self.project is None:
140
- raise RuntimeError('No template project is initialized')
141
- self.project.template = self.safe_template
142
- self.logger.debug(f'Initiating storing local template project (force={force})')
143
- self.project.store(force=force)
144
-
145
- async def store_remote(self, force: bool):
146
- self.template = self.safe_project.template
147
- self._check_metamodel_version()
148
- template_id = self.safe_template.id
149
- template_exists = await self.safe_client.check_template_exists(template_id=template_id)
150
- if template_exists and force:
151
- self.logger.warning('Deleting existing remote template (forced)')
152
- result = await self.safe_client.delete_template(template_id=template_id)
153
- if not result:
154
- self.logger.error('Could not delete template (used by some documents?)')
155
- template_exists = not result
156
-
157
- if template_exists:
158
- self.logger.info('Updating existing remote template')
159
- await self.safe_client.put_template(template=self.safe_template)
160
- self.logger.debug('Retrieving remote assets')
161
- remote_assets = await self.safe_client.get_template_assets(template_id=self.safe_template.id)
162
- self.logger.debug('Retrieving remote files')
163
- remote_files = await self.safe_client.get_template_files(template_id=self.safe_template.id)
164
- await self.cleanup_remote_files(remote_assets=remote_assets, remote_files=remote_files)
165
- else:
166
- self.logger.info('Creating remote template')
167
- await self.safe_client.post_template(template=self.safe_template)
168
- await self.store_remote_files()
169
-
170
- async def _delete_template_file(self, tfile: TemplateFile, project_update: bool = False):
171
- try:
172
- self.logger.debug(f'Deleting existing remote {tfile.remote_type.value} '
173
- f'{tfile.filename.as_posix()} ({tfile.remote_id}) started')
174
- if tfile.remote_type == TemplateFileType.asset:
175
- result = await self.safe_client.delete_template_asset(template_id=self.safe_template.id, asset_id=tfile.remote_id)
176
- else:
177
- result = await self.safe_client.delete_template_file(template_id=self.safe_template.id, file_id=tfile.remote_id)
178
- self.logger.debug(f'Deleting existing remote {tfile.remote_type.value} '
179
- f'{tfile.filename.as_posix()} ({tfile.remote_id}) '
180
- f'finished: {"ok" if result else "failed"}')
181
- if project_update and result:
182
- self.safe_project.remove_template_file(tfile.filename)
183
- except Exception as e:
184
- self.logger.error(f'Failed to delete existing remote {tfile.remote_type.value} '
185
- f'{tfile.filename.as_posix()}: {e}')
186
-
187
- async def cleanup_remote_files(self, remote_assets: List[TemplateFile], remote_files: List[TemplateFile]):
188
- for tfile in self.safe_project.safe_template.files.values():
189
- self.logger.debug(f'Cleaning up remote {tfile.filename.as_posix()}')
190
- for remote_asset in remote_assets:
191
- if remote_asset.filename == tfile.filename:
192
- await self._delete_template_file(tfile=remote_asset, project_update=False)
193
- for remote_file in remote_files:
194
- if remote_file.filename == tfile.filename:
195
- await self._delete_template_file(tfile=remote_file, project_update=False)
196
-
197
- async def _create_template_file(self, tfile: TemplateFile, project_update: bool = False):
198
- try:
199
- self.logger.debug(f'Storing remote {tfile.remote_type.value} '
200
- f'{tfile.filename.as_posix()} started')
201
- if tfile.remote_type == TemplateFileType.asset:
202
- result = await self.safe_client.post_template_asset(template_id=self.safe_template.id, tfile=tfile)
203
- else:
204
- result = await self.safe_client.post_template_file(template_id=self.safe_template.id, tfile=tfile)
205
- self.logger.debug(f'Storing remote {tfile.remote_type.value} '
206
- f'{tfile.filename.as_posix()} finished: {result.remote_id}')
207
- if project_update and result is not None:
208
- self.safe_project.update_template_file(result)
209
- except Exception as e:
210
- self.logger.error(f'Failed to store remote {tfile.remote_type.value} {tfile.filename.as_posix()}: {e}')
211
-
212
- async def store_remote_files(self):
213
- for tfile in self.safe_project.template.files.values():
214
- tfile.remote_id = None
215
- tfile.remote_type = TemplateFileType.file if tfile.is_text else TemplateFileType.asset
216
- await self._create_template_file(tfile=tfile, project_update=True)
217
-
218
- def create_package(self, output: pathlib.Path, force: bool):
219
- if output.exists() and not force:
220
- raise RuntimeError(f'File {output} already exists (not forced)')
221
- self.logger.debug(f'Opening ZIP file for write: {output}')
222
- package = zipfile.ZipFile(output, mode='w', compression=zipfile.ZIP_DEFLATED)
223
- descriptor = self.safe_project.safe_template.serialize_remote()
224
- files = []
225
- assets = []
226
- for tfile in self.safe_project.safe_template.files.values():
227
- if tfile.is_text:
228
- self.logger.info(f'Adding template file {tfile.filename.as_posix()}')
229
- files.append({
230
- 'uuid': str(UUIDGen.generate()),
231
- 'content': tfile.content.decode(encoding=DEFAULT_ENCODING),
232
- 'fileName': str(tfile.filename.as_posix()),
233
- })
234
- else:
235
- self.logger.info(f'Adding template asset {tfile.filename.as_posix()}')
236
- assets.append({
237
- 'uuid': str(UUIDGen.generate()),
238
- 'contentType': tfile.content_type,
239
- 'fileName': str(tfile.filename.as_posix()),
240
- })
241
- self.logger.debug(f'Packaging template asset {tfile.filename}')
242
- package.writestr(str('template/assets/' + tfile.filename.as_posix()), tfile.content)
243
- descriptor['files'] = files
244
- descriptor['assets'] = assets
245
- timestamp = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ')
246
- descriptor['createdAt'] = timestamp
247
- descriptor['updatedAt'] = timestamp
248
- self.logger.debug('Packaging template.json file')
249
- package.writestr('template/template.json', data=json.dumps(descriptor, indent=4))
250
- self.logger.debug('Closing ZIP package')
251
- package.close()
252
- self.logger.debug('ZIP packaging done')
253
-
254
- async def watch_project(self, callback):
255
- async for changes in watchgod.awatch(self.safe_project.template_dir):
256
- await callback((
257
- change for change in ((change[0], pathlib.Path(change[1])) for change in changes)
258
- if self.safe_project.is_template_file(
259
- change[1], include_descriptor=True, include_readme=True
260
- )
261
- ))
262
-
263
- async def _update_descriptor(self):
264
- try:
265
- template_exists = await self.client.check_template_exists(template_id=self.safe_project.template.id)
266
- if template_exists:
267
- self.logger.info(f'Updating existing remote template {self.project.template.id}')
268
- await self.client.put_template(template=self.project.template)
269
- else:
270
- # TODO: optimization - reload full template and send it, skip all other changes
271
- self.logger.info(f'Template {self.safe_project.template.id} does not exist on remote - full sync')
272
- await self.store_remote(force=False)
273
- except DSWCommunicationError as e:
274
- self.logger.error(f'Failed to update template {self.safe_project.safe_template.id}: {e.message}')
275
- except Exception as e:
276
- self.logger.error(f'Failed to update template {self.safe_project.safe_template.id}: {e}')
277
-
278
- async def _delete_file(self, filepath: pathlib.Path):
279
- try:
280
- tfile = self.safe_project.get_template_file(filepath=filepath)
281
- if tfile is None:
282
- # TODO: try to check if exists on remote (may not be synced yet)
283
- self.logger.info(f'File {filepath.as_posix()} not tracked currently - skipping')
284
- return
285
- await self._delete_template_file(tfile=tfile, project_update=True)
286
- except Exception as e:
287
- self.logger.error(f'Failed to delete file {filepath.as_posix()}: {e}')
288
-
289
- async def _update_file(self, filepath: pathlib.Path):
290
- # TODO: optimization - use PUT if possible
291
- try:
292
- tfile = self.safe_project.get_template_file(filepath=filepath)
293
- if tfile is not None:
294
- await self._delete_template_file(tfile=tfile)
295
- tfile = self.safe_project.load_file(filepath=filepath)
296
- await self._create_template_file(tfile=tfile, project_update=True)
297
- except Exception as e:
298
- self.logger.error(f'Failed to update file {filepath.as_posix()}: {e}')
299
-
300
- async def process_changes(self, changes: List[ChangeItem], force: bool):
301
- self.changes_processor.clear()
302
- try:
303
- await self.changes_processor.process_changes(changes, force)
304
- except Exception as e:
305
- self.logger.error(f'Failed to process changes: {e}')
306
-
307
-
308
- class ChangesProcessor:
309
-
310
- def __init__(self, tdk: TDKCore):
311
- self.tdk = tdk # type: TDKCore
312
- self.descriptor_change = None # type: Optional[ChangeItem]
313
- self.readme_change = None # type: Optional[ChangeItem]
314
- self.file_changes = [] # type: List[ChangeItem]
315
-
316
- def clear(self):
317
- self.descriptor_change = None
318
- self.readme_change = None
319
- self.file_changes = []
320
-
321
- def _split_changes(self, changes: List[ChangeItem]):
322
- for change in changes:
323
- if change[1] == self.tdk.safe_project.descriptor_path:
324
- self.descriptor_change = change
325
- elif change[1] == self.tdk.safe_project.used_readme:
326
- self.readme_change = change
327
- elif self.tdk.safe_project.is_template_file(change[1]):
328
- self.file_changes.append(change)
329
-
330
- async def _process_file_changes(self):
331
- deleted = set()
332
- updated = set()
333
- for file_change in self.file_changes:
334
- self.tdk.logger.debug(f'Processing {file_change}')
335
- change_type = file_change[0]
336
- filepath = file_change[1]
337
- if change_type == watchgod.Change.deleted and filepath not in deleted:
338
- self.tdk.logger.debug('Scheduling delete operation')
339
- deleted.add(filepath)
340
- await self.tdk._delete_file(filepath)
341
- elif filepath not in updated:
342
- self.tdk.logger.debug('Scheduling update operation')
343
- updated.add(filepath)
344
- await self.tdk._update_file(filepath)
345
-
346
- async def _reload_descriptor(self, force: bool) -> bool:
347
- if self.descriptor_change is None:
348
- return False
349
- if self.descriptor_change[0] == watchgod.Change.deleted:
350
- raise RuntimeError(f'Deleted template descriptor {self.tdk.safe_project.descriptor_path} ... the end')
351
- self.tdk.logger.debug(f'Reloading {TemplateProject.TEMPLATE_FILE} file')
352
- previous_id = self.tdk.safe_project.safe_template.id
353
- self.tdk.safe_project.load_descriptor()
354
- self.tdk.safe_project.load_readme()
355
- new_id = self.tdk.safe_project.safe_template.id
356
- if new_id != previous_id:
357
- self.tdk.logger.warning(f'Template ID changed from {previous_id} to {new_id}')
358
- self.tdk.safe_project.load()
359
- await self.tdk.store_remote(force=force)
360
- self.tdk.logger.info('Template fully reloaded... waiting for new changes')
361
- return True
362
- return False
363
-
364
- async def _reload_readme(self) -> bool:
365
- if self.readme_change is None:
366
- return False
367
- if self.readme_change[0] == watchgod.Change.deleted:
368
- raise RuntimeError(f'Deleted used README file {self.tdk.safe_project.used_readme}')
369
- self.tdk.logger.debug('Reloading README file')
370
- self.tdk.safe_project.load_readme()
371
- return True
372
-
373
- async def _update_descriptor(self):
374
- if self.readme_change is not None or self.descriptor_change is not None:
375
- self.tdk.logger.debug('Updating template descriptor (metadata)')
376
- await self.tdk._update_descriptor()
377
-
378
- async def process_changes(self, changes: List[ChangeItem], force: bool):
379
- self._split_changes(changes)
380
- full_reload = await self._reload_descriptor(force)
381
- if not full_reload:
382
- await self._reload_readme()
383
- await self._update_descriptor()
384
- await self._process_file_changes()
385
- self.tdk.logger.info('All changes processed... waiting for new changes')
dsw_tdk/validation.py DELETED
@@ -1,206 +0,0 @@
1
- import re
2
-
3
- from typing import List, Dict
4
-
5
- from dsw_tdk.consts import REGEX_SEMVER, REGEX_ORGANIZATION_ID, \
6
- REGEX_TEMPLATE_ID, REGEX_MIME_TYPE, REGEX_KM_ID
7
- from dsw_tdk.model import PackageFilter, Format, Step
8
-
9
-
10
- class ValidationError(BaseException):
11
- def __init__(self, field_name: str, message: str):
12
- self.field_name = field_name
13
- self.message = message
14
-
15
-
16
- def _validate_required(field_name: str, value) -> List[ValidationError]:
17
- if value is None:
18
- return [ValidationError(field_name, 'Missing but it is required')]
19
- return []
20
-
21
-
22
- def _validate_non_empty(field_name: str, value) -> List[ValidationError]:
23
- if value is not None and len(value.strip()) == 0:
24
- return [ValidationError(field_name, 'Cannot be empty or only-whitespace')]
25
- return []
26
-
27
-
28
- def _validate_content_type(field_name: str, value) -> List[ValidationError]:
29
- if value is not None and re.match(REGEX_MIME_TYPE, value) is None:
30
- return [ValidationError(field_name, 'Content type should be valid IANA media type')]
31
- return []
32
-
33
-
34
- def _validate_extension(field_name: str, value) -> List[ValidationError]:
35
- if value is not None and re.match(REGEX_ORGANIZATION_ID, value) is None:
36
- return [ValidationError(field_name, 'File extension should contain only letters, numbers and dots (inside-only)')]
37
- return []
38
-
39
-
40
- def _validate_organization_id(field_name: str, value) -> List[ValidationError]:
41
- if value is not None and re.match(REGEX_ORGANIZATION_ID, value) is None:
42
- return [ValidationError(field_name, 'Organization ID may contain only letters, numbers, and period (inside-only)')]
43
- return []
44
-
45
-
46
- def _validate_template_id(field_name: str, value) -> List[ValidationError]:
47
- if value is not None and re.match(REGEX_TEMPLATE_ID, value) is None:
48
- return [ValidationError(field_name, 'Template ID may contain only letters, numbers, and dash (inside-only)')]
49
- return []
50
-
51
-
52
- def _validate_km_id(field_name: str, value) -> List[ValidationError]:
53
- if value is not None and re.match(REGEX_KM_ID, value) is None:
54
- return [ValidationError(field_name, 'KM ID may contain only letters, numbers, and dash (inside-only)')]
55
- return []
56
-
57
-
58
- def _validate_version(field_name: str, value) -> List[ValidationError]:
59
- if value is not None and re.match(REGEX_SEMVER, value) is None:
60
- return [ValidationError(field_name, 'Version must be in semver format <NUM>.<NUM>.<NUM>')]
61
- return []
62
-
63
-
64
- def _validate_natural(field_name: str, value) -> List[ValidationError]:
65
- if value is not None and (not isinstance(value, int) or value < 1):
66
- return [ValidationError(field_name, 'Field {field_name} must be positive integer')]
67
- return []
68
-
69
-
70
- def _validate_package_id(field_name: str, value: str) -> List[ValidationError]:
71
- res = []
72
- if value is None:
73
- return res
74
- if not isinstance(value, str):
75
- return [ValidationError(field_name, 'Package ID is not a string')]
76
- parts = value.split(':')
77
- if len(parts) != 3:
78
- res.append(ValidationError(field_name, 'Package ID is not valid (only {len(parts)} parts)'))
79
- if re.match(REGEX_ORGANIZATION_ID, parts[0]) is None:
80
- res.append(ValidationError(field_name, 'Package ID contains invalid organization id'))
81
- if re.match(REGEX_KM_ID, parts[1]) is None:
82
- res.append(ValidationError(field_name, 'Package ID contains invalid KM id'))
83
- if re.match(REGEX_SEMVER, parts[2]) is None:
84
- res.append(ValidationError(field_name, 'Package ID contains invalid version'))
85
- return res
86
-
87
-
88
- def _validate_jinja_options(field_name: str, value: Dict[str, str]) -> List[ValidationError]:
89
- res = []
90
- if value is None:
91
- return res
92
- for k in ('template', 'content-type', 'extension'):
93
- if k not in value.keys():
94
- res.append(ValidationError(field_name, 'Jinja option cannot be left out'))
95
- elif value[k] is None or not isinstance(value[k], str) or len(value[k]) == 0:
96
- res.append(ValidationError(field_name, 'Jinja option cannot be empty'))
97
- if 'content-type' in value.keys():
98
- res.extend(_validate_content_type(f'{field_name}.content-type', value['content-type']))
99
- return res
100
-
101
-
102
- class GenericValidator:
103
-
104
- def __init__(self, rules):
105
- self.rules = rules
106
-
107
- def validate_field(self, entity, field_name: str):
108
- for validator in self.rules.get(field_name, []):
109
- err = validator(field_name, getattr(entity, field_name))
110
- if len(err) != 0:
111
- raise err[0]
112
-
113
- def validate(self, entity, field_name_prefix: str = ''):
114
- for field_name, validators in self.rules.items():
115
- if field_name.startswith('__'):
116
- continue
117
- for validator in validators:
118
- err = validator(field_name_prefix + field_name, getattr(entity, field_name))
119
- if len(err) != 0:
120
- raise err[0]
121
- if '__all' in self.rules.keys():
122
- err = self.rules['__all'](field_name_prefix, entity)
123
- if len(err) != 0:
124
- raise err[0]
125
-
126
- def collect_errors(self, entity, field_name_prefix: str = '') -> List[ValidationError]:
127
- result = []
128
- for field_name, validators in self.rules.items():
129
- if field_name.startswith('__'):
130
- continue
131
- for validator in validators:
132
- result.extend(validator(field_name_prefix + field_name, getattr(entity, field_name)))
133
- if '__all' in self.rules.keys():
134
- result.extend(self.rules['__all'](field_name_prefix, entity))
135
- return result
136
-
137
-
138
- PackageFilterValidator = GenericValidator({
139
- 'organization_id': [_validate_organization_id],
140
- 'km_id': [_validate_km_id],
141
- 'min_version': [_validate_version],
142
- 'max_version': [_validate_version],
143
- })
144
-
145
-
146
- def _validate_package_filters(field_name: str, value: List[PackageFilter]) -> List[ValidationError]:
147
- res = []
148
- for v in value:
149
- res.extend(PackageFilterValidator.collect_errors(v, field_name_prefix=f'{field_name}.'))
150
- return res
151
-
152
-
153
- def _validate_step(field_name_prefix: str, value: Step) -> List[ValidationError]:
154
- if value.name == 'jinja':
155
- return _validate_jinja_options(f'{field_name_prefix}options', value.options)
156
- return []
157
-
158
-
159
- StepValidator = GenericValidator({
160
- 'name': [_validate_non_empty],
161
- 'options': [],
162
- '__all': _validate_step
163
- })
164
-
165
-
166
- def _validate_steps(field_name: str, value: List[Step]) -> List[ValidationError]:
167
- res = []
168
- for v in value:
169
- res.extend(StepValidator.collect_errors(v, field_name_prefix=f'{field_name}.'))
170
- return res
171
-
172
-
173
- FormatValidator = GenericValidator({
174
- 'uuid': [_validate_required, _validate_non_empty],
175
- 'name': [_validate_required, _validate_non_empty],
176
- 'short_name': [_validate_required, _validate_non_empty],
177
- 'icon': [_validate_required, _validate_non_empty],
178
- 'color': [_validate_required, _validate_non_empty],
179
- 'steps': [_validate_required, _validate_steps],
180
- })
181
-
182
-
183
- def _validate_formats(field_name: str, value: List[Format]) -> List[ValidationError]:
184
- res = []
185
- uuids = set()
186
- for v in value:
187
- if v.uuid in uuids:
188
- res.append(ValidationError(field_name, f'Duplicate format UUID {v.uuid}'))
189
- uuids.add(v.uuid)
190
- res.extend(FormatValidator.collect_errors(v, field_name_prefix=f'{field_name}.'))
191
- return res
192
-
193
-
194
- TemplateValidator = GenericValidator({
195
- 'template_id': [_validate_required, _validate_template_id],
196
- 'organization_id': [_validate_required, _validate_organization_id],
197
- 'version': [_validate_required, _validate_version],
198
- 'name': [_validate_required, _validate_non_empty],
199
- 'description': [_validate_required, _validate_non_empty],
200
- 'readme': [_validate_required, _validate_non_empty],
201
- 'recommended_package_id': [_validate_package_id],
202
- 'license': [_validate_required, _validate_non_empty],
203
- 'metamodel_version': [_validate_natural],
204
- 'allowed_packages': [_validate_package_filters],
205
- 'formats': [_validate_required, _validate_formats],
206
- })