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/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')
|