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/core.py ADDED
@@ -0,0 +1,565 @@
1
+ import asyncio
2
+ import datetime
3
+ import io
4
+ import json
5
+ import logging
6
+ import pathlib
7
+ import shutil
8
+ import tempfile
9
+ import zipfile
10
+
11
+ import watchfiles
12
+
13
+ from . import consts
14
+ from .api_client import WizardAPIClient, WizardCommunicationError
15
+ from .model import Template, TemplateFile, TemplateFileType, TemplateProject
16
+ from .utils import UUIDGen
17
+ from .validation import TemplateValidator, ValidationError
18
+
19
+
20
+ ChangeItem = tuple[watchfiles.Change, pathlib.Path]
21
+
22
+
23
+ def _change(item: ChangeItem, root: pathlib.Path) -> str:
24
+ return f'{item[0].name.upper()}[{item[1].relative_to(root).as_posix()}]'
25
+
26
+
27
+ class TDKProcessingError(RuntimeError):
28
+
29
+ def __init__(self, message: str, hint: str):
30
+ self.message = message
31
+ self.hint = hint
32
+
33
+
34
+ # pylint: disable-next=too-many-public-methods
35
+ class TDKCore:
36
+
37
+ # pylint: disable-next=too-many-locals
38
+ def _check_metamodel_version(self):
39
+ hint = 'Fix your metamodelVersion in template.json and/or visit docs'
40
+ if self.remote_metamodel_version is None:
41
+ self.logger.warning('Remote metamodel version is unknown, you should use '
42
+ 'a matching version of TDK to avoid issues')
43
+ return
44
+ mm_ver = str(self.safe_template.metamodel_version)
45
+ try:
46
+ if '.' not in mm_ver:
47
+ mm_ver = f'{mm_ver}.0'
48
+ mm_major, mm_minor = map(int, mm_ver.split('.'))
49
+ except ValueError as e:
50
+ raise TDKProcessingError(f'Invalid metamodel version format: {mm_ver}', hint) from e
51
+ mmr_ver = self.remote_metamodel_version
52
+ try:
53
+ if '.' not in mmr_ver:
54
+ mmr_ver = f'{mmr_ver}.0'
55
+ mmr_major, mmr_minor = map(int, mmr_ver.split('.'))
56
+ except ValueError as e:
57
+ raise TDKProcessingError(f'Invalid remote metamodel version format: {mmr_ver}',
58
+ 'Check if connecting to correct API with matching version '
59
+ 'as you have in TDK.') from e
60
+ if (mm_major, mm_minor) == (mmr_major, mmr_minor):
61
+ self.logger.debug('Metamodel version %s matches remote version %s',
62
+ mmr_ver, mmr_ver)
63
+ elif mm_major == mmr_major and mm_minor < mmr_minor:
64
+ self.logger.warning('Local metamodel version %s is older than remote version %s, '
65
+ 'but still compatible', mm_ver, mmr_ver)
66
+ else:
67
+ raise TDKProcessingError(
68
+ f'Unsupported metamodel version: local {mm_ver}, remote {mmr_ver}',
69
+ hint,
70
+ )
71
+
72
+ def __init__(self, template: Template | None = None, project: TemplateProject | None = None,
73
+ client: WizardAPIClient | None = None, logger: logging.Logger | None = None):
74
+ self.template = template
75
+ self.project = project
76
+ self.client = client
77
+ self.remote_version: str = 'unknown~??????'
78
+ self.remote_metamodel_version: str | None = 'unknown'
79
+ self.logger = logger or logging.getLogger()
80
+ self.loop = asyncio.get_event_loop()
81
+ self.changes_processor = ChangesProcessor(self)
82
+ self.remote_id = 'unknown'
83
+
84
+ async def close(self):
85
+ await self.safe_client.close()
86
+
87
+ @property
88
+ def safe_template(self) -> Template:
89
+ if self.template is None:
90
+ raise RuntimeError('No template is loaded')
91
+ return self.template
92
+
93
+ @property
94
+ def safe_project(self) -> TemplateProject:
95
+ if self.project is None:
96
+ raise RuntimeError('No template is loaded')
97
+ return self.project
98
+
99
+ @property
100
+ def safe_client(self) -> WizardAPIClient:
101
+ if self.client is None:
102
+ raise RuntimeError('No DSW API client specified')
103
+ return self.client
104
+
105
+ async def init_client(self, api_url: str, api_key: str):
106
+ self.logger.info('Connecting to %s', api_url)
107
+ self.client = WizardAPIClient(api_url=api_url, api_key=api_key)
108
+ self.remote_version, self.remote_metamodel_version = await self.client.get_api_version()
109
+ user = await self.client.get_current_user()
110
+ self.logger.info('Successfully authenticated as %s %s (%s)',
111
+ user['firstName'], user['lastName'], user['email'])
112
+ self.logger.debug('Connected to API version %s', self.remote_version)
113
+
114
+ def prepare_local(self, template_dir):
115
+ self.logger.debug('Preparing local template project')
116
+ self.project = TemplateProject(template_dir=template_dir, logger=self.logger)
117
+
118
+ def load_local(self, template_dir):
119
+ self.prepare_local(template_dir=template_dir)
120
+ self.logger.info('Loading local template project')
121
+ self.safe_project.load()
122
+
123
+ async def load_remote(self, template_id: str):
124
+ self.logger.info('Retrieving template draft %s', template_id)
125
+ self.template = await self.safe_client.get_template_draft(remote_id=template_id)
126
+ self.logger.debug('Retrieving template draft files')
127
+ files = await self.safe_client.get_template_draft_files(remote_id=template_id)
128
+ self.logger.info('Retrieved %s file(s)', len(files))
129
+ for file in files:
130
+ self.safe_template.files[file.filename.as_posix()] = file
131
+ self.logger.debug('Retrieving template draft assets')
132
+ assets = await self.safe_client.get_template_draft_assets(remote_id=template_id)
133
+ self.logger.info('Retrieved %s asset(s)', len(assets))
134
+ for asset in assets:
135
+ self.safe_template.files[asset.filename.as_posix()] = asset
136
+
137
+ async def download_bundle(self, template_id: str) -> bytes:
138
+ self.logger.info('Retrieving template %s bundle', template_id)
139
+ return await self.safe_client.get_template_bundle(remote_id=template_id)
140
+
141
+ async def list_remote_templates(self) -> list[Template]:
142
+ self.logger.info('Listing remote document templates')
143
+ return await self.safe_client.get_templates()
144
+
145
+ async def list_remote_drafts(self) -> list[Template]:
146
+ self.logger.info('Listing remote document template drafts')
147
+ return await self.safe_client.get_drafts()
148
+
149
+ def verify(self) -> list[ValidationError]:
150
+ template = self.template or self.safe_project.template
151
+ if template is None:
152
+ raise RuntimeError('No template is loaded')
153
+ return TemplateValidator.collect_errors(template)
154
+
155
+ def store_local(self, force: bool):
156
+ if self.project is None:
157
+ raise RuntimeError('No template project is initialized')
158
+ self.project.template = self.safe_template
159
+ if self.project.template is None:
160
+ raise RuntimeError('No template is loaded in the project')
161
+ if len(self.project.safe_template.tdk_config.files) == 0:
162
+ self.project.safe_template.tdk_config.use_default_files()
163
+ self.logger.warning('Using default _tdk.files in template.json, you may want '
164
+ 'to change it to include relevant files')
165
+ self.logger.debug('Initiating storing local template project (force=%s)', force)
166
+ self.project.store(force=force)
167
+
168
+ async def store_remote(self, force: bool):
169
+ self.template = self.safe_project.template
170
+ self._check_metamodel_version()
171
+ org_id = await self.safe_client.get_organization_id()
172
+ if org_id != self.safe_template.organization_id:
173
+ self.logger.warning('There is different organization ID set in the DSW instance'
174
+ ' (local: %s, remote: %s)',
175
+ self.safe_template.organization_id, org_id)
176
+ self.remote_id = self.safe_template.id_with_org(org_id)
177
+ template_exists = await self.safe_client.check_draft_exists(remote_id=self.remote_id)
178
+ if template_exists and force:
179
+ self.logger.warning('Deleting existing remote document template draft (forced)')
180
+ result = await self.safe_client.delete_template_draft(remote_id=self.remote_id)
181
+ if not result:
182
+ self.logger.error('Could not delete document template draft')
183
+ template_exists = not result
184
+
185
+ if template_exists:
186
+ self.logger.info('Updating existing remote document template draft')
187
+ await self.safe_client.update_template_draft(
188
+ template=self.safe_template,
189
+ remote_id=self.remote_id,
190
+ )
191
+ self.logger.debug('Retrieving remote assets')
192
+ remote_assets = await self.safe_client.get_template_draft_assets(
193
+ remote_id=self.remote_id,
194
+ )
195
+ self.logger.debug('Retrieving remote files')
196
+ remote_files = await self.safe_client.get_template_draft_files(
197
+ remote_id=self.remote_id,
198
+ )
199
+ await self.cleanup_remote_files(
200
+ remote_assets=remote_assets,
201
+ remote_files=remote_files,
202
+ )
203
+ else:
204
+ self.logger.info('Creating remote document template draft')
205
+ await self.safe_client.create_new_template_draft(
206
+ template=self.safe_template,
207
+ remote_id=self.remote_id,
208
+ )
209
+ await self.store_remote_files()
210
+
211
+ async def _update_template_file(self, remote_file: TemplateFile, local_file: TemplateFile,
212
+ project_update: bool = False):
213
+ try:
214
+ self.logger.debug('Updating existing remote %s %s (%s) started',
215
+ remote_file.remote_type.value, remote_file.filename.as_posix(),
216
+ remote_file.remote_id)
217
+ local_file.remote_id = remote_file.remote_id
218
+ if remote_file.remote_type == TemplateFileType.ASSET:
219
+ result = await self.safe_client.put_template_draft_asset_content(
220
+ remote_id=self.remote_id,
221
+ file=local_file,
222
+ )
223
+ else:
224
+ result = await self.safe_client.put_template_draft_file_content(
225
+ remote_id=self.remote_id,
226
+ file=local_file,
227
+ )
228
+ self.logger.debug('Updating existing remote %s %s (%s) finished: %s',
229
+ remote_file.remote_type.value, remote_file.filename.as_posix(),
230
+ remote_file.remote_id, 'ok' if result else 'failed')
231
+ if project_update and result:
232
+ self.safe_project.update_template_file(result)
233
+ except Exception as e1:
234
+ try:
235
+ self.logger.debug('Trying to delete/create due to: %s', str(e1))
236
+ await self._delete_template_file(file=remote_file)
237
+ await self._create_template_file(file=local_file, project_update=True)
238
+ except Exception as e2:
239
+ self.logger.error('Failed to update existing remote %s %s: %s',
240
+ remote_file.remote_type.value,
241
+ remote_file.filename.as_posix(), e2)
242
+
243
+ async def _delete_template_file(self, file: TemplateFile, project_update: bool = False):
244
+ try:
245
+ self.logger.debug('Deleting existing remote %s %s (%s) started',
246
+ file.remote_type.value, file.filename.as_posix(),
247
+ file.remote_id)
248
+ if file.remote_type == TemplateFileType.ASSET:
249
+ result = await self.safe_client.delete_template_draft_asset(
250
+ remote_id=self.remote_id,
251
+ asset_id=file.remote_id,
252
+ )
253
+ else:
254
+ result = await self.safe_client.delete_template_draft_file(
255
+ remote_id=self.remote_id,
256
+ file_id=file.remote_id,
257
+ )
258
+ self.logger.debug('Deleting existing remote %s %s (%s) finished: %s',
259
+ file.remote_type.value,
260
+ file.filename.as_posix(),
261
+ file.remote_id, 'ok' if result else 'failed')
262
+ if project_update and result:
263
+ self.safe_project.remove_template_file(file.filename)
264
+ except Exception as e:
265
+ self.logger.error('Failed to delete existing remote %s %s: %s',
266
+ file.remote_type.value, file.filename.as_posix(), e)
267
+
268
+ async def cleanup_remote_files(self, remote_assets: list[TemplateFile],
269
+ remote_files: list[TemplateFile]):
270
+ for local_file in self.safe_project.safe_template.files.values():
271
+ self.logger.debug('Cleaning up remote %s', local_file.filename.as_posix())
272
+ for remote_asset in remote_assets:
273
+ if remote_asset.filename == local_file.filename:
274
+ await self._delete_template_file(file=remote_asset, project_update=False)
275
+ for remote_file in remote_files:
276
+ if remote_file.filename == local_file.filename:
277
+ await self._delete_template_file(file=remote_file, project_update=False)
278
+
279
+ async def _create_template_file(self, file: TemplateFile, project_update: bool = False):
280
+ try:
281
+ self.logger.debug('Storing remote %s %s started',
282
+ file.remote_type.value, file.filename.as_posix())
283
+ if file.remote_type == TemplateFileType.ASSET:
284
+ result = await self.safe_client.post_template_draft_asset(
285
+ remote_id=self.remote_id,
286
+ file=file,
287
+ )
288
+ else:
289
+ result = await self.safe_client.post_template_draft_file(
290
+ remote_id=self.remote_id,
291
+ file=file,
292
+ )
293
+ self.logger.debug('Storing remote %s %s finished: %s',
294
+ file.remote_type.value, file.filename.as_posix(),
295
+ result.remote_id)
296
+ if project_update and result is not None:
297
+ self.safe_project.update_template_file(result)
298
+ except Exception as e:
299
+ self.logger.error('Failed to store remote %s %s: %s',
300
+ file.remote_type.value, file.filename.as_posix(), e)
301
+
302
+ async def store_remote_files(self):
303
+ if len(self.safe_project.safe_template.files) == 0:
304
+ self.logger.warning('No files to store, maybe you forgot to '
305
+ 'update _tdk.files patterns in template.json?')
306
+ for file in self.safe_project.safe_template.files.values():
307
+ file.remote_id = None
308
+ file.remote_type = TemplateFileType.FILE if file.is_text else TemplateFileType.ASSET
309
+ await self._create_template_file(file=file, project_update=True)
310
+
311
+ def create_package(self, output: pathlib.Path, force: bool):
312
+ if output.exists() and not force:
313
+ raise RuntimeError(f'File {output} already exists (not forced)')
314
+ self.logger.debug('Opening ZIP file for write: %s', output.as_posix())
315
+ with zipfile.ZipFile(output, mode='w', compression=zipfile.ZIP_DEFLATED) as pkg:
316
+ descriptor = self.safe_project.safe_template.serialize_remote()
317
+ files = []
318
+ assets = []
319
+ for file in self.safe_project.safe_template.files.values():
320
+ if file.is_text:
321
+ self.logger.info('Adding template file %s', file.filename.as_posix())
322
+ files.append({
323
+ 'uuid': str(UUIDGen.generate()),
324
+ 'content': file.content.decode(encoding=consts.DEFAULT_ENCODING),
325
+ 'fileName': str(file.filename.as_posix()),
326
+ })
327
+ else:
328
+ self.logger.info('Adding template asset %s',
329
+ file.filename.as_posix())
330
+ assets.append({
331
+ 'uuid': str(UUIDGen.generate()),
332
+ 'contentType': file.content_type,
333
+ 'fileName': str(file.filename.as_posix()),
334
+ })
335
+ self.logger.debug('Packaging template asset %s',
336
+ file.filename.as_posix())
337
+ pkg.writestr(f'template/assets/{file.filename.as_posix()}',
338
+ file.content)
339
+ descriptor['files'] = files
340
+ descriptor['assets'] = assets
341
+ if len(files) == 0 and len(assets) == 0:
342
+ self.logger.warning('No files or assets found in the template, maybe you forgot '
343
+ 'to update _tdk.files patterns in template.json?')
344
+ timestamp = datetime.datetime.now(tz=datetime.UTC).strftime('%Y-%m-%dT%H:%M:%S.%fZ')
345
+ descriptor['createdAt'] = timestamp
346
+ descriptor['updatedAt'] = timestamp
347
+ self.logger.debug('Packaging template.json file')
348
+ pkg.writestr('template/template.json',
349
+ data=json.dumps(descriptor, indent=4))
350
+ self.logger.debug('ZIP packaging done')
351
+
352
+ # pylint: disable=too-many-locals
353
+ def extract_package(self, zip_data: bytes, template_dir: pathlib.Path | None, force: bool):
354
+ with tempfile.TemporaryDirectory() as tmp_dir:
355
+ io_zip = io.BytesIO(zip_data)
356
+ with zipfile.ZipFile(io_zip) as pkg:
357
+ pkg.extractall(tmp_dir)
358
+ del io_zip
359
+ tmp_root = pathlib.Path(tmp_dir) / 'template'
360
+ file = tmp_root / 'template.json'
361
+ assets_dir = tmp_root / 'assets'
362
+ self.logger.debug('Extracting template data')
363
+ if not file.exists():
364
+ raise RuntimeError('Malformed package: missing template.json file')
365
+ data = json.loads(file.read_text(encoding=consts.DEFAULT_ENCODING))
366
+ template = Template.load_local(data)
367
+ template.tdk_config.use_default_files()
368
+ self.logger.warning('Using default _tdk.files in template.json, you may want '
369
+ 'to change it to include relevant files')
370
+ self.logger.debug('Preparing template dir')
371
+ if template_dir is None:
372
+ template_dir = pathlib.Path.cwd() / template.id.replace(':', '_')
373
+ if template_dir.exists():
374
+ if not force:
375
+ raise RuntimeError(f'Template directory already exists: '
376
+ f'{template_dir.as_posix()} (use force?)')
377
+ shutil.rmtree(template_dir, ignore_errors=True)
378
+ template_dir.mkdir(parents=True)
379
+ self.logger.debug('Extracting template.json from package')
380
+ local_template_json = template_dir / 'template.json'
381
+ local_template_json.write_text(
382
+ data=json.dumps(template.serialize_local_new(), indent=2),
383
+ encoding=consts.DEFAULT_ENCODING,
384
+ )
385
+ self.logger.debug('Extracting README.md from package')
386
+ local_readme = template_dir / 'README.md'
387
+ local_readme.write_text(
388
+ data=data['readme'].replace('\r\n', '\n'),
389
+ encoding=consts.DEFAULT_ENCODING,
390
+ )
391
+ self.logger.debug('Extracting assets from package')
392
+ for asset_file in assets_dir.rglob('*'):
393
+ if asset_file.is_file():
394
+ target_asset = template_dir / asset_file.relative_to(assets_dir)
395
+ target_dir = target_asset.parent
396
+ target_dir.mkdir(parents=True, exist_ok=True)
397
+ target_asset.write_bytes(asset_file.read_bytes())
398
+ self.logger.debug('Extracting files from package')
399
+ for file_item in data.get('files', []):
400
+ filename = file_item['fileName']
401
+ content = file_item['content'].replace('\r\n', '\n')
402
+ target_file = template_dir / filename
403
+ target_dir = target_file.parent
404
+ target_dir.mkdir(parents=True, exist_ok=True)
405
+ target_file.write_text(data=content, encoding=consts.DEFAULT_ENCODING)
406
+ self.logger.debug('Extracting package done')
407
+
408
+ async def watch_project(self, callback, stop_event: asyncio.Event):
409
+ async for changes in watchfiles.awatch(
410
+ self.safe_project.template_dir,
411
+ stop_event=stop_event,
412
+ ):
413
+ await callback(
414
+ change for change in ((change[0], pathlib.Path(change[1])) for change in changes)
415
+ if self.safe_project.is_template_file(
416
+ change[1],
417
+ include_descriptor=True,
418
+ include_readme=True,
419
+ )
420
+ )
421
+
422
+ async def update_descriptor(self):
423
+ try:
424
+ template_exists = await self.safe_client.check_draft_exists(
425
+ remote_id=self.remote_id,
426
+ )
427
+ if template_exists:
428
+ self.logger.info('Updating existing remote document template draft %s',
429
+ self.safe_project.safe_template.id)
430
+ await self.safe_client.update_template_draft(
431
+ template=self.safe_project.safe_template,
432
+ remote_id=self.remote_id,
433
+ )
434
+ else:
435
+ self.logger.info('Document template draft %s does '
436
+ 'not exist on remote - full sync',
437
+ self.safe_project.safe_template.id)
438
+ await self.store_remote(force=False)
439
+ except WizardCommunicationError as e:
440
+ self.logger.error('Failed to update document template draft %s: %s',
441
+ self.safe_project.safe_template.id, e.message)
442
+ except Exception as e:
443
+ self.logger.error('Failed to update document template draft %s: %s',
444
+ self.safe_project.safe_template.id, e)
445
+
446
+ async def delete_file(self, filepath: pathlib.Path):
447
+ if not filepath.is_file():
448
+ self.logger.debug('%s is not a regular file - skipping',
449
+ filepath.as_posix())
450
+ return
451
+ try:
452
+ file = self.safe_project.get_template_file(filepath=filepath)
453
+ if file is None:
454
+ self.logger.info('File %s not tracked currently - skipping',
455
+ filepath.as_posix())
456
+ return
457
+ await self._delete_template_file(file=file, project_update=True)
458
+ except Exception as e:
459
+ self.logger.error('Failed to delete file %s: %s',
460
+ filepath.as_posix(), e)
461
+
462
+ async def update_file(self, filepath: pathlib.Path):
463
+ if not filepath.is_file():
464
+ self.logger.debug('%s is not a regular file - skipping',
465
+ filepath.as_posix())
466
+ return
467
+ try:
468
+ remote_file = self.safe_project.get_template_file(filepath=filepath)
469
+ local_file = self.safe_project.load_file(filepath=filepath)
470
+ if remote_file is not None:
471
+ await self._update_template_file(remote_file, local_file, project_update=True)
472
+ else:
473
+ await self._create_template_file(file=local_file, project_update=True)
474
+ except Exception as e:
475
+ self.logger.error('Failed to update file %s: %s', filepath.as_posix(), e)
476
+
477
+ async def process_changes(self, changes: list[ChangeItem], force: bool):
478
+ self.changes_processor.clear()
479
+ try:
480
+ await self.changes_processor.process_changes(changes, force)
481
+ except Exception as e:
482
+ self.logger.error('Failed to process changes: %s', e)
483
+
484
+
485
+ class ChangesProcessor:
486
+
487
+ def __init__(self, tdk: TDKCore):
488
+ self.tdk: TDKCore = tdk
489
+ self.descriptor_change: ChangeItem | None = None
490
+ self.readme_change: ChangeItem | None = None
491
+ self.file_changes: list[ChangeItem] = []
492
+
493
+ def clear(self):
494
+ self.descriptor_change = None
495
+ self.readme_change = None
496
+ self.file_changes = []
497
+
498
+ def _split_changes(self, changes: list[ChangeItem]):
499
+ for change in changes:
500
+ if change[1] == self.tdk.safe_project.descriptor_path:
501
+ self.descriptor_change = change
502
+ elif change[1] == self.tdk.safe_project.used_readme:
503
+ self.readme_change = change
504
+ elif self.tdk.safe_project.is_template_file(change[1]):
505
+ self.file_changes.append(change)
506
+
507
+ async def _process_file_changes(self):
508
+ deleted = set()
509
+ updated = set()
510
+ for file_change in self.file_changes:
511
+ self.tdk.logger.debug('Processing: %s',
512
+ _change(file_change, self.tdk.safe_project.template_dir))
513
+ change_type = file_change[0]
514
+ filepath = file_change[1]
515
+ if change_type == watchfiles.Change.deleted and filepath not in deleted:
516
+ self.tdk.logger.debug('Scheduling delete operation')
517
+ deleted.add(filepath)
518
+ await self.tdk.delete_file(filepath)
519
+ elif filepath not in updated:
520
+ self.tdk.logger.debug('Scheduling update operation')
521
+ updated.add(filepath)
522
+ await self.tdk.update_file(filepath)
523
+
524
+ async def _reload_descriptor(self, force: bool) -> bool:
525
+ if self.descriptor_change is None:
526
+ return False
527
+ if self.descriptor_change[0] == watchfiles.Change.deleted:
528
+ raise RuntimeError(f'Deleted {self.tdk.safe_project.descriptor_path} ... the end')
529
+ self.tdk.logger.debug('Reloading %s file', TemplateProject.TEMPLATE_FILE)
530
+ previous_id = self.tdk.safe_project.safe_template.id
531
+ self.tdk.safe_project.load_descriptor()
532
+ self.tdk.safe_project.load_readme()
533
+ new_id = self.tdk.safe_project.safe_template.id
534
+ if new_id != previous_id:
535
+ self.tdk.logger.warning('Template ID changed from %s to %s',
536
+ previous_id, new_id)
537
+ self.tdk.safe_project.load()
538
+ await self.tdk.store_remote(force=force)
539
+ self.tdk.logger.info('Template fully reloaded... waiting for new changes')
540
+ return True
541
+ return False
542
+
543
+ async def _reload_readme(self) -> bool:
544
+ if self.readme_change is None:
545
+ return False
546
+ if self.readme_change[0] == watchfiles.Change.deleted:
547
+ raise RuntimeError(f'Deleted used README file {self.tdk.safe_project.used_readme}')
548
+ self.tdk.logger.debug('Reloading README file')
549
+ self.tdk.safe_project.load_readme()
550
+ return True
551
+
552
+ async def _update_descriptor(self):
553
+ if self.readme_change is not None or self.descriptor_change is not None:
554
+ self.tdk.logger.debug('Updating template descriptor (metadata)')
555
+ await self.tdk.update_descriptor()
556
+ self.tdk.safe_project.template = self.tdk.safe_template
557
+
558
+ async def process_changes(self, changes: list[ChangeItem], force: bool):
559
+ self._split_changes(changes)
560
+ full_reload = await self._reload_descriptor(force)
561
+ if not full_reload:
562
+ await self._reload_readme()
563
+ await self._update_descriptor()
564
+ await self._process_file_changes()
565
+ self.tdk.logger.info('All changes processed... waiting for new changes')