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 → dsw/tdk}/__init__.py +16 -15
- dsw/tdk/__main__.py +5 -0
- dsw/tdk/api_client.py +407 -0
- dsw/tdk/build_info.py +17 -0
- dsw/tdk/cli.py +708 -0
- dsw/tdk/config.py +151 -0
- dsw/tdk/consts.py +25 -0
- dsw/tdk/core.py +565 -0
- {dsw_tdk → dsw/tdk}/model.py +468 -422
- dsw/tdk/py.typed +0 -0
- dsw/tdk/templates/LICENSE.j2 +1 -0
- {dsw_tdk → dsw/tdk}/templates/README.md.j2 +13 -13
- dsw/tdk/templates/env.j2 +3 -0
- {dsw_tdk → dsw/tdk}/templates/starter.j2 +13 -14
- {dsw_tdk → dsw/tdk}/utils.py +198 -173
- dsw/tdk/validation.py +290 -0
- {dsw_tdk-3.13.0.dist-info → dsw_tdk-4.27.0.dist-info}/METADATA +28 -33
- dsw_tdk-4.27.0.dist-info/RECORD +20 -0
- {dsw_tdk-3.13.0.dist-info → dsw_tdk-4.27.0.dist-info}/WHEEL +1 -2
- dsw_tdk-4.27.0.dist-info/entry_points.txt +3 -0
- dsw_tdk/__main__.py +0 -3
- dsw_tdk/api_client.py +0 -273
- dsw_tdk/cli.py +0 -412
- dsw_tdk/consts.py +0 -19
- dsw_tdk/core.py +0 -385
- dsw_tdk/validation.py +0 -206
- dsw_tdk-3.13.0.dist-info/LICENSE +0 -201
- dsw_tdk-3.13.0.dist-info/RECORD +0 -17
- dsw_tdk-3.13.0.dist-info/entry_points.txt +0 -3
- dsw_tdk-3.13.0.dist-info/top_level.txt +0 -1
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
|
-
})
|